39771 lines
1.6 MiB
Plaintext
Executable File
39771 lines
1.6 MiB
Plaintext
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import array
|
|
import base64
|
|
import csv
|
|
import fcntl
|
|
import grp
|
|
import hashlib
|
|
import http.client
|
|
import io
|
|
import json
|
|
import os
|
|
import pwd
|
|
import re
|
|
import secrets
|
|
import shutil
|
|
import signal
|
|
import socket
|
|
import sqlite3
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
|
|
EXPECTED_SELF = Path("/usr/local/sbin/nodedc-deploy")
|
|
DEPLOY_ROOT = Path("/volume1/docker/nodedc-deploy")
|
|
INBOX = DEPLOY_ROOT / "inbox"
|
|
APPLIED_DIR = DEPLOY_ROOT / "applied"
|
|
FAILED_DIR = DEPLOY_ROOT / "failed"
|
|
BACKUPS_DIR = DEPLOY_ROOT / "backups"
|
|
STATE_DIR = DEPLOY_ROOT / "state"
|
|
TMP_DIR = DEPLOY_ROOT / ".tmp"
|
|
RUNTIME_DIR = DEPLOY_ROOT / "runtime"
|
|
STATE_FILE = STATE_DIR / "applied.jsonl"
|
|
FAILED_STATE_FILE = STATE_DIR / "failed.jsonl"
|
|
LOCK_DIR = STATE_DIR / "deploy.lock"
|
|
DOCKER = Path("/usr/local/bin/docker")
|
|
MAP_GATEWAY_SECRET_DIR = Path("/volume1/docker/nodedc-platform/secrets")
|
|
MAP_GATEWAY_SECRET_FILE = MAP_GATEWAY_SECRET_DIR / "map-gateway-admin-secret"
|
|
MAP_EGRESS_PROXY_SECRET_FILE = MAP_GATEWAY_SECRET_DIR / "map-egress-proxy-token"
|
|
PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE = (
|
|
MAP_GATEWAY_SECRET_DIR / "device-core-internal-token"
|
|
)
|
|
PLATFORM_DEVICE_CORE_HUB_TRUST_REL = (
|
|
"platform/deployment/device-core-hub-trust-v1.json"
|
|
)
|
|
PLATFORM_DEVICE_CORE_HUB_TRUST_ENTRIES = (
|
|
"platform/docker-compose.platform-http.yml",
|
|
PLATFORM_DEVICE_CORE_HUB_TRUST_REL,
|
|
)
|
|
PLATFORM_DEVICE_MANAGER_PUBLIC_ROUTE_REL = (
|
|
"platform/deployment/device-manager-public-route-v1.json"
|
|
)
|
|
PLATFORM_DEVICE_MANAGER_PUBLIC_ROUTE_ENTRIES = (
|
|
"platform/Caddyfile.http",
|
|
PLATFORM_DEVICE_MANAGER_PUBLIC_ROUTE_REL,
|
|
)
|
|
LAUNCHER_DEVICE_CORE_SESSION_ENTRIES = (
|
|
"server/control-plane-store.mjs",
|
|
"server/dev-server.mjs",
|
|
"server/device-core-session-access.mjs",
|
|
"server/internal-request-auth.mjs",
|
|
"src/shared/api/adminApi.ts",
|
|
)
|
|
PROXY_CONTUR_ENV_FILE = Path("/volume1/docker/proxy-contur/.env")
|
|
DC_AMD_PROXY_RUNTIME_DIR = Path("/volume1/docker/dc-amd-proxy/runtime")
|
|
DEVICE_PLANE_ROOT = Path("/volume1/docker/nodedc-device-plane")
|
|
DEVICE_PLANE_SECRET_DIR = DEVICE_PLANE_ROOT / "secrets"
|
|
DEVICE_PLANE_MANAGER_DATA_DIR = DEVICE_PLANE_ROOT / "data" / "device-manager"
|
|
DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR = "/var/lib/nodedc-device-manager"
|
|
DEVICE_PLANE_MANAGER_PRESENTATION_PATH = (
|
|
f"{DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}/device-manager-presentation.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_MEDIA_ROOT = (
|
|
f"{DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}/media"
|
|
)
|
|
DEVICE_PLANE_POSTGRES_PASSWORD_FILE = DEVICE_PLANE_SECRET_DIR / "postgres-password"
|
|
DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE = DEVICE_PLANE_SECRET_DIR / "gateway-core-token"
|
|
DEVICE_PLANE_IDENTIFIER_PEPPER_FILE = DEVICE_PLANE_SECRET_DIR / "identifier-pepper"
|
|
DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE = (
|
|
DEVICE_PLANE_SECRET_DIR / "management-core-token"
|
|
)
|
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR = (
|
|
DEVICE_PLANE_SECRET_DIR / "device-edge-channel"
|
|
)
|
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR / "core-private-key.pem"
|
|
)
|
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR / "core-certificate.pem"
|
|
)
|
|
DEVICE_PLANE_EDGE_CHANNEL_PEER_TRUST_DIR = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR / "peers"
|
|
)
|
|
DEVICE_PLANE_EDGE_CHANNEL_RECOVERY_DIR = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR / "recovery"
|
|
)
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR = (
|
|
DEVICE_PLANE_ROOT / "enrollment" / "device-edge-channel"
|
|
)
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR / "core-certificate.pem"
|
|
)
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_FINGERPRINT_FILE = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR / "core-certificate.sha256"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE = "nodedc/device-control-core:local"
|
|
DEVICE_PLANE_GATEWAY_IMAGE = "nodedc/device-gateway:local"
|
|
DEVICE_PLANE_MANAGER_IMAGE = "nodedc/device-manager:local"
|
|
DEVICE_PLANE_BACKHAUL_TARGET_IMAGE = "nodedc/device-backhaul-target:local"
|
|
DEVICE_PLANE_BACKHAUL_FAILED_TARGET_REL = (
|
|
"deployment/device-plane-backhaul-target-v1.json"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_TARGET_REL = (
|
|
"deployment/device-plane-backhaul-target-tailnet-serve-v1.json"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL = (
|
|
"docker-compose.device-plane.backhaul-target.yml"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES = (
|
|
DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
|
"services/device-backhaul-target",
|
|
DEVICE_PLANE_BACKHAUL_TARGET_REL,
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_FAILED_TARGET_ENTRIES = (
|
|
DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
|
"services/device-backhaul-target",
|
|
DEVICE_PLANE_BACKHAUL_FAILED_TARGET_REL,
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_PATCH_ID = (
|
|
"device-plane-b2-discovery-loopback-20260803-006"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"25f9e9e55e283e9b7bb5e128ff14a244f848b1c063acca9724a23206131c9adf"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_SHA256 = (
|
|
"e10807cb3bab4c1d7ae2ac8ce4af1936717036930bb0b1c6f8f2b89c3f82b7c0"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_FAILED_PATCH_ID = (
|
|
"device-plane-backhaul-target-20260803-001"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT_SHA256 = (
|
|
"ed0bda4110a756c32be68990e2e0f647409d5a77eec7e26c18502bafbdc1bb76"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-plane-backhaul-target-20260803-001.tgz."
|
|
"20260804-035519"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_ID = (
|
|
"device-plane-device-plane-backhaul-target-20260803-001-"
|
|
"20260804-035519"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"7b26cf56c52723d9ce559d9f9216757038e4d3aa812821b5f05046bfcf8c01eb"
|
|
),
|
|
"files.txt": (
|
|
"761b9673fddb27558873a72f182d50ca46b2fa6808cbbeb03b40dba197a7586a"
|
|
),
|
|
"source-before.tgz": (
|
|
"b008e5d971527a7e4fccda314d677b6a6583e1a0a5e293cdc937fa2b8f19e10e"
|
|
),
|
|
"existing-files.txt": (
|
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
),
|
|
"missing-files.txt": (
|
|
"761b9673fddb27558873a72f182d50ca46b2fa6808cbbeb03b40dba197a7586a"
|
|
),
|
|
"runtime-before.json": (
|
|
"d45c54ebb286af8850b478b07410dd4ece1135aa9717cd8f0d4aa9c94c56e3d7"
|
|
),
|
|
}
|
|
DEVICE_PLANE_BACKHAUL_FAILED_MESSAGE = (
|
|
"container healthcheck failed for "
|
|
"2b6b1b324368421acee9b0945b18371bc500ece4ce99b2ff12da7da4ab157e49: "
|
|
"unhealthy"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_LOOPBACK_ADDRESS = "127.0.0.1"
|
|
DEVICE_PLANE_BACKHAUL_LISTEN_PORT = 2222
|
|
DEVICE_PLANE_BACKHAUL_TAILNET_ADDRESS = "100.109.216.21"
|
|
DEVICE_PLANE_BACKHAUL_TAILSCALE_SERVE_TARGET = "tcp://127.0.0.1:2222"
|
|
DEVICE_PLANE_TAILSCALE = Path(
|
|
"/var/packages/Tailscale/target/bin/tailscale"
|
|
)
|
|
DEVICE_PLANE_TAILSCALE_PRIVILEGE = Path(
|
|
"/var/packages/Tailscale/conf/privilege"
|
|
)
|
|
DEVICE_PLANE_TAILSCALE_USER = "tailscale"
|
|
DEVICE_PLANE_TAILSCALE_GROUP = "tailscale"
|
|
DEVICE_PLANE_BACKHAUL_PERMITTED_TARGET = "127.0.0.1:9921"
|
|
DEVICE_PLANE_BACKHAUL_ENROLLMENT_DIR = DEVICE_PLANE_ROOT / "enrollment"
|
|
DEVICE_PLANE_BACKHAUL_ENROLLMENT_PUBLIC_KEY_FILE = (
|
|
DEVICE_PLANE_BACKHAUL_ENROLLMENT_DIR / "device-edge-backhaul.pub"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PUBLIC_KEY_FILE = (
|
|
DEVICE_PLANE_BACKHAUL_ENROLLMENT_DIR / "device-edge-vps-backhaul.pub"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_REL = (
|
|
"deployment/device-plane-backhaul-vps-enrollment-v1.json"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_ENTRIES = (
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_REL,
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_PATCH_ID = (
|
|
"device-plane-backhaul-target-tailnet-serve-20260804-002"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"219408705dd4d80a962ed00eeb53a69df0b9ab6458443734d5c9cd1d1f795eba"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_FINGERPRINT = (
|
|
"SHA256:HHTiDYiCRxSiKjBLCip6JMSzGfLGrDz5g8SIkosJcVw"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_BACKUP = (
|
|
"device-plane-backhaul-authorized-keys-before"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_SECRET_DIR = DEVICE_PLANE_SECRET_DIR / "backhaul-target"
|
|
DEVICE_PLANE_BACKHAUL_HOST_KEY_FILE = (
|
|
DEVICE_PLANE_BACKHAUL_SECRET_DIR / "ssh_host_ed25519_key"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE = (
|
|
DEVICE_PLANE_BACKHAUL_SECRET_DIR / "authorized_keys"
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_TRUST_DIR = DEVICE_PLANE_ROOT / "trust" / "backhaul-target"
|
|
DEVICE_PLANE_BACKHAUL_HOST_PUBLIC_KEY_FILE = (
|
|
DEVICE_PLANE_BACKHAUL_TRUST_DIR / "ssh_host_ed25519_key.pub"
|
|
)
|
|
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL = (
|
|
"deployment/device-postgres-bootstrap-v1.json"
|
|
)
|
|
DEVICE_PLANE_POSTGRES_BOOTSTRAP_ENTRIES = (
|
|
"docker-compose.device-plane.yml",
|
|
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
|
|
)
|
|
DEVICE_PLANE_POSTGRES_VOLUME = "nodedc-device-plane-postgres-data"
|
|
DEVICE_PLANE_FOUNDATION_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"docker-compose.device-plane.yml",
|
|
"packages/device-protocol-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway",
|
|
)
|
|
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL = (
|
|
"deployment/device-manager-control-plane-v1.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL = (
|
|
"deployment/device-manager-control-plane-v2.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL = (
|
|
"deployment/device-manager-release-v1.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_REL = (
|
|
"deployment/device-manager-release-v2.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_REL = (
|
|
"deployment/device-manager-release-v3.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_REL = (
|
|
"deployment/device-manager-release-v4.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_REL = (
|
|
"deployment/device-manager-release-v5.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_REL = (
|
|
"deployment/device-manager-release-v6.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_REL = (
|
|
"deployment/device-manager-release-v7.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_REL = (
|
|
"deployment/device-manager-release-v8.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_REL = (
|
|
"deployment/device-manager-release-v9.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_REL = (
|
|
"deployment/device-manager-release-v10.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_REL = (
|
|
"deployment/device-manager-release-v11.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_REL = (
|
|
"deployment/device-manager-release-v12.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_REL = (
|
|
"deployment/device-manager-release-v13.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL = (
|
|
"deployment/device-edge-core-channel-bootstrap-v1.json"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL = (
|
|
"deployment/device-edge-core-channel-upgrade-v1.json"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL = (
|
|
"deployment/device-edge-core-channel-upgrade-v2.json"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL = (
|
|
"deployment/device-edge-core-channel-upgrade-v4.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL = (
|
|
"deployment/device-control-core-release-v1.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL = (
|
|
"deployment/device-control-core-release-v2.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL = (
|
|
"deployment/device-control-core-release-v3.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL = (
|
|
"deployment/device-control-core-release-v4.json"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL = (
|
|
"docker-compose.device-edge-core-channel.yml"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_SHA256 = (
|
|
"cac1e06b21202d8d96f5694b1adb1e67c0a3cdd31fcfca56cb7e19839c8516d8"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_BASE_COMPOSE_SHA256 = (
|
|
"eb1018cc0ffeaa0c019944d8810e2daa01eeca4c06289efff175785fb16457e7"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_BASE_COMPOSE_SHA256 = (
|
|
"6a4c08313cc97bbfd86c4e133c6b2e55ba18a499a3bb6f267c7059754f83e8cf"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"services/device-control-core",
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"services/device-control-core",
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"services/device-control-core",
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"docker-compose.device-plane.yml",
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"services/device-control-core",
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"services/device-control-core",
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"services/device-control-core",
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"packages/infrastructure-telemetry-contract",
|
|
"services/device-control-core",
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"packages/infrastructure-telemetry-contract",
|
|
"services/device-control-core",
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_REL = (
|
|
"deployment/device-control-core-release-v3-reconciliation-v1.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_ENTRIES = (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_PATCH_ID = (
|
|
"device-control-core-release-v3-reconciliation-20260822-042"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_FAILED_PATCH_ID = (
|
|
"device-control-core-release-v3-20260822-040"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-control-core-release-v3-20260822-040.tgz."
|
|
"20260822-184245"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT_SHA256 = (
|
|
"08448a56cdf391076f92c5242e368fd0033b420167874645eebfa1f22184ee92"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_BACKUP_ID = (
|
|
"device-plane-device-control-core-release-v3-20260822-040-"
|
|
"20260822-184245"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID = (
|
|
"sha256:31d35733ee46225b487c0f02a7b52d4ba2d13f5b99f6a717b7f5e6f5460b412a"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"eeb6ba544cde0d5d45a7bf16e921ca8969f0e5307872a53a5448d6a1279f76db"
|
|
),
|
|
"files.txt": (
|
|
"be1bda7d2afff11cfaeacd607a63261f99c17d6c04c8c762424669585ad9a75f"
|
|
),
|
|
"existing-files.txt": (
|
|
"a491b4a4ad7bc8dd8e8fc5b008f992663ef64e569b016efe7de1f4f14e5629ad"
|
|
),
|
|
"missing-files.txt": (
|
|
"d04dd94a0f5a1fd6a7809219e953ade89a43ec5a2e29016cb666f684a77323c0"
|
|
),
|
|
"source-before.tgz": (
|
|
"1c119875cccb52b761d1e2b42a6d666c9f9287782b424283b20180ea502237c4"
|
|
),
|
|
"runtime-before.json": (
|
|
"b694a183345181e32a3335bb50d73c4b8df9ce45e54072f3a8dc06789684fa89"
|
|
),
|
|
}
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_EXISTING = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"services/device-control-core",
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_MISSING = (
|
|
"packages/infrastructure-telemetry-contract",
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_REL = (
|
|
"deployment/device-control-core-incident-audit-v1.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_ENTRIES = (
|
|
DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_PATCH_ID = (
|
|
"device-control-core-incident-audit-20260822-043"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_REL = (
|
|
"deployment/device-control-core-migration-replay-audit-v1.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_ENTRIES = (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_PATCH_ID = (
|
|
"device-control-core-migration-replay-audit-20260822-045"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL = (
|
|
"deployment/device-control-core-migration-replay-recovery-v1.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL = (
|
|
"services/device-control-core/migrations/"
|
|
"014_device_registry_profile_commands.sql"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ENTRIES = (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID = (
|
|
"device-control-core-migration-replay-recovery-20260822-044"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT = (
|
|
"nodedc-device-plane-device-control-core-migration-replay-recovery-"
|
|
"20260822-044.tgz"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256 = (
|
|
"b893d8c90f98943797d32f486d6477d58a3be69eb1291e28c4a4bbd2e96774b7"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-control-core-migration-replay-recovery-"
|
|
"20260822-044.tgz.20260822-232936"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_FAILED_AT = (
|
|
"2026-08-22T20:29:59+00:00"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL = (
|
|
"deployment/"
|
|
"device-control-core-migration-replay-checkpoint-recovery-v2.json"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ENTRIES = (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL,
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_PATCH_ID = (
|
|
"device-control-core-migration-replay-checkpoint-recovery-20260822-046"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ARTIFACT = (
|
|
"nodedc-device-plane-device-control-core-migration-replay-checkpoint-"
|
|
"recovery-20260822-046.tgz"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ARTIFACT_SHA256 = (
|
|
"46000c76977fb583fc7c9cf74ecf624efd8b404f7b8d0322e0270e7b8ac6e450"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_BACKUP_ID = (
|
|
"device-plane-device-control-core-migration-replay-checkpoint-recovery-"
|
|
"20260822-046-20260823-002004"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"ca0d880ad8488a8e9d3b846f0f0b8e2d0a3e0df356b834ffdc1fedec65bb1acc"
|
|
),
|
|
"files.txt": (
|
|
"34cf294a6f5cc68ee7470356bac847221aa8457f8c90312b2eda17ec59b30155"
|
|
),
|
|
"existing-files.txt": (
|
|
"65452d59b095f05d550cafe9573ab8cad180d5c4799a353d67e6ac8e6366c7ad"
|
|
),
|
|
"missing-files.txt": (
|
|
"b15be462f0b40badb4a3c8b5922bd23fd93cb896b80394252f217c2b1102f1f3"
|
|
),
|
|
"source-before.tgz": (
|
|
"e27a9705c63a03158116530095466dcee8024ab1dee5ef26628e5691001d651e"
|
|
),
|
|
"runtime-before.json": (
|
|
"f74bfdb74efb85c7b57cb5e4032d161985b6da8e0208276e218dd7013d1bea6f"
|
|
),
|
|
}
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_PATCH_ID = (
|
|
"device-control-core-release-v4-20260823-047"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_BASE_PATCH_ID = (
|
|
"device-control-core-release-v2-20260822-038"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_BASE_ARTIFACT_SHA256 = (
|
|
"e2d062b82b022dba662522b5d6e192026ac964d78950d903295ca3cbbc95ab28"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256 = (
|
|
"751accf346b34d2774cc7b9572640d2c25fdb0b1db793ac32183b56f48e26508"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256 = (
|
|
"38bd86b42828d44c7101d5433ddc36018e92eedeee37b9de296432ad676edd46"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_FINAL_COMMAND_KINDS = (
|
|
"owner_scope.ensure",
|
|
"project.ensure",
|
|
"collection.ensure",
|
|
"project_grant.upsert",
|
|
"adapter_package.ensure",
|
|
"adapter_version.register",
|
|
"model_profile.register",
|
|
"edge.ensure",
|
|
"route.ensure",
|
|
"enrollment_intent.ensure",
|
|
"device.claim",
|
|
"device.update",
|
|
"device.transfer",
|
|
"discovery.reject",
|
|
"discovery.expire",
|
|
"device_credential_binding.upsert",
|
|
"device_credential_binding.revoke",
|
|
"device_binding.ensure",
|
|
"device_binding.revoke",
|
|
"device_configuration_revision.create",
|
|
"device_configuration_desired.set",
|
|
"asset.ensure",
|
|
"asset_binding.ensure",
|
|
"asset_binding.close",
|
|
"infrastructure_host.ensure",
|
|
"infrastructure_endpoint.ensure",
|
|
"infrastructure_deployment.ensure",
|
|
"infrastructure_service_instance.ensure",
|
|
"health_observation.record",
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_REPLAY_005_COMMAND_KINDS = (
|
|
"owner_scope.ensure",
|
|
"project.ensure",
|
|
"collection.ensure",
|
|
"project_grant.upsert",
|
|
"adapter_package.ensure",
|
|
"adapter_version.register",
|
|
"model_profile.register",
|
|
"edge.ensure",
|
|
"route.ensure",
|
|
"enrollment_intent.ensure",
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_REPLAY_007_COMMAND_KINDS = (
|
|
*DEVICE_PLANE_CONTROL_CORE_REPLAY_005_COMMAND_KINDS,
|
|
"device.claim",
|
|
"device.transfer",
|
|
"discovery.reject",
|
|
"discovery.expire",
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_REPLAY_009_COMMAND_KINDS = (
|
|
*DEVICE_PLANE_CONTROL_CORE_REPLAY_007_COMMAND_KINDS,
|
|
"device_credential_binding.upsert",
|
|
"device_credential_binding.revoke",
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_REPLAY_011_COMMAND_KINDS = (
|
|
"owner_scope.ensure",
|
|
"project.ensure",
|
|
"collection.ensure",
|
|
"project_grant.upsert",
|
|
"adapter_package.ensure",
|
|
"adapter_version.register",
|
|
"model_profile.register",
|
|
"edge.ensure",
|
|
"route.ensure",
|
|
"enrollment_intent.ensure",
|
|
"device.claim",
|
|
"device.transfer",
|
|
"discovery.reject",
|
|
"discovery.expire",
|
|
"device_credential_binding.upsert",
|
|
"device_credential_binding.revoke",
|
|
"device_binding.ensure",
|
|
"device_binding.revoke",
|
|
"device_configuration_revision.create",
|
|
"device_configuration_desired.set",
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_REPLAY_CHECKPOINTS = (
|
|
("replay-005", DEVICE_PLANE_CONTROL_CORE_REPLAY_005_COMMAND_KINDS),
|
|
("replay-007", DEVICE_PLANE_CONTROL_CORE_REPLAY_007_COMMAND_KINDS),
|
|
("replay-009", DEVICE_PLANE_CONTROL_CORE_REPLAY_009_COMMAND_KINDS),
|
|
("replay-011", DEVICE_PLANE_CONTROL_CORE_REPLAY_011_COMMAND_KINDS),
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_REPLAY_CHECKPOINT_PHASES = tuple(
|
|
phase for phase, _kinds in DEVICE_PLANE_CONTROL_CORE_REPLAY_CHECKPOINTS
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TRIGGER_KINDS = (
|
|
"asset.ensure",
|
|
"asset_binding.ensure",
|
|
"asset_binding.close",
|
|
"infrastructure_host.ensure",
|
|
"infrastructure_endpoint.ensure",
|
|
"infrastructure_deployment.ensure",
|
|
"infrastructure_service_instance.ensure",
|
|
"health_observation.record",
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-control-core-release-v3-reconciliation-"
|
|
"20260822-042.tgz.20260822-195448"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_ARTIFACT_SHA256 = (
|
|
"54ab243439bce724fa0a0872b76cc32e0052ea5127153214d92872f02ae831cf"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_BACKUP_ID = (
|
|
"device-plane-device-control-core-release-v3-reconciliation-"
|
|
"20260822-042-20260822-195448"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"37ed634d7ebde9d7da67990fe20cae7809a682c34652c37e163f9c2bc01c6a56"
|
|
),
|
|
"files.txt": (
|
|
"865da0d154344b2c00725f80ba904f968dfbe8da775a2a7542edc0ae04820ce7"
|
|
),
|
|
"existing-files.txt": (
|
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
),
|
|
"missing-files.txt": (
|
|
"865da0d154344b2c00725f80ba904f968dfbe8da775a2a7542edc0ae04820ce7"
|
|
),
|
|
"source-before.tgz": (
|
|
"12be8d9b8ed064c1d17b67d4a126128a962da0dd82aa34c66532d2d55a927124"
|
|
),
|
|
"runtime-before.json": (
|
|
"e65dee775511bc23f8b9238bb85bae7a1bbbc4a1d20d1329ae4b90ba60cef832"
|
|
),
|
|
}
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_PATCH_ID = (
|
|
"device-edge-core-channel-upgrade-v4-20260812-023"
|
|
)
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
|
"device-control-core-release-v2-20260821-030"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_PATCH_ID = (
|
|
"device-edge-core-channel-upgrade-v4-20260812-023"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v3-20260822-032"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"6e0eb3a0a6f19ceab92d46832b93bffbcea21247dbdc2ea50625a51ff460e4ca"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v4-20260822-033"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"52ba322042f1e4f595bbfea99f8bb35630b15984e0da648dc55348bc9e5b2066"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v5-20260822-034"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"acc1d2ae2cda66861054826928c25d01a2428e688cc8132a9c381831bf29ab5a"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v6-20260822-035"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"193faabe930e2b3f212f8eb45288e39f850ec714528b28881095be654baf9a80"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
|
"device-control-core-release-v2-20260822-036"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"8708cc4b59fa0cd5e9c6e6a7b2654ba01ea60271549167aca2631f94000d3da3"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v6-20260822-035"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"193faabe930e2b3f212f8eb45288e39f850ec714528b28881095be654baf9a80"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
|
"device-control-core-release-v2-20260822-038"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"e2d062b82b022dba662522b5d6e192026ac964d78950d903295ca3cbbc95ab28"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v8-20260822-039"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"30a83d4c6b5c029c96c4af19fa558e0ae75e4b60bc897881457304bd17e0e465"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
|
"device-control-core-release-v3-20260822-040"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"08448a56cdf391076f92c5242e368fd0033b420167874645eebfa1f22184ee92"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v8-20260822-039"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"30a83d4c6b5c029c96c4af19fa558e0ae75e4b60bc897881457304bd17e0e465"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
|
"device-control-core-release-v4-20260823-047"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v10-20260823-048"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"e6b983a314db4f8c27d89062dfedf5ed0523cc30421170799d181a19e2d85d4c"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
|
"device-control-core-release-v4-20260823-047"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v11-20260823-049"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"c1e2056b50bfbb0d03d077461d0c27cc56cc52967c3f5620be14871c8a6d5cf0"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
|
"device-control-core-release-v4-20260823-047"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-v12-20260823-050"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"1a49839140e5f2e49763d78f24ee47d946e244bcfde15a9c38266e8bd14c0d49"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
|
"device-control-core-release-v4-20260823-047"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = (
|
|
"device-edge-core-channel-bootstrap-20260812-018"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"5598b7388b491fe524ab46038ce476482a93a6cf07d8ca5e00206c69ded02931"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_PATCH_ID = (
|
|
"device-edge-core-channel-upgrade-20260812-019"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"8e9a220275959f378c1c4b00be5c7192e79afe2134eaab808a64e515870a8438"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_PATCH_ID = (
|
|
"device-edge-core-channel-upgrade-v2-20260812-021"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_PATCH_ID = (
|
|
"device-edge-core-channel-upgrade-v3-20260812-022"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT_SHA256 = (
|
|
"9e2b409a4b2d19711db434e90d03ac8e3db77bd74949f83cace7949f33caf613"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-edge-core-channel-upgrade-v3-"
|
|
"20260812-022.tgz.20260812-123620"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_BACKUP_ID = (
|
|
"device-plane-device-edge-core-channel-upgrade-v3-"
|
|
"20260812-022-20260812-123620"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_PATCH_ID = (
|
|
"device-manager-release-20260811-010"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"d4132993216eb674967dc6fc65d9670cfc2a9efdf46186ca019030f259de2d0e"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_PATCH_ID = (
|
|
"device-manager-release-20260811-016"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_ARTIFACT_SHA256 = (
|
|
"590405821b95b54088f926e0d3b2cdf9c704b339f6749da500c2bb64fe0e952d"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-manager-release-20260811-016.tgz."
|
|
"20260811-215941"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_BACKUP_ID = (
|
|
"device-plane-device-manager-release-20260811-016-20260811-215941"
|
|
)
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_INVALID_CERTIFICATE_FINGERPRINT = (
|
|
"56:16:E0:3A:F4:03:85:FD:42:86:85:AF:2A:AF:1E:90:16:C8:F7:91:"
|
|
"7C:AD:02:7D:B7:C2:ED:07:06:56:81:F6"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256 = (
|
|
"4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256 = (
|
|
"e7dff0f5873ad4586bd55946d3db2bb86092a5e149e886d120adc041e056c256"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_FAVICON_SHA256 = {
|
|
"favicon.ico": (
|
|
"f8933114a85646335ea5c94944f56d3cd8c48a6032016719f7905ff244ec0aa2"
|
|
),
|
|
"favicon/favicon.ico": (
|
|
"f8933114a85646335ea5c94944f56d3cd8c48a6032016719f7905ff244ec0aa2"
|
|
),
|
|
"favicon/icon-adaptive.svg": (
|
|
"481984e83997d786bb0a72ad1ee80037db13aef3a0792ab3109df95c2199b38e"
|
|
),
|
|
"favicon/apple-touch-icon.png": (
|
|
"afdccc28152a566e264e533ca218362f05d5bcec936c647f9a54f414c0bd4763"
|
|
),
|
|
"favicon/icon-192.png": (
|
|
"5b10a24feb4754f15c69761cef42f91a01f885d04095156b1a12e254875fdd4d"
|
|
),
|
|
"favicon/icon-512.png": (
|
|
"f98bac3dba59b7eefbe89f8bb8abc25567226a54ab7ed3b6b1a4caffbdd9ee15"
|
|
),
|
|
"favicon/manifest.webmanifest.json": (
|
|
"2a8ecdc6e6c64833f812ae02bbc0c7bd9b435e0cfa75cc21d6edf054d41275fc"
|
|
),
|
|
}
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256 = (
|
|
"369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9"
|
|
)
|
|
DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256
|
|
)
|
|
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway/package.json",
|
|
"services/device-edge-relay/package.json",
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway/package.json",
|
|
"services/device-edge-relay/package.json",
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway/package.json",
|
|
"services/device-edge-relay/package.json",
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway/package.json",
|
|
"services/device-edge-relay/package.json",
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway/package.json",
|
|
"services/device-edge-relay/package.json",
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_REL = (
|
|
"deployment/device-manager-control-plane-reconciliation-v1.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_FAILED_PATCH_ID = (
|
|
"device-manager-control-plane-20260810-001"
|
|
)
|
|
DEVICE_PLANE_MANAGER_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-manager-control-plane-20260810-001.tgz."
|
|
"20260811-000321"
|
|
)
|
|
DEVICE_PLANE_MANAGER_FAILED_ARTIFACT_SHA256 = (
|
|
"50e275c1085286bcb3bb2b273aefc8bbba70f446ca2c7bd464dc745710a291a6"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_PATCH_ID = (
|
|
"device-manager-control-plane-reconciliation-20260811-002"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT = (
|
|
"nodedc-device-plane-device-manager-control-plane-reconciliation-"
|
|
"20260811-002.tgz"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_ARTIFACT_SHA256 = (
|
|
"dd86dd58e4f649db0981db5089e003caf3961356179f2abb514662351487e1e6"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_APPLY_BACKUP_ID = (
|
|
"device-plane-device-manager-control-plane-reconciliation-"
|
|
"20260811-002-20260811-004839"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_BACKUP_ID = (
|
|
"device-plane-device-manager-control-plane-20260810-001-"
|
|
"20260811-000321"
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"70c8e6514f0a41cdd8892e29ca74c311c68e31bb74a7653d9b37e790db968526"
|
|
),
|
|
"files.txt": (
|
|
"2101d48cfb832f50e6112e443b4b61813e9e6d94c8171ca66fb23ed7f51bc575"
|
|
),
|
|
"existing-files.txt": (
|
|
"b605a86ce5ed6c0adb7b4cbeb5758795fe4219367df2c18db9411825f232e308"
|
|
),
|
|
"missing-files.txt": (
|
|
"84c546b7ddb0cd8c80b3de29f583ec48678bc9bb47ad0460860dc0022a6b87e9"
|
|
),
|
|
"source-before.tgz": (
|
|
"df6f29d173a56b0a067358d4b763c2de280450cf597957d1e79b8ae168e57735"
|
|
),
|
|
"runtime-before.json": (
|
|
"590310c9ad923516a714c7d47ba26eee2d82113cb800a5a7bff29a7de94f165d"
|
|
),
|
|
}
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_EXISTING = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"packages/device-protocol-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway/package.json",
|
|
"services/device-edge-relay/package.json",
|
|
)
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_MISSING = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL = (
|
|
"deployment/device-manager-control-plane-v2-reconciliation-v1.json"
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ENTRIES = (
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID = (
|
|
"device-manager-control-plane-20260811-003"
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-manager-control-plane-20260811-003.tgz."
|
|
"20260811-012505"
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256 = (
|
|
"ba29618ffbfed55448768794f28b18dda439ddb39a1d2a4f1dece19de7f29990"
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_PATCH_ID = (
|
|
"device-manager-control-plane-v2-reconciliation-20260811-004"
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID = (
|
|
"device-plane-device-manager-control-plane-20260811-003-"
|
|
"20260811-012505"
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"4f7535e5eeee0bc3339173c2425787c153528d26f2c60f6cdd386ad3359e05cf"
|
|
),
|
|
"files.txt": (
|
|
"191e81991258673881435083e4a6733062ec0e30afb9b80a875c1a6581dd18a2"
|
|
),
|
|
"existing-files.txt": (
|
|
"b605a86ce5ed6c0adb7b4cbeb5758795fe4219367df2c18db9411825f232e308"
|
|
),
|
|
"missing-files.txt": (
|
|
"c1ab9da97b257b467fd222eda0e34b5a7b29b628c71f10a5f4069b2991b3d39d"
|
|
),
|
|
"source-before.tgz": (
|
|
"32d6d6341e410806bcff7007c5eacec34090bc337483222f396f19ca2edad9be"
|
|
),
|
|
"runtime-before.json": (
|
|
"af8f2c71caeedcd61ebf02de98ff6c61d35c832fbd446a6b38a7e7c16150c2bd"
|
|
),
|
|
}
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_EXISTING = (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"packages/device-protocol-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway/package.json",
|
|
"services/device-edge-relay/package.json",
|
|
)
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_MISSING = (
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
"services/device-manager",
|
|
DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_REL,
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_REL = (
|
|
"deployment/device-plane-foundation-recovery-v1.json"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_ENTRIES = (
|
|
*DEVICE_PLANE_FOUNDATION_ENTRIES,
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_REL,
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_REL = (
|
|
"deployment/device-plane-foundation-network-publication-v1.json"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_ENTRIES = (
|
|
*DEVICE_PLANE_FOUNDATION_ENTRIES,
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_REL,
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL = (
|
|
"deployment/device-plane-b2-discovery-ingress-v1.json"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_ENTRIES = (
|
|
*DEVICE_PLANE_FOUNDATION_ENTRIES,
|
|
"services/device-edge-relay/package.json",
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL,
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_PREDECESSOR_PATCH_ID = (
|
|
"device-plane-foundation-network-publication-20260725-003"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"6fdd5a12c310786db1753882fc1378184fe378d2cc533633a8c73c951521b7bf"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_COMPOSE_SHA256 = (
|
|
"eb1018cc0ffeaa0c019944d8810e2daa01eeca4c06289efff175785fb16457e7"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_REL = (
|
|
"deployment/device-plane-b2-discovery-loopback-recovery-v1.json"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_ENTRIES = (
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_REL,
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_FAILED_ENTRIES = (
|
|
*DEVICE_PLANE_FOUNDATION_ENTRIES,
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL,
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_FAILED_PATCH_ID = (
|
|
"device-plane-b2-discovery-loopback-20260801-003"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-plane-b2-discovery-loopback-"
|
|
"20260801-003.tgz.20260802-154311"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_FAILED_ARTIFACT_SHA256 = (
|
|
"7273c5bf67fe6bc1f1da66ad726009240d39ee3aee58201b96c23d6f707a3d84"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_BACKUP_ID = (
|
|
"device-plane-device-plane-b2-discovery-loopback-20260801-003-"
|
|
"20260802-154311"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"2d93219283cb2f7ca65a8349c84a1d6ff0568bea33d564a9acb5286df0061c93"
|
|
),
|
|
"files.txt": (
|
|
"596a364ea56b92789b4ba9f9e6d2628e5a7a56a1380abfc3124c08e08ff9d6ae"
|
|
),
|
|
"existing-files.txt": (
|
|
"3b7dda5cc42ce460e4a74f6df986d4a4fa8aa28511e9d9b123deb445a0382083"
|
|
),
|
|
"missing-files.txt": (
|
|
"fc252f30b3ea51aafcf8002d64de077b33dcfd20e77fa05cb88457876780b620"
|
|
),
|
|
"runtime-before.json": (
|
|
"4171d00e5c8a8db9d26042241ff50c70a0aa96cfdbdd2eeb596f0e860349e6a2"
|
|
),
|
|
"source-before.tgz": (
|
|
"1cbe3b31be02ce733afe17f3dd391708787f0b3ffcea038d0619316e831f7e34"
|
|
),
|
|
}
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_PATCH_ID = (
|
|
"device-plane-b2-discovery-loopback-recovery-20260802-004"
|
|
)
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_ARTIFACT_SHA256 = (
|
|
"0888a6ff3404772179d74b46919fd3f193adacedc40944d20349a37d262974aa"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_FAILED_PATCH_ID = (
|
|
"device-plane-foundation-20260725-001"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-plane-foundation-20260725-001.tgz."
|
|
"20260725-223441"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT_SHA256 = (
|
|
"23d428de547854ad8b1a026671e2f850386ab0be98bde80f016f1e9db631ee24"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_ID = (
|
|
"device-plane-device-plane-foundation-20260725-001-20260725-223441"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"21ff4250147f99fd1461191e1f05aee99e92da9cdedce4513749046c26327c77"
|
|
),
|
|
"files.txt": (
|
|
"3b7dda5cc42ce460e4a74f6df986d4a4fa8aa28511e9d9b123deb445a0382083"
|
|
),
|
|
"existing-files.txt": (
|
|
"f2e2c6e7501d3a383cd7e909a2e4e4cfbb3b0cda132f508f54e478bfb455585b"
|
|
),
|
|
"missing-files.txt": (
|
|
"308d86656e0a0863d5fa16833ec67d1698c0cef7e009a7bd987e62a9e6469c63"
|
|
),
|
|
"source-before.tgz": (
|
|
"df451cdf7a5798c332d03f15473358b1e7fdb7b96212037aaed2d8d00dfecf68"
|
|
),
|
|
}
|
|
DEVICE_PLANE_FOUNDATION_PREDECESSOR_COMPOSE_SHA256 = (
|
|
"e3afbb1ab252914e21dfb0acc1a2016fd8af471aa13bb14fb578f106f2cdae36"
|
|
)
|
|
DEVICE_PLANE_POSTGRES_BOOTSTRAP_DESCRIPTOR_SHA256 = (
|
|
"9475f12d151aee386cba74a4c0f9e7485416d5616d3ad71dfd8963fe114abf61"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_IMAGE_IDS = {
|
|
"device-control-core": (
|
|
"sha256:78a9fdfbe46f6e9531536abb997ae105fc7ea5c9a8176cb88ca8c0644900c254"
|
|
),
|
|
"device-gateway": (
|
|
"sha256:9be91a3496d1ac627b6b621ade0c2ae77afa10e9ddb9b2071823769bc5a5a472"
|
|
),
|
|
"device-postgres": (
|
|
"sha256:0001755f47e38bfd163788ccc543387b7f3da35a01b38a7c2ca1cd0277f06b5d"
|
|
),
|
|
}
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_PATCH_ID = (
|
|
"device-plane-foundation-recovery-20260725-002"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT = (
|
|
"nodedc-device-plane-device-plane-foundation-recovery-20260725-002.tgz."
|
|
"20260725-232447"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT_SHA256 = (
|
|
"9183cc385142584bfd12510bb0a3e6b833b2fd26607436f2486a564c628ea1bf"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_BACKUP_ID = (
|
|
"device-plane-device-plane-foundation-recovery-20260725-002-"
|
|
"20260725-232447"
|
|
)
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_BACKUP_SHA256 = {
|
|
"manifest.env": (
|
|
"e1c31f28474d66c33f8b91e890c112e605ffe376ed3e7292d2f8504ea18fd9c1"
|
|
),
|
|
"files.txt": (
|
|
"bb0e3deec5952811dd413afdf24d116e5bf3fd3e041f78bfe5cc5ca87166b8ff"
|
|
),
|
|
"existing-files.txt": (
|
|
"f2e2c6e7501d3a383cd7e909a2e4e4cfbb3b0cda132f508f54e478bfb455585b"
|
|
),
|
|
"missing-files.txt": (
|
|
"e57cda977037cabbe01de04ece1a1e9f15cb9eb3c0ad2996811d6ea3d4f4b3d2"
|
|
),
|
|
"runtime-before.json": (
|
|
"f500964078c5814d2130fe57b5c67fb89dea000355591836e1b72088b37170ef"
|
|
),
|
|
"source-before.tgz": (
|
|
"eed474b04608e206f09ebe68ca80da675a2318fa3efaf3be90f2d983429f9d9f"
|
|
),
|
|
}
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_COMPOSE_SHA256 = (
|
|
"47d4d153d27bc6f418c1f787ea5ff0153b1ee082f96b92e28486805cb46b3f15"
|
|
)
|
|
DEVICE_PLANE_PRIVATE_NETWORK = "nodedc-device-plane-private"
|
|
DEVICE_PLANE_CONTROL_NETWORK = "nodedc-device-plane-control"
|
|
DEVICE_PLANE_EGRESS_NETWORK = "nodedc-device-plane-egress"
|
|
DEVICE_PLANE_FOUNDATION_PREDECESSOR_CONTAINER_IDS = {
|
|
"device-control-core": (
|
|
"ccdaeee71472a557ccecb485e81b31f5a4ee37b1b3f3cb4d5edbcf12d02bb08b"
|
|
),
|
|
"device-gateway": (
|
|
"d59258eb86ae8ec728ceaaed5b4976dd1359bdcecd6abc7950e73ffabf1d8eba"
|
|
),
|
|
"device-postgres": (
|
|
"44702b995b4397131646ed8076144efd116cb38d00d9a3d641252b7ad635d7a1"
|
|
),
|
|
}
|
|
DEVICE_PLANE_RUNTIME_SERVICES = (
|
|
"device-control-core",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
)
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE = "device-backhaul-target"
|
|
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "external-data-plane-provisioner"
|
|
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / "token"
|
|
ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / "engine-credential-provisioner-ed25519.pem"
|
|
ENGINE_CREDENTIAL_SINK_STATE_DIR = Path("/volume2/nodedc-demo/nodedc-data/engine-credential-sink")
|
|
ENGINE_CREDENTIAL_SINK_PUBLIC_KEY_FILE = ENGINE_CREDENTIAL_SINK_STATE_DIR / "issuer-public-key.pem"
|
|
ENGINE_CREDENTIAL_PROVISIONER_KEY_ID = "engine-credential-provisioner-20260716-001"
|
|
ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE = "node:20-alpine"
|
|
ENGINE_CREDENTIAL_BACKEND_IMAGE = "nodedc/engine-backend:credential-sink-20260716-001"
|
|
ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256 = "254f9086142eb469a0bcfcb393330a126e4d128a0df7455f0ac71d52b95e770b"
|
|
ENGINE_CREDENTIAL_BACKEND_NODE_MODULES_DIR = Path("/volume2/nodedc-demo/nodedc-backend-node_modules")
|
|
ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL = "nodedc-source/services/backend/credential-sink/docker-compose.immutable-runtime.yml"
|
|
ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR = RUNTIME_DIR / "engine-credential-sink"
|
|
ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "docker-compose.immutable-runtime.yml"
|
|
ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "canonical-rootfs.tar"
|
|
ENGINE_CREDENTIAL_BACKEND_METADATA_FILE = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "runtime-metadata.json"
|
|
ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "activation.ready"
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL = "nodedc-source/services/backend/data-product-publish-grant/docker-compose.immutable-runtime.yml"
|
|
ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL = "nodedc-source/services/backend/data-product-read-grant/docker-compose.immutable-runtime.yml"
|
|
ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL = "nodedc-source/server/engineAgents/store.js"
|
|
ENGINE_AGENT_FULL_GRANT_MIGRATION_PREDECESSOR_SHA256 = "52daa43499d6d9a97fe7ffa891edb9212b7791e733f91dd3ca686d42739b7e9a"
|
|
ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256 = "2e62654c2dc12905efcc83a9dff45a818dd7b47924a10600160835c4416540e9"
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_COMPOSE_SHA256 = "258cebb64ff1943c939655cc55bdce00fc5c4dced67ec291d84d6df066ace50e"
|
|
ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "engine-edp-managed-provisioner"
|
|
ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE = ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR / "private-key.pem"
|
|
ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR = Path("/volume1/docker/nodedc-platform/trust/engine-managed-provisioner")
|
|
ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR / "public-key.pem"
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "foundry-edp-managed-provisioner"
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR / "private-key.pem"
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR = Path("/volume1/docker/nodedc-platform/trust/foundry-managed-provisioner")
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR / "public-key.pem"
|
|
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR = MAP_GATEWAY_SECRET_DIR / "external-data-plane-reader-grants"
|
|
FOUNDRY_BINDING_GRANTS_DIR = MAP_GATEWAY_SECRET_DIR / "foundry-binding-grants"
|
|
N8N_PRIVATE_EXTENSION_RELEASES_ROOT = Path("/volume1/docker/nodedc-platform/n8n-private-extensions")
|
|
ENGINE_N8N_SEALED_RELEASES_ROOT = Path("/volume2/nodedc-demo/n8n-private-extensions")
|
|
ENGINE_N8N_TRANSITION_DESCRIPTOR_REL = "nodedc-source/services/n8n/private-extensions/ndc-activation.json"
|
|
ENGINE_N8N_COMPOSE_OVERRIDE_REL = "nodedc-source/services/n8n/private-extensions/docker-compose.ndc-private-extension.yml"
|
|
ENGINE_N8N_SCHEMA_ROOT_REL = "nodedc-source/server/assets/n8n/schema/v2.3.2"
|
|
ENGINE_N8N_NODES_CATALOG_REL = f"{ENGINE_N8N_SCHEMA_ROOT_REL}/nodes.catalog.json"
|
|
ENGINE_N8N_CREDENTIALS_CATALOG_REL = f"{ENGINE_N8N_SCHEMA_ROOT_REL}/credentials.catalog.json"
|
|
ENGINE_N8N_SCHEMA_META_REL = f"{ENGINE_N8N_SCHEMA_ROOT_REL}/meta.json"
|
|
ENGINE_N8N_ICON_REL = "nodedc-source/server/assets/n8n/icons/ndc.svg"
|
|
ENGINE_N8N_DARK_ICON_REL = "nodedc-source/server/assets/n8n/icons/ndc.dark.svg"
|
|
ENGINE_NODE_INTELLIGENCE_RELEASE_ID = "2.33.2-974a9fb3492f"
|
|
ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT = "974a9fb3492fe2c4984ee0549085d531cdc6242a"
|
|
ENGINE_NODE_INTELLIGENCE_IMAGE = (
|
|
f"nodedc/engine-node-intelligence:{ENGINE_NODE_INTELLIGENCE_RELEASE_ID}"
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_SERVICE = "nodedc-node-intelligence"
|
|
ENGINE_NODE_INTELLIGENCE_RUNTIME_UID = 11007
|
|
ENGINE_NODE_INTELLIGENCE_RUNTIME_GID = 11007
|
|
ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR = RUNTIME_DIR / "engine-node-intelligence"
|
|
ENGINE_NODE_INTELLIGENCE_SECRET_FILE = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR / "auth-token"
|
|
ENGINE_NODE_INTELLIGENCE_RELEASES_DIR = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR / "releases"
|
|
ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH = (
|
|
"/run/nodedc-secrets/engine-node-intelligence-auth-token"
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_SOURCE_REL = "nodedc-source/server/nodeIntelligence"
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL = "nodedc-source/server/routes/engineAgentGateway.js"
|
|
ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL = "nodedc-source/services/node-intelligence"
|
|
ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL = (
|
|
f"{ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL}/docker-compose.immutable-runtime.yml"
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL = (
|
|
f"{ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL}/activation.json"
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL = (
|
|
f"{ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL}/image/engine-node-intelligence.tar"
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256 = (
|
|
"6b8c80fa997ef7c438d199a6ee942c5e37aebc4057667af417897fa636131db4"
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_PREDECESSOR_COMPOSE_SHA256 = (
|
|
"258cebb64ff1943c939655cc55bdce00fc5c4dced67ec291d84d6df066ace50e"
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES = (
|
|
ENGINE_NODE_INTELLIGENCE_SOURCE_REL,
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
|
|
ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL,
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES = (
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
|
|
ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL,
|
|
)
|
|
ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL = ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL
|
|
ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json",
|
|
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.4.tgz",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js",
|
|
"nodedc-source/server/dataProductPublishGrant/signedDataPlaneClient.js",
|
|
"nodedc-source/server/dataProductReadGrant/acceptance.js",
|
|
"nodedc-source/server/dataProductReadGrant/service.js",
|
|
"nodedc-source/server/dataProductReadGrant/store.js",
|
|
"nodedc-source/server/engineAgents/store.js",
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js",
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/server/routes/ndcAgentMcp.js",
|
|
ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL,
|
|
"nodedc-source/server/dataProductPublishGrant/service.js",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js",
|
|
ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_CONTROL_PLANE_PREDECESSOR_SHA256 = {
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "604a75ad3b9b463cea5e578760c8a23592e2e00b8acfc0099730a347e27bf4e8",
|
|
"nodedc-source/server/routes/ndcAgentMcp.js": "534e2c85e1faecc72a00da7ad32d0584ee09b6bd89eeec2221c3b23d80e1e962",
|
|
"nodedc-source/server/routes/n8n.js": "de6e3c76740af86e2eaeedede140b5ee54eb526f03db270324d7a4fe513f6817",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "a96d3695fdb3ed7012b9041182f37b09e32d1dfa0dbb391d8db2d7d25a39d64d",
|
|
"nodedc-source/server/dataProductPublishGrant/signedDataPlaneClient.js": "1a26728ac69c5ef3fdce1ce1154a6ce330f72286b6e07825796a246070d15bbf",
|
|
"nodedc-source/server/engineAgents/store.js": "2e62654c2dc12905efcc83a9dff45a818dd7b47924a10600160835c4416540e9",
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js": "946353ed8582c5d96e08361603425cc45eaf231f730ae46a75028d3bef352ee9",
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "7d3d7a9bc48614e6cd238199caa384073096b7ec8273703833681151b464ef17",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json": "9f299b20db4855b1bb4a9bd2e1095dfd059cd90ec82e936884a380aa41249e28",
|
|
"nodedc-source/server/dataProductPublishGrant/service.js": "85ded82d707e0ef5b1ad739b5a9be24c1432e2dd9e5051108739f09daf20f47c",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js": "ef2a5e6bfad9f0b1710db006a0d30d01cd9bcaf06524b0e9f975dd771efe3952",
|
|
}
|
|
ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256 = {
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15",
|
|
"nodedc-source/server/routes/ndcAgentMcp.js": "534e2c85e1faecc72a00da7ad32d0584ee09b6bd89eeec2221c3b23d80e1e962",
|
|
"nodedc-source/server/routes/n8n.js": "f293a7794405badbabd2bf7ef088f96fe1167d9e249f05cbfc8af280a0d3e8f8",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "901b8fad80018ce177b34ced804b39cb140a47e831414057f484296b373c651d",
|
|
"nodedc-source/server/dataProductPublishGrant/signedDataPlaneClient.js": "c2a13d5eb49937fe70ec6451d0465cac54c19c7fae44448db099079473ec02fc",
|
|
"nodedc-source/server/dataProductReadGrant/acceptance.js": "202dbe575b2f2e1c584aa7e6e99e38fa84f12496feb454e3dbd3f98e2d0396dd",
|
|
"nodedc-source/server/dataProductReadGrant/service.js": "65b8cdd6b603333bac1feb303e9791feb1e9da39dc9b5bfc3df3961273b0052e",
|
|
"nodedc-source/server/dataProductReadGrant/store.js": "c3fcdc59a794f57d92e3d2ac713ee28341347cebd25364c4060beaa3dc2151de",
|
|
"nodedc-source/server/engineAgents/store.js": "debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f",
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js": "761a874b102a938bc6018159ddacdaac71ad6ae08e9f0f8d7f3b58a0165a5131",
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "521098de69fe288a56bd4158c849c1055ba24be08e19f7a4111618d0e8138445",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json": "cbe113e9b10bb84b9ccbffa3e261908e58a8430c11abe2cf4fd5304741b04597",
|
|
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.4.tgz": "e74c0136d346f904b42589d75cb11a952b4d2f21153f1b057fba28e9acf4f95f",
|
|
ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL: "952df0cb2ac477e644f5f3a9d64b4872ce1da33356eeab0bec17dcb2931cf653",
|
|
"nodedc-source/server/dataProductPublishGrant/service.js": "ded3832c9677345a988448ae8e69cef254fd9bf39d2de20cffa295c1feedcd7c",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js": "a92303b2732e21f68c1ac732fa26e4983cbadf3515741cee983b916a283d754c",
|
|
}
|
|
ENGINE_MCP_CONTROL_PLANE_NEW_PATHS = (
|
|
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.4.tgz",
|
|
"nodedc-source/server/dataProductReadGrant/acceptance.js",
|
|
"nodedc-source/server/dataProductReadGrant/service.js",
|
|
"nodedc-source/server/dataProductReadGrant/store.js",
|
|
ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL,
|
|
)
|
|
ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json",
|
|
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js",
|
|
"nodedc-source/server/engineAgents/store.js",
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
|
|
ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_ONTOLOGY_SDK_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "521098de69fe288a56bd4158c849c1055ba24be08e19f7a4111618d0e8138445",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json": "cbe113e9b10bb84b9ccbffa3e261908e58a8430c11abe2cf4fd5304741b04597",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "30f89052b6557a2444550bd6e7eed30747b2d9b7ff3a22d079d9905fe0a04702",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "901b8fad80018ce177b34ced804b39cb140a47e831414057f484296b373c651d",
|
|
"nodedc-source/server/engineAgents/store.js": "debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f",
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15",
|
|
}
|
|
ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256 = {
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "adff3e474c914680f7d36b204d1dc03e69dc8065fff1f80b6713526cfc970137",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json": "0741647e4f7f58f69f3021d367484609a8e1eb7b8727d6ad9639bd9906b7f455",
|
|
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz": "72a1b2d41a12a298eaae63ba3334c7c66b6ca53de6a3f03a48607d4ff6da42fb",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "4800e1a1c2af5e4893b1a403c04e91f55689383457caff833edc9e35e8e7e37a",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "d5511a8bd3b4238af88a89537661c9ca4c0126bca7b5ad225985e27ccdb2c64c",
|
|
"nodedc-source/server/engineAgents/store.js": "4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285",
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "25fa013ddbfd7d0c7ece8c792daec5f345062757c85eb56cfbff45bf1e812152",
|
|
}
|
|
ENGINE_MCP_ONTOLOGY_SDK_NEW_PATHS = (
|
|
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz",
|
|
)
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json",
|
|
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/engineAgents/store.js",
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
|
|
ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "521098de69fe288a56bd4158c849c1055ba24be08e19f7a4111618d0e8138445",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json": "cbe113e9b10bb84b9ccbffa3e261908e58a8430c11abe2cf4fd5304741b04597",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "17f3e368f3264cbbd708965c9e1fd735aa974f15a1383bf88cd6d14a43dbf32d",
|
|
"nodedc-source/server/engineAgents/store.js": "debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f",
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15",
|
|
}
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256 = {
|
|
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "c15c9da4f90f44a4e9e12f3683127e906d614fb98e562faa0c939505c973e074",
|
|
"nodedc-source/server/assets/engine-agent-npm/package.json": "2ca8dcab0fa04bb1b21add1f75a9be61d5aa97753443ee1917302b4e0da5780a",
|
|
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz": "d007a81cb4e4af569b3c54d3869b0f60b5597e3b531bff232145fa1851d8572a",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "63e0741646197f0b1b3c64a4095e1bc8fb3a95ee6caf20b0293f89d869c9e620",
|
|
"nodedc-source/server/engineAgents/store.js": "4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285",
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "5331f6dc8dc306f641370a2968eb025ee3e278e27ae9a217e4cfc2901fec369c",
|
|
}
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_NEW_PATHS = (
|
|
"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 = (
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json"
|
|
)
|
|
ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES = (
|
|
ENGINE_PROVIDER_SECURITY_CATALOG_REL,
|
|
)
|
|
ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256 = (
|
|
"dd67d8b01091072a4e7be3820b9639c12f2f897ccbbd79208123564d10c1cd81"
|
|
)
|
|
ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256 = (
|
|
"773335bb616a5c03eb2108c4f53ef092c02e75ee75f014511201bc12e2956b27"
|
|
)
|
|
ENGINE_CONTROL_PLANE_STATE_REL = "nodedc-control-plane"
|
|
ENGINE_PUBLISH_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/publish-grants"
|
|
ENGINE_READ_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/read-grants"
|
|
ENGINE_CONTROL_PLANE_STATE_PATH = Path("/volume2/nodedc-demo/nodedc-control-plane")
|
|
ENGINE_PUBLISH_GRANT_STATE_PATH = ENGINE_CONTROL_PLANE_STATE_PATH / "publish-grants"
|
|
ENGINE_READ_GRANT_STATE_PATH = ENGINE_CONTROL_PLANE_STATE_PATH / "read-grants"
|
|
ENGINE_EDP_PRIVATE_KEY_CONTAINER_PATH = "/run/nodedc-secrets/engine-edp-managed-provisioner-private.pem"
|
|
ENGINE_CONTROL_PLANE_CONTAINER_PATH = "/var/lib/nodedc-control-plane"
|
|
ENGINE_PUBLISH_GRANT_CONTAINER_PATH = "/var/lib/nodedc-control-plane/publish-grants"
|
|
ENGINE_READ_GRANT_CONTAINER_PATH = "/var/lib/nodedc-control-plane/read-grants"
|
|
EXTERNAL_DATA_PLANE_MANAGED_TRUST_CONTAINER_PATH = "/run/nodedc-trust/engine-managed-provisioner"
|
|
EXTERNAL_DATA_PLANE_INTERNAL_URL = "http://external-data-plane:18106"
|
|
PLATFORM_EXTERNAL_DATA_PLANE_COMPOSE_REL = "platform/docker-compose.external-data-plane.yml"
|
|
EXTERNAL_DATA_PLANE_DATABASE_SERVICE = "external-data-plane-postgres"
|
|
EXTERNAL_DATA_PLANE_SERVICE = "external-data-plane"
|
|
EXTERNAL_DATA_PLANE_IMAGE = "nodedc/external-data-plane:local"
|
|
ENGINE_N8N_VERSION = "2.3.2"
|
|
ENGINE_N8N_BASE_IMAGE = "docker.n8n.io/n8nio/n8n:2.3.2"
|
|
ENGINE_N8N_BASE_ARCHITECTURE = "amd64"
|
|
ENGINE_N8N_RUNTIME_PACKAGE_PATH = "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc"
|
|
ENGINE_N8N_NODE_MODULES_PATH = "/usr/local/lib/node_modules/n8n/node_modules"
|
|
ENGINE_N8N_ICON_SHA256 = "1c928fc996d8b82121e8f8e327d74b1dd21556c106de99c5e8d317d926c0ba17"
|
|
ENGINE_N8N_DARK_ICON_SHA256 = "ef3b8551da4ce04527736405afa9a220a2c76d047bd18f6f7124b9adb4216b0c"
|
|
ENGINE_N8N_RELEASE_CATALOG_JSON_SHA256 = {
|
|
"0.1.2-05e4b38b14b4a019": {
|
|
"nodes": "230f712230f7ee8debaa4b44a4358cc19c205bc43305d8ed89d5e3daac466506",
|
|
"credentials": "a26824ffc0e15db857c792153de3417febc0dbba4880380d6c8cd544b3c80f4d",
|
|
"meta": "caf7635420c61333e65f68350f5567217aed96fb51354b38764bcac2c24e7966",
|
|
},
|
|
"0.1.3-3354149b5245e39a": {
|
|
"nodes": "b0a5215699a6e691b8cfc505ab457d5632ef6d83adb3537520d0b60760ba16b2",
|
|
"credentials": "a26824ffc0e15db857c792153de3417febc0dbba4880380d6c8cd544b3c80f4d",
|
|
"meta": "b8aa60c0d919573626fa0694a1e4ae9ede015325a5d312e3e05d5ac293084aa7",
|
|
},
|
|
"0.1.4-59dc9f7882721d6a": {
|
|
"nodes": "b0a5215699a6e691b8cfc505ab457d5632ef6d83adb3537520d0b60760ba16b2",
|
|
"credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e",
|
|
"meta": "8fadbc594c5d14b1d53ab69b677f00823949410b9a37f4e43ecc321e89d68983",
|
|
},
|
|
"0.1.5-3c8ae53f010d7c88": {
|
|
"nodes": "fb2ef4d38b9c071fae117a27381714658a1a19334b82fc79d64d13f27555fe5a",
|
|
"credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e",
|
|
"meta": "b324592c38d518af8320d8540c7061e4684056ae88a676cf5e50c6eef03c0dd7",
|
|
},
|
|
"0.1.6-25cc2d7a52a0d0ee": {
|
|
"nodes": "fb2ef4d38b9c071fae117a27381714658a1a19334b82fc79d64d13f27555fe5a",
|
|
"credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e",
|
|
"meta": "58adcb182e5f231a4798636a99887a1f214e3c6e1eb4c0dc2d9b2f3aa80a99ef",
|
|
},
|
|
}
|
|
ENGINE_N8N_INACTIVE_NODES_CATALOG_JSON_SHA256 = "b70d9d8130d498c55de46a5d0758c844f1242b70803457b2291ab3a9de8056f2"
|
|
ENGINE_N8N_INACTIVE_CREDENTIALS_CATALOG_JSON_SHA256 = "680e9f52aac791efbd38e3bd99bd51ef5cded9756d867897c6c2755850e87b50"
|
|
ENGINE_N8N_INACTIVE_META_JSON_SHA256 = "0ac555d7ab31cb0ca042db4f474e83cfefbf20b5bbb2e1509fead27ed21e8acb"
|
|
MAP_GATEWAY_RUNTIME_GID = 1000
|
|
EXTERNAL_DATA_PLANE_RUNTIME_UID = 11006
|
|
EXTERNAL_DATA_PLANE_RUNTIME_GID = 11006
|
|
|
|
MAX_ARTIFACT_BYTES = 512 * 1024 * 1024
|
|
MAX_MEMBER_COUNT = 20000
|
|
MAX_PAYLOAD_BYTES = 1024 * 1024 * 1024
|
|
MAX_FILE_BYTES = 256 * 1024 * 1024
|
|
|
|
PATCH_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
|
|
MANIFEST_KEYS = {"id", "component", "type"}
|
|
MAP_GATEWAY_SECRET_RE = re.compile(r"^[A-Za-z0-9_-]{48,256}$")
|
|
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_RE = re.compile(r"^[A-Za-z0-9_-]{48,256}$")
|
|
ENGINE_NODE_INTELLIGENCE_SECRET_RE = re.compile(r"^[A-Za-z0-9_-]{48,256}$")
|
|
N8N_PRIVATE_EXTENSION_RELEASE_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+-[a-f0-9]{16}$")
|
|
N8N_PRIVATE_EXTENSION_NODES = (
|
|
"dist/nodes/NdcDataProductPublish/NdcDataProductPublish.node.js",
|
|
"dist/nodes/NdcDataProductRead/NdcDataProductRead.node.js",
|
|
"dist/nodes/NdcFoundryBinding/NdcFoundryBinding.node.js",
|
|
)
|
|
N8N_PRIVATE_EXTENSION_CREDENTIALS = (
|
|
"dist/credentials/NdcDataProductWriterApi.credentials.js",
|
|
"dist/credentials/NdcDataProductReaderApi.credentials.js",
|
|
"dist/credentials/NdcFoundryBindingApi.credentials.js",
|
|
"dist/credentials/NdcProviderRotatingAccessApi.credentials.js",
|
|
)
|
|
ENGINE_N8N_NODE_TYPES = (
|
|
"n8n-nodes-ndc.ndcDataProductPublish",
|
|
"n8n-nodes-ndc.ndcDataProductRead",
|
|
"n8n-nodes-ndc.ndcFoundryBinding",
|
|
)
|
|
ENGINE_N8N_CREDENTIAL_TYPES = (
|
|
"ndcDataProductWriterApi",
|
|
"ndcDataProductReaderApi",
|
|
"ndcFoundryBindingApi",
|
|
"ndcProviderRotatingAccessApi",
|
|
)
|
|
ENGINE_N8N_CREDENTIAL_TYPES_BY_RELEASE = {
|
|
"0.1.2-05e4b38b14b4a019": ENGINE_N8N_CREDENTIAL_TYPES[:3],
|
|
"0.1.3-3354149b5245e39a": ENGINE_N8N_CREDENTIAL_TYPES[:3],
|
|
"0.1.4-59dc9f7882721d6a": ENGINE_N8N_CREDENTIAL_TYPES,
|
|
"0.1.5-3c8ae53f010d7c88": ENGINE_N8N_CREDENTIAL_TYPES,
|
|
"0.1.6-25cc2d7a52a0d0ee": ENGINE_N8N_CREDENTIAL_TYPES,
|
|
}
|
|
ENGINE_CREDENTIAL_SINK_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/credentialPolicies/ndcPrivateNode.js",
|
|
"nodedc-source/server/credentialSink",
|
|
"nodedc-source/server/index.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/routes/engineCredentialSink.js",
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/server/routes/ndcAgentMcp.js",
|
|
ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL,
|
|
)
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/dataProductPublishGrant",
|
|
"nodedc-source/server/engineAgents/store.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/routes/n8n.js",
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL,
|
|
)
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_PREFIX = (
|
|
"nodedc-source/server/dataProductPublishGrant/"
|
|
)
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_FILES = (
|
|
"acceptance.js",
|
|
"providerCatalog.js",
|
|
"service.js",
|
|
"signedDataPlaneClient.js",
|
|
"store.js",
|
|
)
|
|
ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_PREDECESSOR_SHA256 = (
|
|
"992159fc457ec76ce1f45aad337604c8a72b29252d44fbccda515f0fd6ea6428"
|
|
)
|
|
ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256 = (
|
|
"9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a"
|
|
)
|
|
ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CHANGED_PATHS = (
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js",
|
|
"nodedc-source/server/dataProductPublishGrant/service.js",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js",
|
|
)
|
|
ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js",
|
|
"nodedc-source/server/dataProductPublishGrant/service.js",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js",
|
|
)
|
|
ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "992159fc457ec76ce1f45aad337604c8a72b29252d44fbccda515f0fd6ea6428",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "901b8fad80018ce177b34ced804b39cb140a47e831414057f484296b373c651d",
|
|
# Publish Grant 20260718-004 was applied after MCP Control Plane
|
|
# 20260718-003 and is therefore the authoritative installed predecessor.
|
|
"nodedc-source/server/dataProductPublishGrant/service.js": "6a417b25b080c05b40c771df4e2dca163491af2c9083ff73dc7e54ad3b360241",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js": "a92303b2732e21f68c1ac732fa26e4983cbadf3515741cee983b916a283d754c",
|
|
}
|
|
ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256 = {
|
|
"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",
|
|
}
|
|
ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js",
|
|
)
|
|
ENGINE_PROVIDER_ROTATING_SLOT_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "689c6fbf695e582d983159973a21142787d1b19bb1d343da4c62c03092ac291f",
|
|
}
|
|
ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256 = {
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "17f3e368f3264cbbd708965c9e1fd735aa974f15a1383bf88cd6d14a43dbf32d",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "f6cf5f4de9e57f87fb904e2136a9f34d494e1ffc289aec3ed9ed788b5583a062",
|
|
}
|
|
ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js": "761a874b102a938bc6018159ddacdaac71ad6ae08e9f0f8d7f3b58a0165a5131",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "8d65991fbc23578fd3e4830844d52827212c78aa94a9cb3d13bac7594b888497",
|
|
}
|
|
ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256 = {
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js": "2dfa6b4f37d9bfa8b92a8109d4060d02dd2634ceb8f7924504b83ccf3fdd1523",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "84b0e15a10cedf334ad04d6f31c908da2dc155503c9974f0eeb75b01e20fb884",
|
|
}
|
|
ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/assets/provider-packages/v1/depttrans-zone-authority-v1.json",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js",
|
|
"nodedc-source/server/dataProductPublishGrant/service.js",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js",
|
|
)
|
|
ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "63e0741646197f0b1b3c64a4095e1bc8fb3a95ee6caf20b0293f89d869c9e620",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "1a14299167ebe17efd677fa3c59b84c80e12d6846bac6f40f9bcae0729ab22c6",
|
|
"nodedc-source/server/dataProductPublishGrant/service.js": "83f045f4e0f332644310172ed51bb652d808bf04155b11c02e46bdffc95f7220",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js": "98662da6acd0489a9cae4b726eb61ce89d9b2c788b2ea95433e9c4f02930c095",
|
|
}
|
|
ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256 = {
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "1aac712a05e55137d69eedaf363969460ffe866288e8e18e35711967ab232f39",
|
|
"nodedc-source/server/assets/provider-packages/v1/depttrans-zone-authority-v1.json": "1482032178b4816e599df570face9b1dfa8ae1fa2943ac8f941c561e8cb9aa9d",
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "e4c216b0affd89ddbd48abcec86febd48eb12a4c56507851daeb47f2ccd60c9a",
|
|
"nodedc-source/server/dataProductPublishGrant/service.js": "408836564e9a422eb5e648cc24d764f4bfdf7f83d93306742e8615fa8186a642",
|
|
"nodedc-source/server/dataProductPublishGrant/store.js": "ace5971d0772e9a9499f0849e6b754e3677cc2ca3080e573a12e26acf5a6c0c0",
|
|
}
|
|
ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_NEW_PATHS = (
|
|
"nodedc-source/server/assets/provider-packages/v1/depttrans-zone-authority-v1.json",
|
|
)
|
|
ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/routes/n8n.js",
|
|
)
|
|
ENGINE_PROVIDER_TARGET_HOST_POLICY_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js": "f293a7794405badbabd2bf7ef088f96fe1167d9e249f05cbfc8af280a0d3e8f8",
|
|
}
|
|
ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js": "9bc3638e271102abec91bb80413329befb89d60c0e4dc0548f0dd11e93220d0a",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/executionProfileDecoderV1.json"
|
|
)
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/routes/n8n.js",
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js": "752cb1524160adc1c95163c34e139b615c063d2ce8980f2a63879bddbd0f1e08",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_TARGET_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js": "1c2427c1d5830c40b1e8ae05f3d683fc39d07d7e0fe2431b0f6efbbb1d3fcb88",
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL: "93e431902e9bcd3b828a82ed6b42b48d939051f21bf8824dafcf2addac8a711c",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_NEW_PATHS = (
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/telemetryReadingCatalogV1.json"
|
|
)
|
|
ENGINE_MCP_TELEMETRY_CATALOG_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL,
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_TELEMETRY_CATALOG_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js": "1c2427c1d5830c40b1e8ae05f3d683fc39d07d7e0fe2431b0f6efbbb1d3fcb88",
|
|
"nodedc-source/server/routes/engineAgentGateway.js": "4f600a2781be9118bec891fa2a6f20d7f55e0892ef2f06af24059193cebd28f4",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "63e7619c6971d02102583bb5d80d33ece293b952f113200fa0b76f0e88c2dd32",
|
|
}
|
|
ENGINE_MCP_TELEMETRY_CATALOG_FOUNDATION_SHA256 = {
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL: "93e431902e9bcd3b828a82ed6b42b48d939051f21bf8824dafcf2addac8a711c",
|
|
}
|
|
ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js": "903245ae363e9b9ac161498f17988a38876a0e0ac8d80f3fa1b0112ceb7fe906",
|
|
"nodedc-source/server/routes/engineAgentGateway.js": "69bfc91e913a3fad04e13aca86efb9d62f73c0c7d1f8f7200907494b29fa9e8d",
|
|
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL: "b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "25ed3efd858aaf82c242dba501f0acc0c6c3dc91e8845707a4d3325750eab59f",
|
|
}
|
|
ENGINE_MCP_TELEMETRY_CATALOG_NEW_PATHS = (
|
|
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/executionPlanMaterializationV1.json"
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_DESCRIPTOR_REL,
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js": "903245ae363e9b9ac161498f17988a38876a0e0ac8d80f3fa1b0112ceb7fe906",
|
|
"nodedc-source/server/routes/engineAgentGateway.js": "69bfc91e913a3fad04e13aca86efb9d62f73c0c7d1f8f7200907494b29fa9e8d",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "25ed3efd858aaf82c242dba501f0acc0c6c3dc91e8845707a4d3325750eab59f",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_FOUNDATION_SHA256 = {
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL: "93e431902e9bcd3b828a82ed6b42b48d939051f21bf8824dafcf2addac8a711c",
|
|
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL: "b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_TARGET_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js": "391fc81228fdacd6efc6c0868991a3485f71869708e49ac15819db6ccce1bfce",
|
|
"nodedc-source/server/routes/engineAgentGateway.js": "9020cf49a3558c0497fed4ecfd81367cbc11b5b249881804c29fe437900c71f8",
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js": "c35b4c7ad9319aabbf1366a11ff52a6e99d961893bafdfcb4e84fc5f24fc04be",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js": "beb3f664073f9a372432643936a04d0cb0028cd695f759c68bd053e9cd892fe4",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js": "ee3bcfd06b3a5fa46800df974a2dedfdd55eeaf662329f837486c011a9bd713e",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json": "a153e040e0a592bad4375b98d9a1d83d148923aa0f0fd47d225b92eda281c4e9",
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_DESCRIPTOR_REL: "52c0152cff49c251be5581a9209d2e63ba710c16e815bdd6ece24f7c9dd7e480",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "03b2ba120c3929e9cf99940ddc927082de376ceff762895a84103144202aef42",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_NEW_PATHS = (
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/executionPlanTelemetryRuntimeV2.json"
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"beb3f664073f9a372432643936a04d0cb0028cd695f759c68bd053e9cd892fe4",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"a153e040e0a592bad4375b98d9a1d83d148923aa0f0fd47d225b92eda281c4e9",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js":
|
|
"391fc81228fdacd6efc6c0868991a3485f71869708e49ac15819db6ccce1bfce",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"9020cf49a3558c0497fed4ecfd81367cbc11b5b249881804c29fe437900c71f8",
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js":
|
|
"c35b4c7ad9319aabbf1366a11ff52a6e99d961893bafdfcb4e84fc5f24fc04be",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"ee3bcfd06b3a5fa46800df974a2dedfdd55eeaf662329f837486c011a9bd713e",
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_DESCRIPTOR_REL:
|
|
"52c0152cff49c251be5581a9209d2e63ba710c16e815bdd6ece24f7c9dd7e480",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"03b2ba120c3929e9cf99940ddc927082de376ceff762895a84103144202aef42",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_TARGET_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"6b783ad15c26dc7de0645082c8a426002d943138c70bf31a240247b16a33b6a0",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"5bdfc92284a7c836ff326e3a89b559110e376b32efd34b5f23f2bec272745781",
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_DESCRIPTOR_REL:
|
|
"68b275efb6303284336d8b637c966c24d891a4bd0b3fec4fb83247359929ef79",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_NEW_PATHS = (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/executionPlanModuleOwnershipV3.json"
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_DESCRIPTOR_REL,
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"ee3bcfd06b3a5fa46800df974a2dedfdd55eeaf662329f837486c011a9bd713e",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"9020cf49a3558c0497fed4ecfd81367cbc11b5b249881804c29fe437900c71f8",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"5bdfc92284a7c836ff326e3a89b559110e376b32efd34b5f23f2bec272745781",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"03b2ba120c3929e9cf99940ddc927082de376ceff762895a84103144202aef42",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js":
|
|
"391fc81228fdacd6efc6c0868991a3485f71869708e49ac15819db6ccce1bfce",
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js":
|
|
"c35b4c7ad9319aabbf1366a11ff52a6e99d961893bafdfcb4e84fc5f24fc04be",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"6b783ad15c26dc7de0645082c8a426002d943138c70bf31a240247b16a33b6a0",
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_DESCRIPTOR_REL:
|
|
"52c0152cff49c251be5581a9209d2e63ba710c16e815bdd6ece24f7c9dd7e480",
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_DESCRIPTOR_REL:
|
|
"68b275efb6303284336d8b637c966c24d891a4bd0b3fec4fb83247359929ef79",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_TARGET_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"17c5f502b3ceacc45278eef7418d08e1e55a8b3e46e00942e559d25566fdba41",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"7e34b93aa30b06cd52853b48013baef2970cc27e4fddc333a1011dc73ea8c08a",
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_DESCRIPTOR_REL:
|
|
"d187d539a219fec453e06c55c3f5486ef66059371b4ae9b145266307b2af9889",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"4cdeb85ebb2e43f088f095f75b7fc31aabd8ab81c8ac96ded4aafd7dbd8e30bd",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_NEW_PATHS = (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/normalizedIdentitySearchV1.json"
|
|
)
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_DESCRIPTOR_REL,
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js":
|
|
"391fc81228fdacd6efc6c0868991a3485f71869708e49ac15819db6ccce1bfce",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"17c5f502b3ceacc45278eef7418d08e1e55a8b3e46e00942e559d25566fdba41",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"4854a62fb44cb3dd715f2cb8728740575453a666f5c31abe9d41bc2562be5e95",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"2e70b607b4a347592ce2f4732f6da0781ac94ec4829ac336021cb501fdafe9aa",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
|
"2fce1c623a8acf9c01a43463e6d38eda71ad28379a2d91b2ca40f31f8dccf3c6",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"4cdeb85ebb2e43f088f095f75b7fc31aabd8ab81c8ac96ded4aafd7dbd8e30bd",
|
|
}
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js":
|
|
"c35b4c7ad9319aabbf1366a11ff52a6e99d961893bafdfcb4e84fc5f24fc04be",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c",
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL:
|
|
"93e431902e9bcd3b828a82ed6b42b48d939051f21bf8824dafcf2addac8a711c",
|
|
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL:
|
|
"b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882",
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_DESCRIPTOR_REL:
|
|
"52c0152cff49c251be5581a9209d2e63ba710c16e815bdd6ece24f7c9dd7e480",
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_DESCRIPTOR_REL:
|
|
"68b275efb6303284336d8b637c966c24d891a4bd0b3fec4fb83247359929ef79",
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_DESCRIPTOR_REL:
|
|
"d187d539a219fec453e06c55c3f5486ef66059371b4ae9b145266307b2af9889",
|
|
}
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_TARGET_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js":
|
|
"a2bc69c72f68e57ed27120d0c88f4ffe6310bbf0c4360c8b0dba0953ccaaf522",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"01958d541c778002d33e3aead0cfe02df2084eb7222ce941cc7149d73a10f135",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"2b8e5ee3d73d16f3cd6e34d1f7394946a6270a17b0e9e6511f12921ba2561fd3",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
|
"fe5256fd2ba295819daecc8d2acae34687e807978a6730dfa20703cb3bab0c1b",
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_DESCRIPTOR_REL:
|
|
"41738185fe103642912b0aa1c29c51860970c38f9f916cc3725e79e258b9ea7e",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"3e7aeb1d28eb291461f79cd656ece6488bc6d372124088e92f53bf89c3373f61",
|
|
}
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_NEW_PATHS = (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/l1CredentialReuseV1.json"
|
|
)
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/routes/n8n.js",
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js":
|
|
"a2bc69c72f68e57ed27120d0c88f4ffe6310bbf0c4360c8b0dba0953ccaaf522",
|
|
}
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872",
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_DESCRIPTOR_REL:
|
|
"41738185fe103642912b0aa1c29c51860970c38f9f916cc3725e79e258b9ea7e",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"3e7aeb1d28eb291461f79cd656ece6488bc6d372124088e92f53bf89c3373f61",
|
|
}
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_TARGET_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js":
|
|
"6620e4bdd573e9f6b636a4b059eafef76d59da2fdb2183038fa5ec95357d8478",
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_DESCRIPTOR_REL:
|
|
"2ada49ef8bb2f146a9ea8d3c9ab5ee55f6a4e1b17e0f4b62a16f27368bd0eb05",
|
|
}
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_NEW_PATHS = (
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/l1CredentialProvenanceV2.json"
|
|
)
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/routes/n8n.js",
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js":
|
|
"6620e4bdd573e9f6b636a4b059eafef76d59da2fdb2183038fa5ec95357d8478",
|
|
}
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872",
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_DESCRIPTOR_REL:
|
|
"41738185fe103642912b0aa1c29c51860970c38f9f916cc3725e79e258b9ea7e",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"3e7aeb1d28eb291461f79cd656ece6488bc6d372124088e92f53bf89c3373f61",
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_DESCRIPTOR_REL:
|
|
"2ada49ef8bb2f146a9ea8d3c9ab5ee55f6a4e1b17e0f4b62a16f27368bd0eb05",
|
|
}
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_TARGET_SHA256 = {
|
|
"nodedc-source/server/routes/n8n.js":
|
|
"af07edcf784c420855134ab9178f020d259a1cac703cd643418ab7b4eb94dbd3",
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_DESCRIPTOR_REL:
|
|
"5887da6e5cb611450e03a110be9786acbe47ae1b00217b8bf09e0967f71f6a3d",
|
|
}
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_NEW_PATHS = (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/executionPlanSandboxRuntimeV4.json"
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"01958d541c778002d33e3aead0cfe02df2084eb7222ce941cc7149d73a10f135",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"2b8e5ee3d73d16f3cd6e34d1f7394946a6270a17b0e9e6511f12921ba2561fd3",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
|
"fe5256fd2ba295819daecc8d2acae34687e807978a6730dfa20703cb3bab0c1b",
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_DESCRIPTOR_REL:
|
|
"41738185fe103642912b0aa1c29c51860970c38f9f916cc3725e79e258b9ea7e",
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_DESCRIPTOR_REL:
|
|
"5887da6e5cb611450e03a110be9786acbe47ae1b00217b8bf09e0967f71f6a3d",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_TARGET_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"64f5196a83018505c6dac77a0f8674c27941257a874eed9b024c1c94f169be2b",
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_DESCRIPTOR_REL:
|
|
"a4692643afb86dfeeee6ca24aefcc181913e715eff38fa158667d2f10b901847",
|
|
}
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_NEW_PATHS = (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/geliosItemsEnvelopeV12.json"
|
|
)
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"2b8e5ee3d73d16f3cd6e34d1f7394946a6270a17b0e9e6511f12921ba2561fd3",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
|
"fe5256fd2ba295819daecc8d2acae34687e807978a6730dfa20703cb3bab0c1b",
|
|
}
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"64f5196a83018505c6dac77a0f8674c27941257a874eed9b024c1c94f169be2b",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872",
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_DESCRIPTOR_REL:
|
|
"a4692643afb86dfeeee6ca24aefcc181913e715eff38fa158667d2f10b901847",
|
|
}
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_TARGET_SHA256 = {
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"eb0c32fa0e8017b23e2d225fcb2d5805aa6dd1bf674e8aa248afc6a4079a7403",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
|
"42104d1267ca7840446c0c02edd3f9ecb29f8da8eb3e8f384cbd6dca0f676c4c",
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_DESCRIPTOR_REL:
|
|
"16e39f54ebae776de9acfdf0078c79291310eeca5a1f720eb8b82496f4d419a3",
|
|
}
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_NEW_PATHS = (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/registeredExecutionProfilesV2.json"
|
|
)
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js",
|
|
"nodedc-source/server/l2ExecutionPlan/registeredProfiles.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"eb0c32fa0e8017b23e2d225fcb2d5805aa6dd1bf674e8aa248afc6a4079a7403",
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js":
|
|
"c35b4c7ad9319aabbf1366a11ff52a6e99d961893bafdfcb4e84fc5f24fc04be",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"3e7aeb1d28eb291461f79cd656ece6488bc6d372124088e92f53bf89c3373f61",
|
|
}
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"64f5196a83018505c6dac77a0f8674c27941257a874eed9b024c1c94f169be2b",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c",
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_DESCRIPTOR_REL:
|
|
"16e39f54ebae776de9acfdf0078c79291310eeca5a1f720eb8b82496f4d419a3",
|
|
}
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256 = {
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"940510499a9260dbff718b7b1f96f72f8e9ac593b460805af3007731578a7668",
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js":
|
|
"fec56b154ae483ad21bbaaeb4c55707bffce223c27874ac135ce58a9203259a0",
|
|
"nodedc-source/server/l2ExecutionPlan/registeredProfiles.js":
|
|
"3c521c1652c61c9d757197c76e053fd3ec602e13ef7f6a4856835ecc1a8a61d1",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"8f04edc11251de86b825351c92338be9537207887cf802474b6b6d8c3cce4077",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"3d72709e40b79c01a62a6af4bde911fca4f609d5ac487d284cf97fd8ebd73686",
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL:
|
|
"1db523f035a47c6b00b41b26efe1390ad2d24acece9d30dd039eff56626e934d",
|
|
}
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS = (
|
|
"nodedc-source/server/l2ExecutionPlan/registeredProfiles.js",
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FAILED_PATHS = (
|
|
"nodedc-source/server/deployTransitions/registeredExecutionProfilesV1.json",
|
|
)
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL = (
|
|
"nodedc-source/server/deployTransitions/"
|
|
"geliosUnitsItemsEnvelopeV12Patch1.json"
|
|
)
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES = (
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256 = {
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"940510499a9260dbff718b7b1f96f72f8e9ac593b460805af3007731578a7668",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
|
"42104d1267ca7840446c0c02edd3f9ecb29f8da8eb3e8f384cbd6dca0f676c4c",
|
|
}
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256 = {
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js":
|
|
"fec56b154ae483ad21bbaaeb4c55707bffce223c27874ac135ce58a9203259a0",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"64f5196a83018505c6dac77a0f8674c27941257a874eed9b024c1c94f169be2b",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c",
|
|
"nodedc-source/server/l2ExecutionPlan/registeredProfiles.js":
|
|
"3c521c1652c61c9d757197c76e053fd3ec602e13ef7f6a4856835ecc1a8a61d1",
|
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
|
"8f04edc11251de86b825351c92338be9537207887cf802474b6b6d8c3cce4077",
|
|
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
|
|
"3d72709e40b79c01a62a6af4bde911fca4f609d5ac487d284cf97fd8ebd73686",
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL:
|
|
"1db523f035a47c6b00b41b26efe1390ad2d24acece9d30dd039eff56626e934d",
|
|
}
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256 = {
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
|
"d0cc95fa5e55bda315947fdb13bd8a0d8931b9484677e4c4cc0dcb0e9614e215",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
|
"1f1b866b6f837bffc6d529dfffd8bfa9c87495bfcd9ae3515a79765143d94b1d",
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL:
|
|
"7a1594573e98342d3a3edcc0dc4f663f06b30ca24cc20adb5f36d50591066d96",
|
|
}
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS = (
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL,
|
|
)
|
|
ENGINE_AGENT_FULL_GRANT_MIGRATION_ARTIFACT_ENTRIES = (
|
|
ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL,
|
|
)
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256 = {
|
|
"docker-compose.yml": ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_COMPOSE_SHA256,
|
|
"nodedc-source/server/credentialPolicies/ndcPrivateNode.js": "723874a02dc7b8a68b22ff2304431cfe64f28933cedf5f1e6cda79b2e1cf704a",
|
|
"nodedc-source/server/credentialSink/core.js": "9f0facc41fd398fcd955cffdd486abb126cdcd756c87ffde667fcbe00e2c41d3",
|
|
"nodedc-source/server/credentialSink/requestAuth.js": "f8c9237c3e6f4219dee0d4f956d6e97f76f5bd7ba8a6fd0b4fb21aceffa38138",
|
|
"nodedc-source/server/credentialSink/store.js": "c78dc285a973b6acd8a2330f0310935ad08720b2905454492f292d235abf12c0",
|
|
"nodedc-source/server/credentialSink/vendor/engine-credential-sink.mjs": "b4800eead9bf94793ff1280d06e34aad6b37ef055a9d7f8d83d7fb5f9bd66d8b",
|
|
"nodedc-source/server/index.js": "b2b790b02839570d967a2ca68b00e2724485a99389ac9b3a589a1b22302a36b8",
|
|
"nodedc-source/server/routes/engineAgentGateway.js": "604a75ad3b9b463cea5e578760c8a23592e2e00b8acfc0099730a347e27bf4e8",
|
|
"nodedc-source/server/routes/engineCredentialSink.js": "9cbb69dbc8cbe6181cd5b0170fe9c4d717b0a173866ca98cba3e3766c0bab94e",
|
|
"nodedc-source/server/routes/n8n.js": "783d822e2457d82e890f43bc00c7e33822077dc2841f0511a89ecb210fd36d48",
|
|
"nodedc-source/server/routes/ndcAgentMcp.js": "534e2c85e1faecc72a00da7ad32d0584ee09b6bd89eeec2221c3b23d80e1e962",
|
|
ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL: "944fa64b08255eb8207b93fd327aebb98ecd9400d37d25fcfa8e3a040ee44afe",
|
|
}
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256 = {
|
|
**ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256,
|
|
# This exact successor was installed by the later restart-safe-auth
|
|
# transition; initial Publish-grant installation still uses the original
|
|
# predecessor map above.
|
|
"nodedc-source/server/index.js": "0ac408e0e9a7bc5c8e13a00afc957b982b065e2bc414919839e0b0a11aa05ba4",
|
|
}
|
|
ENGINE_CREDENTIAL_SINK_CONTRACT_SHA256 = "b4800eead9bf94793ff1280d06e34aad6b37ef055a9d7f8d83d7fb5f9bd66d8b"
|
|
ENGINE_BASE_COMPOSE_SERVICES = (
|
|
"postgresql",
|
|
"authentik-server",
|
|
"authentik-worker",
|
|
"n8n-postgres",
|
|
"ops-postgres",
|
|
"n8n",
|
|
"nodedc-backend",
|
|
"nodedc-frontend-build",
|
|
"app",
|
|
)
|
|
|
|
GITEA_ROOT = Path("/volume1/docker/nodedc-gitea")
|
|
GITEA_COMPOSE_PROJECT = "nodedc-gitea"
|
|
GITEA_SERVICE = "gitea"
|
|
GITEA_COMPOSE_REL = "docker-compose.gitea.yml"
|
|
GITEA_FRESH_INSTALL_DESCRIPTOR_REL = (
|
|
"deployment/gitea-fresh-install-v1.json"
|
|
)
|
|
GITEA_FRESH_INSTALL_ENTRIES = (
|
|
GITEA_COMPOSE_REL,
|
|
GITEA_FRESH_INSTALL_DESCRIPTOR_REL,
|
|
)
|
|
GITEA_IMAGE = (
|
|
"docker.gitea.com/gitea:1.27.1-rootless@"
|
|
"sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148"
|
|
)
|
|
GITEA_IMAGE_ID = (
|
|
"sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148"
|
|
)
|
|
GITEA_REPO_DIGEST = (
|
|
"docker.gitea.com/gitea@"
|
|
"sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148"
|
|
)
|
|
GITEA_COMPOSE_SHA256 = (
|
|
"25868a40996c405543b4627d06499b68f43556e607d839e969a400b0bc0ddadb"
|
|
)
|
|
GITEA_MINIMUM_COMPOSE_VERSION = (2, 20, 1)
|
|
GITEA_REQUIRED_DOCKER_VERSION = "24.0.2"
|
|
GITEA_RUNTIME_UID = 1000
|
|
GITEA_RUNTIME_GID = 1000
|
|
GITEA_NGINX_WORKER_UID = 1023
|
|
GITEA_NGINX_GID = 1023
|
|
GITEA_DATA_DIR = GITEA_ROOT / "data"
|
|
GITEA_CONFIG_DIR = GITEA_ROOT / "config"
|
|
GITEA_SOCKET_DIR = GITEA_ROOT / "socket"
|
|
GITEA_SOCKET_FILE = GITEA_SOCKET_DIR / "gitea.sock"
|
|
GITEA_SECRET_DIR = GITEA_ROOT / "secrets"
|
|
GITEA_SECRET_KEY_FILE = GITEA_SECRET_DIR / "secret-key"
|
|
GITEA_INTERNAL_TOKEN_FILE = GITEA_SECRET_DIR / "internal-token"
|
|
GITEA_HTTP_PORT = 3000
|
|
GITEA_DISABLED_SSH_HOST_PORT = 4022
|
|
GITEA_FORBIDDEN_LEGACY_NETWORK = "nodedc-gitea_internal"
|
|
GITEA_LEGACY_CONTAINER = "gitea"
|
|
GITEA_LEGACY_REVERSE_PROXY_IP = "172.22.0.222"
|
|
GITEA_REVERSE_PROXY_CONFIG = Path("/usr/syno/etc/www/ReverseProxy.json")
|
|
GITEA_REVERSE_PROXY_UUID = "5bc46027-0307-4261-af7e-4f94a3c508c9"
|
|
GITEA_REVERSE_PROXY_DESCRIPTION = "Gittea"
|
|
GITEA_REVERSE_PROXY_GENERATED_CONFIG = Path(
|
|
"/usr/local/etc/nginx/sites-available/"
|
|
"82fc9da8-f8a2-48d1-8ea5-6ba8dc8299d8.w3conf"
|
|
)
|
|
GITEA_NGINX = Path("/usr/bin/nginx")
|
|
GITEA_NGINX_VERSION = "nginx version: nginx/1.23.1"
|
|
GITEA_NGINX_MAIN_CONFIG = Path("/etc/nginx/nginx.conf.run")
|
|
GITEA_NGINX_BRIDGE_CONFIG = Path(
|
|
"/usr/local/etc/nginx/conf.d/http.nodedc-gitea-uds.conf"
|
|
)
|
|
GITEA_NGINX_BRIDGE_CONTENT = (
|
|
"map $uri $nodedc_gitea_login_limit_key {\n"
|
|
" default \"\";\n"
|
|
" /user/login $http_x_real_ip;\n"
|
|
"}\n"
|
|
"\n"
|
|
"limit_req_zone $nodedc_gitea_login_limit_key "
|
|
"zone=nodedc_gitea_login:10m rate=10r/m;\n"
|
|
"limit_conn_zone $http_x_real_ip zone=nodedc_gitea_conn:10m;\n"
|
|
"\n"
|
|
"server {\n"
|
|
" listen 127.0.0.1:3000;\n"
|
|
" server_name nodedc-gitea-uds.internal;\n"
|
|
"\n"
|
|
" location / {\n"
|
|
" limit_req zone=nodedc_gitea_login burst=10 nodelay;\n"
|
|
" limit_conn nodedc_gitea_conn 40;\n"
|
|
" proxy_http_version 1.1;\n"
|
|
" proxy_set_header Host $http_host;\n"
|
|
" proxy_set_header X-Real-IP $http_x_real_ip;\n"
|
|
" proxy_set_header X-Forwarded-For $http_x_forwarded_for;\n"
|
|
" proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;\n"
|
|
" proxy_pass http://unix:/volume1/docker/nodedc-gitea/socket/gitea.sock:;\n"
|
|
" }\n"
|
|
"}\n"
|
|
)
|
|
GITEA_NGINX_BRIDGE_SHA256 = (
|
|
"164f37a12a4f91e656cf20bd5b109978d16d723bdfde236653722aaf820780c9"
|
|
)
|
|
GITEA_IPTABLES = Path("/usr/bin/iptables")
|
|
GITEA_SECRET_RE = re.compile(r"^[A-Za-z0-9_-]{64,256}$")
|
|
GITEA_EXPECTED_ENVIRONMENT = {
|
|
"GITEA_WORK_DIR": "/var/lib/gitea",
|
|
"GITEA__database__DB_TYPE": "sqlite3",
|
|
"GITEA__database__PATH": "/var/lib/gitea/data/gitea.db",
|
|
"GITEA__server__DOMAIN": "git.dcserve.ru",
|
|
"GITEA__server__ROOT_URL": "https://git.dcserve.ru/",
|
|
"GITEA__server__PROTOCOL": "http+unix",
|
|
"GITEA__server__HTTP_ADDR": "/run/gitea/gitea.sock",
|
|
"GITEA__server__UNIX_SOCKET_PERMISSION": "0666",
|
|
"GITEA__server__LOCAL_ROOT_URL": "http://unix/",
|
|
"GITEA__server__DISABLE_SSH": "true",
|
|
"GITEA__server__START_SSH_SERVER": "false",
|
|
"GITEA__server__SSH_CREATE_AUTHORIZED_KEYS_FILE": "false",
|
|
"GITEA__server__LFS_START_SERVER": "false",
|
|
"GITEA__server__LFS_ALLOW_PURE_SSH": "false",
|
|
"GITEA__server__OFFLINE_MODE": "true",
|
|
"GITEA__server__LANDING_PAGE": "login",
|
|
"GITEA__security__INSTALL_LOCK": "true",
|
|
"GITEA__security__SECRET_KEY_URI": "file:/run/secrets/gitea_secret_key",
|
|
"GITEA__security__INTERNAL_TOKEN_URI": (
|
|
"file:/run/secrets/gitea_internal_token"
|
|
),
|
|
"GITEA__security__REVERSE_PROXY_LIMIT": "1",
|
|
"GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES": "127.0.0.0/8,::1/128",
|
|
"GITEA__security__DISABLE_GIT_HOOKS": "true",
|
|
"GITEA__security__DISABLE_WEBHOOKS": "true",
|
|
"GITEA__security__IMPORT_LOCAL_PATHS": "false",
|
|
"GITEA__security__ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET": "true",
|
|
"GITEA__security__PASSWORD_HASH_ALGO": "argon2",
|
|
"GITEA__security__MIN_PASSWORD_LENGTH": "16",
|
|
"GITEA__security__PASSWORD_COMPLEXITY": "lower,upper,digit,spec",
|
|
"GITEA__security__TWO_FACTOR_AUTH": "enforced",
|
|
"GITEA__security__DISABLE_QUERY_AUTH_TOKEN": "true",
|
|
"GITEA__security__ALLOWED_HOST_LIST": "loopback",
|
|
"GITEA__service__DISABLE_REGISTRATION": "true",
|
|
"GITEA__service__REQUIRE_SIGNIN_VIEW": "true",
|
|
"GITEA__service__SHOW_REGISTRATION_BUTTON": "false",
|
|
"GITEA__service__DEFAULT_KEEP_EMAIL_PRIVATE": "true",
|
|
"GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION": "false",
|
|
"GITEA__service__DEFAULT_USER_IS_RESTRICTED": "true",
|
|
"GITEA__service__DEFAULT_USER_VISIBILITY": "private",
|
|
"GITEA__service__ALLOWED_USER_VISIBILITY_MODES": "private",
|
|
"GITEA__service__DEFAULT_ORG_VISIBILITY": "private",
|
|
"GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION": "false",
|
|
"GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION_API": "false",
|
|
"GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION": "false",
|
|
"GITEA__service__ENABLE_NOTIFY_MAIL": "false",
|
|
"GITEA__service__ENABLE_BASIC_AUTHENTICATION": "false",
|
|
"GITEA__admin__DISABLE_REGULAR_ORG_CREATION": "true",
|
|
"GITEA__admin__USER_DISABLED_FEATURES": (
|
|
"deletion,manage_ssh_keys,manage_gpg_keys,change_username"
|
|
),
|
|
"GITEA__repository__FORCE_PRIVATE": "true",
|
|
"GITEA__repository__DEFAULT_PRIVATE": "private",
|
|
"GITEA__repository__USER_MAX_CREATION_LIMIT": "0",
|
|
"GITEA__repository__ORG_MAX_CREATION_LIMIT": "0",
|
|
"GITEA__repository__ENABLE_PUSH_CREATE_USER": "false",
|
|
"GITEA__repository__ENABLE_PUSH_CREATE_ORG": "false",
|
|
"GITEA__repository__DISABLE_MIGRATIONS": "true",
|
|
"GITEA__repository__ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES": "false",
|
|
"GITEA__repository__ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES": "false",
|
|
"GITEA__repository__DISABLE_HTTP_GIT": "false",
|
|
"GITEA__repository.upload__ENABLED": "false",
|
|
"GITEA__attachment__ENABLED": "false",
|
|
"GITEA__actions__ENABLED": "false",
|
|
"GITEA__packages__ENABLED": "false",
|
|
"GITEA__oauth2__ENABLED": "false",
|
|
"GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION": "false",
|
|
"GITEA__openid__ENABLE_OPENID_SIGNIN": "false",
|
|
"GITEA__openid__ENABLE_OPENID_SIGNUP": "false",
|
|
"GITEA__federation__ENABLED": "false",
|
|
"GITEA__mailer__ENABLED": "false",
|
|
"GITEA__session__COOKIE_SECURE": "true",
|
|
"GITEA__session__SAME_SITE": "strict",
|
|
"GITEA__api__ENABLE_SWAGGER": "false",
|
|
"GITEA__migrations__ALLOW_LOCALNETWORKS": "false",
|
|
"GITEA__migrations__SKIP_TLS_VERIFY": "false",
|
|
"GITEA__cors__ENABLED": "false",
|
|
"GITEA__metrics__ENABLED": "false",
|
|
"GITEA__cron.update_checker__ENABLED": "false",
|
|
"GITEA__log__MODE": "console",
|
|
"GITEA__log__LEVEL": "Info",
|
|
}
|
|
|
|
# Incident salvage is deliberately additive to the fresh-install component.
|
|
# It never imports the compromised SQLite database, config, home, credentials,
|
|
# hooks or arbitrary repository trees. Only the exact v2 decision rows and
|
|
# validated Git object/ref material may cross the read-only snapshot boundary.
|
|
GITEA_SALVAGE_DESCRIPTOR_REL = (
|
|
"deployment/gitea-incident-salvage-v3.json"
|
|
)
|
|
GITEA_SALVAGE_DECISION_PREFIX = "deployment/gitea-incident-salvage"
|
|
GITEA_SALVAGE_DISPOSITION_REL = (
|
|
f"{GITEA_SALVAGE_DECISION_PREFIX}/confirmed-disposition-v1.json"
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_DISPOSITION_REL = (
|
|
f"{GITEA_SALVAGE_DECISION_PREFIX}/confirmed-closure-disposition-v1.json"
|
|
)
|
|
GITEA_SALVAGE_DECISION_MANIFEST_REL = (
|
|
f"{GITEA_SALVAGE_DECISION_PREFIX}/confirmed-decision.json"
|
|
)
|
|
GITEA_SALVAGE_USERS_REL = (
|
|
f"{GITEA_SALVAGE_DECISION_PREFIX}/users.decisions.csv"
|
|
)
|
|
GITEA_SALVAGE_REPOSITORIES_REL = (
|
|
f"{GITEA_SALVAGE_DECISION_PREFIX}/repositories.decisions.csv"
|
|
)
|
|
GITEA_SALVAGE_ENTRIES = (
|
|
GITEA_COMPOSE_REL,
|
|
GITEA_SALVAGE_DESCRIPTOR_REL,
|
|
GITEA_SALVAGE_DISPOSITION_REL,
|
|
GITEA_SALVAGE_CLOSURE_DISPOSITION_REL,
|
|
GITEA_SALVAGE_DECISION_MANIFEST_REL,
|
|
GITEA_SALVAGE_USERS_REL,
|
|
GITEA_SALVAGE_REPOSITORIES_REL,
|
|
)
|
|
GITEA_SALVAGE_IMAGE = (
|
|
"docker.gitea.com/gitea:1.27.2-rootless@"
|
|
"sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c"
|
|
)
|
|
GITEA_SALVAGE_IMAGE_ID = (
|
|
"sha256:272085a806e6d182352cdb011c0ebab1d2efc7ec45247de84de5659c7bc5c4c6"
|
|
)
|
|
GITEA_SALVAGE_REPO_DIGEST = (
|
|
"docker.gitea.com/gitea@"
|
|
"sha256:7de5f49ada687b8c8d2938f547cdb7634839764ba51f297457bae35cee3abd2c"
|
|
)
|
|
GITEA_SALVAGE_COMPOSE_SHA256 = (
|
|
"2f031d5bfff4f42c73cabd8c94487ec3e4f1e1b0a96d3b7eec958904f735908a"
|
|
)
|
|
GITEA_SALVAGE_DESCRIPTOR_SHA256 = (
|
|
"9b98eb1a1640fd5569cf051a621837379b167eff4527313a43a0a851e7cc181a"
|
|
)
|
|
GITEA_SALVAGE_DISPOSITION_SHA256 = (
|
|
"0a066724bcf6e4933133db6cab6cc273393e3c262dd00dda0bbf9ceebd84f78c"
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_DISPOSITION_SHA256 = (
|
|
"7ed66d9848268431a703fe24b22c41afbaa7c5ff48949604d6fc448d93e0d243"
|
|
)
|
|
GITEA_SALVAGE_DECISION_MANIFEST_SHA256 = (
|
|
"dc9528462624158eb44218d37cc7054d551ca2d7ded562592982aa3f34c9fc2a"
|
|
)
|
|
GITEA_SALVAGE_USERS_SHA256 = (
|
|
"e3b82f1073a86eea9e567edff062dd1689d21ec0edcf3ed92e844da9351ee8b6"
|
|
)
|
|
GITEA_SALVAGE_REPOSITORIES_SHA256 = (
|
|
"76b4bae2ab5cec490330c19bfc5ae9429abf7636705ae028c1c64fd54a6a0493"
|
|
)
|
|
GITEA_SALVAGE_SNAPSHOT_ROOT = Path(
|
|
"/volume1/.nodedc-security-snapshots/docker-gitea-incident-20260814"
|
|
)
|
|
GITEA_SALVAGE_SNAPSHOT_UUID = "f5a3fe3a-93ea-bb4d-847f-6221a6bcbc9f"
|
|
GITEA_SALVAGE_SNAPSHOT_DATABASE = (
|
|
GITEA_SALVAGE_SNAPSHOT_ROOT / "gitea/gitea/gitea.db"
|
|
)
|
|
GITEA_SALVAGE_SNAPSHOT_DATABASE_BYTES = 182681600
|
|
GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256 = (
|
|
"8db9e74a5641662a808d8252c5d6c9de43fe9efd8687634bfbff2d4361a13052"
|
|
)
|
|
GITEA_SALVAGE_SNAPSHOT_REPOSITORIES = (
|
|
GITEA_SALVAGE_SNAPSHOT_ROOT / "gitea/git/repositories"
|
|
)
|
|
GITEA_SALVAGE_BTRFS = Path("/usr/sbin/btrfs")
|
|
GITEA_SALVAGE_LEGACY_ROOT = Path("/volume1/docker/gitea")
|
|
GITEA_SALVAGE_MAINTENANCE_CONTAINER = "nodedc-gitea-salvage-maintenance"
|
|
GITEA_SALVAGE_REPOSITORY_QUARANTINE = GITEA_ROOT / "quarantine/repositories"
|
|
GITEA_SALVAGE_BOOTSTRAP_DIR = GITEA_ROOT / "bootstrap"
|
|
GITEA_SALVAGE_AUDIT_DIR = GITEA_ROOT / "audit"
|
|
GITEA_SALVAGE_IDENTITY_MAP = GITEA_SALVAGE_AUDIT_DIR / "identity-map.json"
|
|
GITEA_SALVAGE_DCTOUCH_PASSWORD_FILE = (
|
|
GITEA_SALVAGE_BOOTSTRAP_DIR / "dctouch.password"
|
|
)
|
|
GITEA_SALVAGE_SILVER_PASSWORD_FILE = (
|
|
GITEA_SALVAGE_BOOTSTRAP_DIR / "silver.password"
|
|
)
|
|
GITEA_SALVAGE_APP_INI = GITEA_CONFIG_DIR / "app.ini"
|
|
GITEA_SALVAGE_DB = GITEA_DATA_DIR / "gitea/gitea.db"
|
|
GITEA_SALVAGE_REPOSITORY_ROOT = GITEA_DATA_DIR / "git/repositories"
|
|
GITEA_SALVAGE_USER_COUNTS = {
|
|
"KEEP_ACTIVE": 2,
|
|
"KEEP_LOCKED": 8,
|
|
"DELETE": 962,
|
|
}
|
|
GITEA_SALVAGE_REPOSITORY_COUNTS = {"KEEP": 45, "DELETE": 2013}
|
|
GITEA_SALVAGE_ACTIVE_USERS = {1: "dctouch", 16: "SILVER"}
|
|
GITEA_SALVAGE_LOCKED_USERS = {
|
|
2: "KOPYLOV",
|
|
4: "kalininUN",
|
|
6: "arturHITECA",
|
|
7: "uePATSUKEVICH",
|
|
11: "sadenov-a",
|
|
15: "KKK",
|
|
17: "ayoauo",
|
|
18: "ayoayuoo",
|
|
}
|
|
# Exact stopped-container identity, reviewed from root-owned Docker inspect.
|
|
# The mutable `latest` text is provenance only; the immutable image ID and full
|
|
# stopped/mount topology below are the execution authority.
|
|
GITEA_SALVAGE_EXPECTED_LEGACY_IMAGE = "gitea/gitea:latest"
|
|
GITEA_SALVAGE_EXPECTED_LEGACY_IMAGE_ID = (
|
|
"sha256:bf95d9a45ce4fe38b027d051cdc4a4bc531513489fa6244af4074efbb1c376d6"
|
|
)
|
|
GITEA_SALVAGE_EXPECTED_REF_MANIFEST_SHA256 = None
|
|
GITEA_SALVAGE_DISPOSITION_REFERENCE_MANIFEST_SHA256 = (
|
|
"9cddaf0e4d4cf22dd264a6ae589ccc50d29e07f85c55e9d34b14627cecb8a311"
|
|
)
|
|
GITEA_SALVAGE_EXPECTED_UNSUPPORTED_REPORT_SHA256 = (
|
|
"4b2cecf88c62fc5c4a43419885e88a01c9f9aac03133afb19dae0a7caef106ac"
|
|
)
|
|
GITEA_SALVAGE_DISPOSITION_UNSUPPORTED_SCHEMA_SHA256 = (
|
|
"b5e3b6776926c4f1627fafd882362ed0ef986bfc86fc6ac6507a43976531b6db"
|
|
)
|
|
GITEA_SALVAGE_DISPOSITION_TOPICS_SHA256 = (
|
|
"df6e3612186234bfcf3c172ef4e0fff933baaa691a510f9780ebf9e21c8d4d05"
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_PREDECESSOR_ARTIFACT_SHA256 = (
|
|
"d6870b5583a2f329eadb4e6cda65fdf4d271532df5ffbf8bfb1403968a434672"
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_PREDECESSOR_DISPOSITION_SHA256 = (
|
|
"0a066724bcf6e4933133db6cab6cc273393e3c262dd00dda0bbf9ceebd84f78c"
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_MAX_ROWS_PER_RELATION = 2_000_000
|
|
GITEA_SALVAGE_CLOSURE_MAX_TEXT_BYTES_PER_FIELD = 64 * 1024 * 1024
|
|
GITEA_SALVAGE_CLOSURE_ACTOR_RELATIONS = (
|
|
("access_cache", "access", "DROP_CACHE_RECOMPUTE"),
|
|
("collaborations", "collaboration", "RECREATE_KEPT_ACTOR_AFTER_ID_MAP"),
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_DEPENDENTS = (
|
|
(
|
|
"comments",
|
|
"comment",
|
|
"poster_id",
|
|
("content", "patch", "old_title", "new_title", "old_ref", "new_ref"),
|
|
),
|
|
("issue_assignees", "issue_assignees", "assignee_id", ()),
|
|
(
|
|
"issue_content_histories",
|
|
"issue_content_history",
|
|
"poster_id",
|
|
("content_text",),
|
|
),
|
|
("issue_labels", "issue_label", None, ()),
|
|
("issue_users", "issue_user", "uid", ()),
|
|
("issue_watches", "issue_watch", "user_id", ()),
|
|
("reviews", "review", "reviewer_id", ("content",)),
|
|
("stopwatches", "stopwatch", "user_id", ()),
|
|
("tracked_times", "tracked_time", "user_id", ()),
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_METRICS = tuple(
|
|
sorted(
|
|
{
|
|
"action_artifacts",
|
|
"action_run_indexes",
|
|
"action_run_jobs",
|
|
"action_runners",
|
|
"action_runs",
|
|
"action_schedules",
|
|
"action_secrets",
|
|
"action_tasks",
|
|
"action_variables",
|
|
"comments",
|
|
"issue_assignees",
|
|
"issue_content_histories",
|
|
"issue_dependencies",
|
|
"issue_labels",
|
|
"issue_users",
|
|
"issue_watches",
|
|
"issues_ordinary",
|
|
"labels",
|
|
"milestones",
|
|
"notifications",
|
|
"package_blobs",
|
|
"package_files",
|
|
"package_properties",
|
|
"package_versions",
|
|
"packages",
|
|
"project_boards",
|
|
"project_issue_links",
|
|
"projects",
|
|
"pull_auto_merges",
|
|
"pull_request_wrappers",
|
|
"reactions",
|
|
"releases",
|
|
"repo_units",
|
|
"review_states",
|
|
"reviews",
|
|
"stopwatches",
|
|
"tracked_times",
|
|
}
|
|
)
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_TEXT_COLUMNS = {
|
|
"comments": ("content", "new_ref", "new_title", "old_ref", "old_title", "patch"),
|
|
"issue_content_histories": ("content_text",),
|
|
"issues_ordinary": ("content", "name"),
|
|
"labels": ("color", "description", "name"),
|
|
"milestones": ("content", "name"),
|
|
"project_boards": ("color", "title"),
|
|
"projects": ("description", "title"),
|
|
"pull_auto_merges": ("merge_style", "message"),
|
|
"pull_request_wrappers": ("content", "name"),
|
|
"releases": ("note", "tag_name", "target", "title"),
|
|
"repo_units": ("config",),
|
|
"review_states": ("updated_files",),
|
|
"reviews": ("content",),
|
|
}
|
|
GITEA_SALVAGE_CLOSURE_NUMERIC_COLUMNS = {
|
|
"action_artifacts": ("file_compressed_size", "file_size"),
|
|
"action_tasks": ("log_length", "log_size"),
|
|
}
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_COUNTS = (
|
|
"comment_assignee",
|
|
"comment_assignee_team",
|
|
"comment_cross_reference",
|
|
"comment_cross_reference_comment",
|
|
"comment_current_milestone",
|
|
"comment_current_project",
|
|
"comment_dependent_issue",
|
|
"comment_label",
|
|
"comment_old_milestone",
|
|
"comment_old_project",
|
|
"comment_resolve_doer",
|
|
"comment_review",
|
|
"comment_tracked_time",
|
|
"content_history_comment",
|
|
"content_history_issue",
|
|
"pull_merger",
|
|
"review_reviewer_team",
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_ACTORS = (
|
|
"comment_assignee",
|
|
"comment_resolve_doer",
|
|
"pull_merger",
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_REPOSITORIES = (
|
|
"comment_cross_reference",
|
|
"comment_current_milestone",
|
|
"comment_current_project",
|
|
"comment_dependent_issue",
|
|
"comment_label",
|
|
"comment_old_milestone",
|
|
"comment_old_project",
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_NULLABLE_COLUMNS = (
|
|
"comment.assignee_id",
|
|
"comment.dependent_issue_id",
|
|
"comment.label_id",
|
|
"comment.milestone_id",
|
|
"comment.old_milestone_id",
|
|
"comment.old_project_id",
|
|
"comment.original_author_id",
|
|
"comment.project_id",
|
|
"comment.ref_action",
|
|
"comment.ref_comment_id",
|
|
"comment.ref_is_pull",
|
|
"comment.ref_issue_id",
|
|
"comment.ref_repo_id",
|
|
"comment.resolve_doer_id",
|
|
"comment.review_id",
|
|
"comment.time_id",
|
|
"issue_content_history.comment_id",
|
|
"pull_request.merger_id",
|
|
"review.original_author_id",
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_EXTERNAL_AUTHOR_SOURCES = ("comment", "review")
|
|
GITEA_SALVAGE_DISPOSITION_REMAINING_BLOCKERS = (
|
|
"attachment-physical-verifier-pending",
|
|
"candidate-root-activation-hard-frozen",
|
|
"collaboration-kept-user-mapping-verifier-pending",
|
|
"forensic-ref-archive-verifier-pending",
|
|
"issue-pr-metadata-sanitized-archive-verifier-pending",
|
|
"lfs-reachable-pointer-physical-verifier-pending",
|
|
"package-action-physical-closure-verifier-pending",
|
|
"reference-manifest-fsck-reachability-verifier-pending",
|
|
"repository-object-reconstruction-verifier-pending",
|
|
"target-unit-policy-acceptance-pending",
|
|
"unsupported-schema-catalog-verifier-pending",
|
|
)
|
|
GITEA_SALVAGE_CLOSURE_REMAINING_BLOCKERS = (
|
|
"attachment-physical-verifier-pending",
|
|
"candidate-root-activation-hard-frozen",
|
|
"closure-report-review-pin-pending",
|
|
"collaboration-kept-user-mapping-verifier-pending",
|
|
"forensic-ref-archive-verifier-pending",
|
|
"issue-pr-metadata-sanitized-archive-verifier-pending",
|
|
"lfs-reachable-pointer-physical-verifier-pending",
|
|
"package-action-physical-closure-verifier-pending",
|
|
"reference-manifest-fsck-reachability-verifier-pending",
|
|
"repository-object-reconstruction-verifier-pending",
|
|
"target-unit-policy-acceptance-pending",
|
|
"unsupported-schema-catalog-verifier-pending",
|
|
)
|
|
GITEA_SALVAGE_FS_IOC_GETFLAGS = 0x80086601
|
|
GITEA_SALVAGE_FS_NOCOW_FL = 0x00800000
|
|
GITEA_SALVAGE_EXCLUSIVE_ALLOCATION_BUDGET_BYTES = 512 * 1024 * 1024
|
|
GITEA_SALVAGE_DERIVED_INFO_PACKS_MAX_BYTES = 1024 * 1024
|
|
GITEA_SALVAGE_DERIVED_PACK_BITMAP_MAX_BYTES = 2 * 1024 * 1024
|
|
GITEA_SALVAGE_DERIVED_COMMIT_GRAPH_MAX_BYTES = 1024 * 1024
|
|
GITEA_SALVAGE_EVIDENCE_MAX_BYTES = 8 * 1024 * 1024
|
|
GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES = 16 * 1024**4
|
|
GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES = (1 << 63) - 1
|
|
GITEA_SALVAGE_EXPECTED_UNSUPPORTED_SCHEMA_SHA256 = None
|
|
GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TABLES = (
|
|
("issues", "issue", "repo_id"),
|
|
("pull_requests_base", "pull_request", "base_repo_id"),
|
|
("pull_requests_head", "pull_request", "head_repo_id"),
|
|
("releases", "release", "repo_id"),
|
|
("attachments", "attachment", "repo_id"),
|
|
("collaborators", "collaboration", "repo_id"),
|
|
("deploy_keys", "deploy_key", "repo_id"),
|
|
("webhooks", "webhook", "repo_id"),
|
|
("protected_branches", "protected_branch", "repo_id"),
|
|
("milestones", "milestone", "repo_id"),
|
|
("labels", "label", "repo_id"),
|
|
("stars", "star", "repo_id"),
|
|
("watches", "watch", "repo_id"),
|
|
("access_grants", "access", "repo_id"),
|
|
("topics", "repo_topic", "repo_id"),
|
|
("mirrors", "mirror", "repo_id"),
|
|
("push_mirrors", "push_mirror", "repo_id"),
|
|
("lfs_objects", "lfs_meta_object", "repository_id"),
|
|
("lfs_locks", "lfs_lock", "repo_id"),
|
|
("packages", "package", "repo_id"),
|
|
("action_runs", "action_run", "repo_id"),
|
|
("action_schedules", "action_schedule", "repo_id"),
|
|
("action_runners", "action_runner", "repo_id"),
|
|
("action_variables", "action_variable", "repo_id"),
|
|
("action_secrets", "secret", "repo_id"),
|
|
)
|
|
GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TEXT_METADATA = (
|
|
"description",
|
|
"website",
|
|
"original_url",
|
|
"topics",
|
|
"avatar",
|
|
)
|
|
GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_NUMERIC_HINTS = (
|
|
"num_watches",
|
|
"num_stars",
|
|
"num_issues",
|
|
"num_pulls",
|
|
"num_milestones",
|
|
"num_projects",
|
|
"num_action_runs",
|
|
"lfs_size",
|
|
)
|
|
# This is a code-owned, schema-only review surface. Table and column names from
|
|
# the compromised database never become SQL identifiers. Direct repository
|
|
# relations above are counted now; the tables below expose the exact schema
|
|
# needed to design a later, reviewed issue/PR/package/Actions dependency closure.
|
|
GITEA_SALVAGE_UNSUPPORTED_SCHEMA_TABLES = tuple(
|
|
sorted(
|
|
{
|
|
"repository",
|
|
"repo_unit",
|
|
*(table for _label, table, _column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TABLES),
|
|
"action_artifact",
|
|
"action_run_index",
|
|
"action_run_job",
|
|
"action_task",
|
|
"attachment",
|
|
"comment",
|
|
"issue_assignees",
|
|
"issue_content_history",
|
|
"issue_dependency",
|
|
"issue_label",
|
|
"issue_user",
|
|
"issue_watch",
|
|
"notification",
|
|
"package_blob",
|
|
"package_file",
|
|
"package_property",
|
|
"package_version",
|
|
"project",
|
|
"project_board",
|
|
"project_issue",
|
|
"pull_auto_merge",
|
|
"reaction",
|
|
"review",
|
|
"review_state",
|
|
"stopwatch",
|
|
"tracked_time",
|
|
}
|
|
)
|
|
)
|
|
|
|
COMPONENTS = {
|
|
"mission-core-map-access": {
|
|
"payload_root": Path("/volume1/docker/nodedc-platform/mission-core-map-access"),
|
|
"bootstrap_root": True,
|
|
"artifact_only": True,
|
|
"services": (),
|
|
},
|
|
"engine": {
|
|
"payload_root": Path("/volume2/nodedc-demo"),
|
|
"compose_root": Path("/volume2/nodedc-demo"),
|
|
"services": ("nodedc-backend", "app"),
|
|
"compose_no_deps": True,
|
|
"publish_dist": True,
|
|
"healthchecks": (
|
|
"http://127.0.0.1:8080/",
|
|
"http://127.0.0.1:3001/health",
|
|
),
|
|
},
|
|
"launcher": {
|
|
"payload_root": Path("/volume1/docker/nodedc-platform/launcher/source"),
|
|
"build_root": Path("/volume1/docker/nodedc-platform/launcher/source"),
|
|
"build": ("build", "--no-cache", "-t", "nodedc/launcher:local", "."),
|
|
"compose_root": Path("/volume1/docker/nodedc-platform/platform"),
|
|
"compose_env_file": Path("/volume1/docker/nodedc-platform/platform/.env.synology"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml"),
|
|
),
|
|
"compose_no_deps": True,
|
|
"services": ("launcher",),
|
|
"healthchecks": (
|
|
{
|
|
"url": "http://127.0.0.1:18080/healthz",
|
|
"headers": {"Host": "hub.nodedc.ru"},
|
|
},
|
|
),
|
|
},
|
|
"platform": {
|
|
"payload_root": Path("/volume1/docker/nodedc-platform"),
|
|
"compose_root": Path("/volume1/docker/nodedc-platform/platform"),
|
|
"compose_env_file": Path("/volume1/docker/nodedc-platform/platform/.env.synology"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml"),
|
|
Path("/volume1/docker/nodedc-platform/platform/docker-compose.external-data-plane.yml"),
|
|
),
|
|
"compose_no_deps": True,
|
|
"services": ("notification-postgres", "notification-core", "ai-workspace-hub", "launcher", "reverse-proxy", "external-data-plane-postgres", "external-data-plane", "map-gateway"),
|
|
"healthchecks": (
|
|
"http://127.0.0.1:5185/healthz",
|
|
"http://127.0.0.1:18081/healthz",
|
|
{
|
|
"url": "http://127.0.0.1:18080/healthz",
|
|
"headers": {"Host": "hub.nodedc.ru"},
|
|
},
|
|
{
|
|
"url": "http://127.0.0.1:18080/",
|
|
"headers": {"Host": "id.nodedc.ru"},
|
|
},
|
|
),
|
|
},
|
|
"tasker": {
|
|
"payload_root": Path("/volume1/docker/nodedc-platform/tasker"),
|
|
"compose_root": Path("/volume1/docker/nodedc-platform/tasker/plane-app"),
|
|
"compose_project": "nodedc-tasker",
|
|
"compose_env_file": Path("/volume1/docker/nodedc-platform/tasker/plane-app/.env.synology"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/nodedc-platform/tasker/plane-app/docker-compose.yaml"),
|
|
Path("/volume1/docker/nodedc-platform/tasker/plane-app/docker-compose.synology.override.yml"),
|
|
),
|
|
"services": ("api", "worker", "beat-worker", "web"),
|
|
"healthchecks": (
|
|
{
|
|
"url": "http://127.0.0.1:18090/",
|
|
"headers": {"Host": "ops.nodedc.ru"},
|
|
},
|
|
),
|
|
},
|
|
"ops-agents": {
|
|
"payload_root": Path("/volume1/docker/nodedc-platform/ops-agents"),
|
|
"compose_root": Path("/volume1/docker/nodedc-platform/ops-agents"),
|
|
"compose_env_file": Path("/volume1/docker/nodedc-platform/ops-agents/.env"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/nodedc-platform/ops-agents/docker-compose.synology.yml"),
|
|
),
|
|
"compose_build": True,
|
|
"compose_no_deps": True,
|
|
"services": ("agent-gateway",),
|
|
"healthchecks": (
|
|
"http://172.22.0.222:18190/readyz",
|
|
),
|
|
},
|
|
"bim-viewer": {
|
|
"payload_root": Path("/volume1/docker/nodedc-platform/bim-viewer/source"),
|
|
"build_root": Path("/volume1/docker/nodedc-platform/bim-viewer/source/converter"),
|
|
"build": ("build", "--no-cache", "-t", "nodedc/bim-converter:local", "."),
|
|
"compose_root": Path("/volume1/docker/nodedc-platform/bim-viewer/source"),
|
|
"compose_env_file": Path("/volume1/docker/nodedc-platform/bim-viewer/source/.env"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/nodedc-platform/bim-viewer/source/docker-compose.beam.yml"),
|
|
),
|
|
"compose_no_deps": True,
|
|
"services": ("ndc-beam-viewer", "nodedc-bim-converter"),
|
|
"healthchecks": (
|
|
"http://127.0.0.1:18100/api/auth/session",
|
|
),
|
|
},
|
|
"n8n-private-extension": {
|
|
# This component only stages a verified, immutable offline release in
|
|
# Platform-owned storage. It deliberately has no Compose file, service,
|
|
# container mutation or Engine activation side effect.
|
|
"payload_root": N8N_PRIVATE_EXTENSION_RELEASES_ROOT,
|
|
"bootstrap_root": True,
|
|
"artifact_only": True,
|
|
"immutable_payload": True,
|
|
"services": (),
|
|
},
|
|
"module-foundry": {
|
|
"payload_root": Path("/volume1/docker/nodedc-platform/module-foundry/source"),
|
|
"compose_root": Path("/volume1/docker/nodedc-platform/module-foundry/source/infra"),
|
|
"compose_project": "nodedc-module-foundry",
|
|
"compose_env_file": Path("/volume1/docker/nodedc-platform/module-foundry/source/.env"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/nodedc-platform/module-foundry/source/infra/docker-compose.module-foundry.yml"),
|
|
),
|
|
"bootstrap_root": True,
|
|
"compose_build": True,
|
|
"compose_no_deps": True,
|
|
"services": ("nodedc-module-foundry",),
|
|
"healthchecks": (
|
|
"http://172.22.0.222:9920/healthz",
|
|
),
|
|
},
|
|
"device-plane": {
|
|
"payload_root": DEVICE_PLANE_ROOT,
|
|
"compose_root": DEVICE_PLANE_ROOT,
|
|
"compose_project": "nodedc-device-plane",
|
|
"compose_files": (
|
|
DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml",
|
|
),
|
|
"bootstrap_root": True,
|
|
"compose_no_deps": True,
|
|
# PostgreSQL is durable state infrastructure. The default legacy
|
|
# selection remains Core + Gateway; the exact Device Manager slice
|
|
# separately registers the third stateless service.
|
|
"services": ("device-control-core", "device-gateway"),
|
|
},
|
|
"gitea": {
|
|
"payload_root": GITEA_ROOT,
|
|
"compose_root": GITEA_ROOT,
|
|
"compose_project": GITEA_COMPOSE_PROJECT,
|
|
"compose_files": (GITEA_ROOT / GITEA_COMPOSE_REL,),
|
|
"bootstrap_root": True,
|
|
"compose_no_deps": True,
|
|
"services": (GITEA_SERVICE,),
|
|
},
|
|
"proxy-contur": {
|
|
"payload_root": Path("/volume1/docker/proxy-contur"),
|
|
"compose_root": Path("/volume1/docker/proxy-contur"),
|
|
"compose_project": "proxy-contur",
|
|
"compose_env_file": Path("/volume1/docker/proxy-contur/.env"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/proxy-contur/docker-compose.yml"),
|
|
),
|
|
"compose_build": True,
|
|
"compose_no_deps": True,
|
|
"services": ("proxy-contur",),
|
|
"health_container": "proxy-contur",
|
|
},
|
|
"dc-amd-proxy": {
|
|
"payload_root": Path("/volume1/docker/dc-amd-proxy"),
|
|
"compose_root": Path("/volume1/docker/dc-amd-proxy"),
|
|
"compose_project": "dc-amd-proxy",
|
|
"bootstrap_root": True,
|
|
"compose_build": True,
|
|
"compose_no_deps": True,
|
|
"services": ("dc-amd-proxy",),
|
|
"health_container": "dc-amd-proxy",
|
|
},
|
|
"dc-cms": {
|
|
"payload_root": Path("/volume1/docker/dc-cms/source"),
|
|
"compose_root": Path("/volume1/docker/dc-cms/source/infra"),
|
|
"compose_env_file": Path("/volume1/docker/dc-cms/source/infra/.env.synology"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/dc-cms/source/infra/docker-compose.yml"),
|
|
),
|
|
"bootstrap_root": True,
|
|
"compose_build": True,
|
|
"services": ("postgresql-authentik", "authentik-server", "authentik-worker", "authentik-bootstrap", "cms-app", "reverse-proxy"),
|
|
"healthchecks": (
|
|
{
|
|
"url": "http://172.22.0.222:9918/auth/login?returnTo=%2F",
|
|
"headers": {"Host": "cms.dcserve.ru"},
|
|
},
|
|
{
|
|
"url": "http://172.22.0.222:9919/",
|
|
"headers": {"Host": "auth.dcserve.ru"},
|
|
},
|
|
),
|
|
},
|
|
"dc-cms-site-nodedc": {
|
|
"payload_root": Path("/volume1/docker/dc-cms/sites/nodedc"),
|
|
"compose_root": Path("/volume1/docker/dc-cms/source/infra"),
|
|
"compose_env_file": Path("/volume1/docker/dc-cms/source/infra/.env.synology"),
|
|
"compose_files": (
|
|
Path("/volume1/docker/dc-cms/source/infra/docker-compose.yml"),
|
|
),
|
|
"compose_no_deps": True,
|
|
"services": ("cms-app", "reverse-proxy"),
|
|
"healthchecks": (
|
|
{
|
|
"url": "http://172.22.0.222:9918/auth/login?returnTo=%2F",
|
|
"headers": {"Host": "cms.dcserve.ru"},
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
|
|
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
return None
|
|
|
|
|
|
NO_REDIRECT_OPENER = urllib.request.build_opener(NoRedirectHandler)
|
|
|
|
|
|
class DeployError(Exception):
|
|
pass
|
|
|
|
|
|
class ReconciliationRequired(DeployError):
|
|
pass
|
|
|
|
|
|
def die(message):
|
|
raise DeployError(message)
|
|
|
|
|
|
def utc_now():
|
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
|
|
|
|
def stamp():
|
|
return datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
|
|
|
|
def sha256_file(path):
|
|
import hashlib
|
|
|
|
h = hashlib.sha256()
|
|
with path.open("rb") as f:
|
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
def sha256_json_value(value):
|
|
import hashlib
|
|
|
|
canonical = json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
).encode("utf-8")
|
|
return hashlib.sha256(canonical).hexdigest()
|
|
|
|
|
|
def safe_name(value):
|
|
return re.sub(r"[^A-Za-z0-9._-]", "_", value)
|
|
|
|
|
|
def is_relative_to(child, parent):
|
|
try:
|
|
child.relative_to(parent)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def ensure_layout():
|
|
for path in (INBOX, APPLIED_DIR, FAILED_DIR, BACKUPS_DIR, STATE_DIR, TMP_DIR, RUNTIME_DIR):
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
|
|
for path in (STATE_DIR, BACKUPS_DIR, TMP_DIR, RUNTIME_DIR):
|
|
os.chown(path, 0, 0)
|
|
path.chmod(0o700)
|
|
|
|
for path in (APPLIED_DIR, FAILED_DIR):
|
|
os.chown(path, 0, 0)
|
|
path.chmod(0o755)
|
|
|
|
|
|
def ensure_platform_runtime_secret(secret_file, secret_re, label):
|
|
# This is Platform infrastructure state, not a product setting. The
|
|
# root-owned runner creates it once and only explicitly bound service mounts
|
|
# can read it. It must never be copied into an artifact or a shared .env.
|
|
secret_dir = secret_file.parent
|
|
try:
|
|
directory_stat = secret_dir.lstat()
|
|
except FileNotFoundError:
|
|
secret_dir.mkdir(parents=True, exist_ok=False)
|
|
directory_stat = secret_dir.lstat()
|
|
|
|
if stat.S_ISLNK(directory_stat.st_mode) or not stat.S_ISDIR(directory_stat.st_mode):
|
|
die(f"{label} secret directory is unsafe: {secret_dir}")
|
|
os.chown(secret_dir, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
secret_dir.chmod(0o710)
|
|
|
|
try:
|
|
secret_stat = secret_file.lstat()
|
|
except FileNotFoundError:
|
|
secret_value = secrets.token_urlsafe(48)
|
|
temporary = secret_dir / f".{secret_file.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640)
|
|
os.write(descriptor, f"{secret_value}\n".encode("ascii"))
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
os.fchmod(descriptor, 0o640)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary, secret_file)
|
|
fsync_directory(secret_dir)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
return "created"
|
|
|
|
if stat.S_ISLNK(secret_stat.st_mode) or not stat.S_ISREG(secret_stat.st_mode):
|
|
die(f"{label} secret file is unsafe: {secret_file}")
|
|
if secret_stat.st_uid != 0 or secret_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
|
die(f"{label} secret file has unsafe ownership or permissions: {secret_file}")
|
|
if secret_stat.st_size > 512:
|
|
die(f"{label} secret file is too large: {secret_file}")
|
|
try:
|
|
secret_value = secret_file.read_text(encoding="ascii").strip()
|
|
except UnicodeDecodeError:
|
|
die(f"{label} secret file is not ascii: {secret_file}")
|
|
if not secret_re.fullmatch(secret_value):
|
|
die(f"{label} secret file has invalid format: {secret_file}")
|
|
os.chown(secret_file, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
secret_file.chmod(0o640)
|
|
return "reused"
|
|
|
|
|
|
def ensure_map_gateway_admin_secret():
|
|
return ensure_platform_runtime_secret(
|
|
MAP_GATEWAY_SECRET_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"map gateway",
|
|
)
|
|
|
|
|
|
def read_proxy_contur_token():
|
|
try:
|
|
source_stat = PROXY_CONTUR_ENV_FILE.lstat()
|
|
except FileNotFoundError:
|
|
die("proxy-contur env file is required for Map egress")
|
|
if stat.S_ISLNK(source_stat.st_mode) or not stat.S_ISREG(source_stat.st_mode):
|
|
die("proxy-contur env file is unsafe")
|
|
if source_stat.st_size > 64 * 1024 or source_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
|
die("proxy-contur env file has unsafe permissions")
|
|
try:
|
|
lines = PROXY_CONTUR_ENV_FILE.read_text(encoding="utf-8").splitlines()
|
|
except UnicodeDecodeError:
|
|
die("proxy-contur env file is not utf-8")
|
|
|
|
value = None
|
|
for raw in lines:
|
|
line = raw.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if line.startswith("export "):
|
|
line = line[7:].lstrip()
|
|
if not line.startswith("PROXY_TOKEN="):
|
|
continue
|
|
value = line.partition("=")[2].strip()
|
|
if len(value) >= 2 and value[0] in ("'", '"') and value[-1] == value[0]:
|
|
value = value[1:-1]
|
|
break
|
|
|
|
if not value or len(value) > 4096 or any(ord(char) < 33 or ord(char) == 127 for char in value):
|
|
die("proxy-contur PROXY_TOKEN is missing or invalid")
|
|
return value
|
|
|
|
|
|
def sync_map_egress_proxy_secret():
|
|
# The Map Gateway gets a read-only file copy of the existing proxy token.
|
|
# The value is never printed, placed in an artifact, or exposed to Foundry.
|
|
token = read_proxy_contur_token()
|
|
try:
|
|
parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False)
|
|
parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(parent_stat.st_mode):
|
|
die(f"map egress parent secret directory is unsafe: {MAP_GATEWAY_SECRET_DIR}")
|
|
os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
MAP_GATEWAY_SECRET_DIR.chmod(0o710)
|
|
|
|
if MAP_EGRESS_PROXY_SECRET_FILE.exists() or MAP_EGRESS_PROXY_SECRET_FILE.is_symlink():
|
|
target_stat = MAP_EGRESS_PROXY_SECRET_FILE.lstat()
|
|
if stat.S_ISLNK(target_stat.st_mode) or not stat.S_ISREG(target_stat.st_mode):
|
|
die("map egress proxy secret file is unsafe")
|
|
|
|
temporary = MAP_GATEWAY_SECRET_DIR / f".{MAP_EGRESS_PROXY_SECRET_FILE.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640)
|
|
os.write(descriptor, f"{token}\n".encode("utf-8"))
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
os.fchmod(descriptor, 0o640)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary, MAP_EGRESS_PROXY_SECRET_FILE)
|
|
fsync_directory(MAP_GATEWAY_SECRET_DIR)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
return "synced"
|
|
|
|
|
|
def ensure_root_owned_grant_directory(path, label):
|
|
try:
|
|
parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False)
|
|
parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(parent_stat.st_mode):
|
|
die(f"{label} parent directory is unsafe: {MAP_GATEWAY_SECRET_DIR}")
|
|
os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
MAP_GATEWAY_SECRET_DIR.chmod(0o710)
|
|
|
|
try:
|
|
directory_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
path.mkdir(parents=False, exist_ok=False)
|
|
directory_stat = path.lstat()
|
|
if stat.S_ISLNK(directory_stat.st_mode) or not stat.S_ISDIR(directory_stat.st_mode):
|
|
die(f"{label} directory is unsafe: {path}")
|
|
os.chown(path, 0, 0)
|
|
path.chmod(0o500)
|
|
|
|
for entry in path.iterdir():
|
|
entry_stat = entry.lstat()
|
|
if (not re.fullmatch(r"[a-f0-9]{64}", entry.name)
|
|
or stat.S_ISLNK(entry_stat.st_mode)
|
|
or not stat.S_ISREG(entry_stat.st_mode)
|
|
or entry_stat.st_uid != 0
|
|
or entry_stat.st_gid != 0
|
|
or stat.S_IMODE(entry_stat.st_mode) != 0o400
|
|
or entry_stat.st_size < 2
|
|
or entry_stat.st_size > 128 * 1024):
|
|
die(f"{label} record is unsafe: {entry}")
|
|
|
|
|
|
def ensure_external_data_plane_provisioner_secret():
|
|
# Unlike the Map Gateway key, this capability can mint scoped writers.
|
|
# Keep it in an isolated child directory readable only by the dedicated
|
|
# EDP/provisioner uid, never by the shared gid 1000.
|
|
try:
|
|
parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False)
|
|
parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(parent_stat.st_mode):
|
|
die(f"external data plane parent secret directory is unsafe: {MAP_GATEWAY_SECRET_DIR}")
|
|
os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
MAP_GATEWAY_SECRET_DIR.chmod(0o710)
|
|
|
|
try:
|
|
directory_stat = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR.mkdir(parents=False, exist_ok=False)
|
|
directory_stat = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR.lstat()
|
|
if stat.S_ISLNK(directory_stat.st_mode) or not stat.S_ISDIR(directory_stat.st_mode):
|
|
die(f"external data plane provisioner secret directory is unsafe: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR}")
|
|
os.chown(EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR, EXTERNAL_DATA_PLANE_RUNTIME_UID, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR.chmod(0o500)
|
|
|
|
try:
|
|
secret_stat = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE.lstat()
|
|
except FileNotFoundError:
|
|
secret_value = secrets.token_urlsafe(48)
|
|
temporary = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / f".{EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o400)
|
|
os.write(descriptor, f"{secret_value}\n".encode("ascii"))
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, EXTERNAL_DATA_PLANE_RUNTIME_UID, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
os.fchmod(descriptor, 0o400)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary, EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE)
|
|
fsync_directory(EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
return "created"
|
|
|
|
if stat.S_ISLNK(secret_stat.st_mode) or not stat.S_ISREG(secret_stat.st_mode):
|
|
die(f"external data plane provisioner secret file is unsafe: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
|
|
if (secret_stat.st_uid != EXTERNAL_DATA_PLANE_RUNTIME_UID or
|
|
secret_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID or
|
|
stat.S_IMODE(secret_stat.st_mode) != 0o400):
|
|
die(f"external data plane provisioner secret file has unsafe ownership or permissions: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
|
|
if secret_stat.st_size > 512:
|
|
die(f"external data plane provisioner secret file is too large: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
|
|
try:
|
|
secret_value = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE.read_text(encoding="ascii").strip()
|
|
except UnicodeDecodeError:
|
|
die(f"external data plane provisioner secret file is not ascii: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
|
|
if not EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_RE.fullmatch(secret_value):
|
|
die(f"external data plane provisioner secret file has invalid format: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
|
|
return "reused"
|
|
|
|
|
|
def resolve_openssl_binary():
|
|
candidate = shutil.which("openssl")
|
|
if not candidate:
|
|
die("openssl is required to provision runtime identities")
|
|
path = Path(candidate).resolve()
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die("resolved openssl binary is missing")
|
|
if (not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_uid != 0
|
|
or path_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)):
|
|
die(f"openssl binary is unsafe: {path}")
|
|
return path
|
|
|
|
|
|
def run_openssl(arguments, label):
|
|
result = subprocess.run(
|
|
[str(resolve_openssl_binary()), *arguments],
|
|
check=False,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.PIPE,
|
|
timeout=30,
|
|
)
|
|
if result.returncode != 0:
|
|
detail = re.sub(
|
|
r"[^A-Za-z0-9 ._:/()\[\],+-]",
|
|
"?",
|
|
result.stderr.decode("utf-8", errors="replace").strip(),
|
|
)[:320]
|
|
die(
|
|
f"openssl {label} failed"
|
|
+ (f": {detail}" if detail else "")
|
|
)
|
|
|
|
|
|
def capture_openssl(arguments, label):
|
|
result = subprocess.run(
|
|
[str(resolve_openssl_binary()), *arguments],
|
|
check=False,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
timeout=30,
|
|
)
|
|
if result.returncode != 0:
|
|
detail = re.sub(
|
|
r"[^A-Za-z0-9 ._:/()\[\],+-]",
|
|
"?",
|
|
result.stderr.decode("utf-8", errors="replace").strip(),
|
|
)[:320]
|
|
die(
|
|
f"openssl {label} failed"
|
|
+ (f": {detail}" if detail else "")
|
|
)
|
|
return result.stdout
|
|
|
|
|
|
def ensure_safe_runtime_directory(path, mode, label):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
path.mkdir(parents=True, exist_ok=False)
|
|
path_stat = path.lstat()
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISDIR(path_stat.st_mode):
|
|
die(f"{label} directory is unsafe: {path}")
|
|
os.chown(path, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
path.chmod(mode)
|
|
|
|
|
|
def device_edge_channel_certificate_fingerprint(path):
|
|
raw = capture_openssl(
|
|
["x509", "-in", str(path), "-noout", "-fingerprint", "-sha256"],
|
|
"Device Edge channel certificate fingerprint",
|
|
).decode("ascii", errors="strict").strip()
|
|
match = re.fullmatch(
|
|
r"SHA256 Fingerprint=((?:[A-F0-9]{2}:){31}[A-F0-9]{2})",
|
|
raw,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
if not match:
|
|
die("Device Edge channel certificate fingerprint is invalid")
|
|
return match.group(1).upper()
|
|
|
|
|
|
def install_public_runtime_export(source, destination, mode, label):
|
|
source_bytes = source.read_bytes()
|
|
if b"PRIVATE KEY" in source_bytes:
|
|
die(f"{label} contains private key material")
|
|
try:
|
|
destination_stat = destination.lstat()
|
|
except FileNotFoundError:
|
|
destination_stat = None
|
|
if destination_stat is not None and (
|
|
stat.S_ISLNK(destination_stat.st_mode)
|
|
or not stat.S_ISREG(destination_stat.st_mode)
|
|
):
|
|
die(f"{label} destination is unsafe: {destination}")
|
|
temporary = destination.parent / (
|
|
f".{destination.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(
|
|
str(temporary),
|
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
mode,
|
|
)
|
|
os.write(descriptor, source_bytes)
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
os.fchmod(descriptor, mode)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary, destination)
|
|
fsync_directory(destination.parent)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
|
|
|
|
def generate_device_edge_channel_core_identity(private_key, certificate):
|
|
# Never inherit Synology's global /etc/ssl/openssl.cnf. DSM currently
|
|
# points req.x509_extensions at v3_ca, which otherwise adds CA:TRUE before
|
|
# our workload certificate extensions and produces a conflicting cert.
|
|
config = private_key.parent / (
|
|
f".device-edge-core-openssl.{os.getpid()}.{time.time_ns()}.cnf"
|
|
)
|
|
config_text = """[ req ]
|
|
prompt = no
|
|
distinguished_name = device_edge_core_dn
|
|
x509_extensions = device_edge_core_client
|
|
|
|
[ device_edge_core_dn ]
|
|
CN = nodedc-device-control-core
|
|
|
|
[ device_edge_core_client ]
|
|
basicConstraints = critical,CA:FALSE
|
|
keyUsage = critical,digitalSignature
|
|
extendedKeyUsage = clientAuth
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid,issuer
|
|
"""
|
|
try:
|
|
descriptor = os.open(
|
|
str(config),
|
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
0o600,
|
|
)
|
|
try:
|
|
os.write(descriptor, config_text.encode("ascii"))
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
run_openssl([
|
|
"req", "-x509", "-newkey", "ed25519", "-nodes",
|
|
"-days", "3650",
|
|
"-config", str(config),
|
|
"-extensions", "device_edge_core_client",
|
|
"-keyout", str(private_key),
|
|
"-out", str(certificate),
|
|
], "Device Edge channel Core identity generation")
|
|
finally:
|
|
if config.exists():
|
|
config.unlink()
|
|
|
|
|
|
def device_edge_channel_certificate_text(path):
|
|
return capture_openssl(
|
|
["x509", "-in", str(path), "-noout", "-text"],
|
|
"Device Edge channel certificate extension inspection",
|
|
).decode("utf-8", errors="strict")
|
|
|
|
|
|
def validate_device_edge_channel_certificate_extensions(path):
|
|
text = device_edge_channel_certificate_text(path)
|
|
required = (
|
|
("X509v3 Basic Constraints: critical", "CA:FALSE"),
|
|
("X509v3 Key Usage: critical", "Digital Signature"),
|
|
("X509v3 Extended Key Usage:", "TLS Web Client Authentication"),
|
|
)
|
|
for heading, value in required:
|
|
if text.count(heading) != 1 or text.count(value) != 1:
|
|
die("Device Edge channel Core certificate extension mismatch")
|
|
if "CA:TRUE" in text or "Certificate Sign" in text:
|
|
die("Device Edge channel Core certificate CA capability is forbidden")
|
|
return "exact-clientAuth"
|
|
|
|
|
|
def device_edge_channel_invalid_identity_is_exact_recoverable():
|
|
private_key = DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE
|
|
certificate = DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE
|
|
if not private_key.is_file() or not certificate.is_file():
|
|
return False
|
|
if (
|
|
device_edge_channel_certificate_fingerprint(certificate)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_INVALID_CERTIFICATE_FINGERPRINT
|
|
):
|
|
return False
|
|
text = device_edge_channel_certificate_text(certificate)
|
|
if (
|
|
text.count("X509v3 Basic Constraints:") != 2
|
|
or text.count("CA:TRUE") != 1
|
|
or text.count("CA:FALSE") != 1
|
|
):
|
|
return False
|
|
if (
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE.exists()
|
|
or DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE.is_symlink()
|
|
or DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_FINGERPRINT_FILE.exists()
|
|
or DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_FINGERPRINT_FILE.is_symlink()
|
|
):
|
|
return False
|
|
if any(DEVICE_PLANE_EDGE_CHANNEL_PEER_TRUST_DIR.iterdir()):
|
|
return False
|
|
certificate_public = capture_openssl(
|
|
["x509", "-in", str(certificate), "-pubkey", "-noout"],
|
|
"invalid Device Edge certificate public key",
|
|
)
|
|
private_public = capture_openssl(
|
|
["pkey", "-in", str(private_key), "-pubout"],
|
|
"invalid Device Edge private key public derivation",
|
|
)
|
|
return certificate_public == private_public
|
|
|
|
|
|
def recover_invalid_device_edge_channel_core_identity():
|
|
if not device_edge_channel_invalid_identity_is_exact_recoverable():
|
|
die(
|
|
"Device Edge channel Core identity is invalid but does not match "
|
|
"the exact unexported failed-016 recovery boundary"
|
|
)
|
|
recovery = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_RECOVERY_DIR
|
|
/ DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_PATCH_ID
|
|
)
|
|
if recovery.exists() or recovery.is_symlink():
|
|
die("Device Edge channel Core identity recovery destination exists")
|
|
DEVICE_PLANE_EDGE_CHANNEL_RECOVERY_DIR.mkdir(mode=0o700, exist_ok=False)
|
|
os.chown(DEVICE_PLANE_EDGE_CHANNEL_RECOVERY_DIR, 0, 0)
|
|
recovery.mkdir(mode=0o700)
|
|
os.chown(recovery, 0, 0)
|
|
private_destination = recovery / "core-private-key.invalid.pem"
|
|
certificate_destination = recovery / "core-certificate.invalid.pem"
|
|
os.replace(
|
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE,
|
|
private_destination,
|
|
)
|
|
os.replace(
|
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE,
|
|
certificate_destination,
|
|
)
|
|
os.chown(private_destination, 0, 0)
|
|
private_destination.chmod(0o600)
|
|
os.chown(certificate_destination, 0, 0)
|
|
certificate_destination.chmod(0o600)
|
|
fsync_directory(recovery)
|
|
fsync_directory(DEVICE_PLANE_EDGE_CHANNEL_RECOVERY_DIR)
|
|
fsync_directory(DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR)
|
|
return "failed-016-invalid-unexported-quarantined"
|
|
|
|
|
|
def ensure_device_edge_channel_core_identity(
|
|
*,
|
|
allow_invalid_unexported_recovery=False,
|
|
):
|
|
# The Core private key is born on Synology and never enters a deployment
|
|
# artifact, Compose environment, Ops, or runner output. Only the matching
|
|
# public certificate and fingerprint are exported for the explicit VPS
|
|
# trust handoff.
|
|
ensure_safe_runtime_directory(
|
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR,
|
|
0o710,
|
|
"Device Edge channel identity",
|
|
)
|
|
ensure_safe_runtime_directory(
|
|
DEVICE_PLANE_EDGE_CHANNEL_PEER_TRUST_DIR,
|
|
0o710,
|
|
"Device Edge channel peer trust",
|
|
)
|
|
private_key = DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE
|
|
certificate = DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE
|
|
private_exists = private_key.exists() or private_key.is_symlink()
|
|
certificate_exists = certificate.exists() or certificate.is_symlink()
|
|
if private_exists != certificate_exists:
|
|
die("Device Edge channel Core identity is incomplete")
|
|
|
|
recovered = False
|
|
if private_exists and allow_invalid_unexported_recovery:
|
|
if device_edge_channel_invalid_identity_is_exact_recoverable():
|
|
recover_invalid_device_edge_channel_core_identity()
|
|
private_exists = False
|
|
certificate_exists = False
|
|
recovered = True
|
|
|
|
created = False
|
|
if not private_exists:
|
|
private_tmp = private_key.with_name(
|
|
f".{private_key.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
certificate_tmp = certificate.with_name(
|
|
f".{certificate.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
try:
|
|
generate_device_edge_channel_core_identity(
|
|
private_tmp,
|
|
certificate_tmp,
|
|
)
|
|
os.chown(private_tmp, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
private_tmp.chmod(0o640)
|
|
os.chown(certificate_tmp, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
certificate_tmp.chmod(0o640)
|
|
fsync_file(private_tmp)
|
|
fsync_file(certificate_tmp)
|
|
os.replace(private_tmp, private_key)
|
|
os.replace(certificate_tmp, certificate)
|
|
fsync_directory(DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR)
|
|
created = True
|
|
finally:
|
|
if private_tmp.exists():
|
|
private_tmp.unlink()
|
|
if certificate_tmp.exists():
|
|
certificate_tmp.unlink()
|
|
|
|
for path, label in (
|
|
(private_key, "private key"),
|
|
(certificate, "certificate"),
|
|
):
|
|
path_stat = path.lstat()
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_uid != 0
|
|
or path_stat.st_gid != MAP_GATEWAY_RUNTIME_GID
|
|
or stat.S_IMODE(path_stat.st_mode) != 0o640
|
|
or path_stat.st_size < 64
|
|
or path_stat.st_size > 32 * 1024
|
|
):
|
|
die(f"Device Edge channel Core {label} boundary mismatch")
|
|
|
|
private_text = private_key.read_text(encoding="ascii")
|
|
certificate_text = certificate.read_text(encoding="ascii")
|
|
if "PRIVATE KEY" not in private_text or "PRIVATE KEY" in certificate_text:
|
|
die("Device Edge channel Core private/public boundary mismatch")
|
|
if certificate_text.count("-----BEGIN CERTIFICATE-----") != 1:
|
|
die("Device Edge channel Core certificate cardinality mismatch")
|
|
run_openssl(["pkey", "-in", str(private_key), "-check", "-noout"],
|
|
"Device Edge channel Core private key validation")
|
|
run_openssl(["x509", "-in", str(certificate), "-noout", "-checkend", "604800"],
|
|
"Device Edge channel Core certificate lifetime validation")
|
|
validate_device_edge_channel_certificate_extensions(certificate)
|
|
run_openssl([
|
|
"verify", "-purpose", "sslclient", "-CAfile", str(certificate),
|
|
str(certificate),
|
|
], "Device Edge channel Core certificate purpose validation")
|
|
certificate_public = capture_openssl(
|
|
["x509", "-in", str(certificate), "-pubkey", "-noout"],
|
|
"Device Edge channel Core certificate public key",
|
|
)
|
|
private_public = capture_openssl(
|
|
["pkey", "-in", str(private_key), "-pubout"],
|
|
"Device Edge channel Core private key public derivation",
|
|
)
|
|
if certificate_public != private_public:
|
|
die("Device Edge channel Core certificate/private key mismatch")
|
|
|
|
ensure_safe_runtime_directory(
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR,
|
|
0o755,
|
|
"Device Edge channel public export",
|
|
)
|
|
install_public_runtime_export(
|
|
certificate,
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE,
|
|
0o444,
|
|
"Device Edge channel Core certificate export",
|
|
)
|
|
fingerprint = device_edge_channel_certificate_fingerprint(certificate)
|
|
fingerprint_tmp = DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR / (
|
|
f".core-certificate.sha256.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
try:
|
|
fingerprint_tmp.write_text(f"SHA256={fingerprint}\n", encoding="ascii")
|
|
os.chown(fingerprint_tmp, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
fingerprint_tmp.chmod(0o444)
|
|
fsync_file(fingerprint_tmp)
|
|
os.replace(
|
|
fingerprint_tmp,
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_FINGERPRINT_FILE,
|
|
)
|
|
fsync_directory(DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR)
|
|
finally:
|
|
if fingerprint_tmp.exists():
|
|
fingerprint_tmp.unlink()
|
|
if recovered:
|
|
return "recovered+created"
|
|
return "created" if created else "reused"
|
|
|
|
|
|
def validate_device_edge_channel_public_export():
|
|
certificate = DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE
|
|
exported_certificate = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE
|
|
)
|
|
exported_fingerprint = (
|
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_FINGERPRINT_FILE
|
|
)
|
|
for path, label in (
|
|
(exported_certificate, "certificate"),
|
|
(exported_fingerprint, "fingerprint"),
|
|
):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Device Edge channel public {label} export is missing")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_uid != 0
|
|
or path_stat.st_gid != MAP_GATEWAY_RUNTIME_GID
|
|
or stat.S_IMODE(path_stat.st_mode) != 0o444
|
|
or path_stat.st_size < 32
|
|
or path_stat.st_size > 32 * 1024
|
|
):
|
|
die(f"Device Edge channel public {label} export is unsafe")
|
|
certificate_bytes = certificate.read_bytes()
|
|
exported_bytes = exported_certificate.read_bytes()
|
|
if b"PRIVATE KEY" in exported_bytes or exported_bytes != certificate_bytes:
|
|
die("Device Edge channel public certificate export mismatch")
|
|
fingerprint = device_edge_channel_certificate_fingerprint(certificate)
|
|
try:
|
|
fingerprint_text = exported_fingerprint.read_text(encoding="ascii")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Device Edge channel public fingerprint export is unreadable")
|
|
if fingerprint_text != f"SHA256={fingerprint}\n":
|
|
die("Device Edge channel public fingerprint export mismatch")
|
|
return "exact-public-only"
|
|
|
|
|
|
def validate_ed25519_public_key_file(path):
|
|
try:
|
|
value = path.read_text(encoding="ascii")
|
|
except (OSError, UnicodeDecodeError):
|
|
die(f"Engine credential issuer public key is unreadable: {path}")
|
|
match = re.fullmatch(
|
|
r"-----BEGIN PUBLIC KEY-----\n([A-Za-z0-9+/=\n]+)-----END PUBLIC KEY-----\n?",
|
|
value,
|
|
)
|
|
if not match:
|
|
die("Engine credential issuer public key PEM format mismatch")
|
|
try:
|
|
der = base64.b64decode(match.group(1).replace("\n", ""), validate=True)
|
|
except ValueError:
|
|
die("Engine credential issuer public key base64 is invalid")
|
|
# SubjectPublicKeyInfo for Ed25519 is the fixed RFC 8410 algorithm header
|
|
# plus one 32-byte public key. This rejects accidental RSA/EC key drift
|
|
# without printing either private or public key material.
|
|
if len(der) != 44 or not der.startswith(bytes.fromhex("302a300506032b6570032100")):
|
|
die("Engine credential issuer key is not Ed25519")
|
|
|
|
|
|
def fsync_file(path):
|
|
descriptor = os.open(str(path), os.O_RDONLY)
|
|
try:
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def ensure_engine_credential_issuer_keypair():
|
|
# The private issuer never enters an artifact, Compose environment or
|
|
# runner output. The Engine receives only its matching public key through
|
|
# the already persistent nodedc-data mount.
|
|
ensure_external_data_plane_provisioner_secret()
|
|
state_dir = ENGINE_CREDENTIAL_SINK_STATE_DIR
|
|
try:
|
|
state_stat = state_dir.lstat()
|
|
except FileNotFoundError:
|
|
state_dir.mkdir(parents=True, exist_ok=False)
|
|
state_stat = state_dir.lstat()
|
|
if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISDIR(state_stat.st_mode):
|
|
die(f"Engine credential sink state directory is unsafe: {state_dir}")
|
|
os.chown(state_dir, 0, 0)
|
|
state_dir.chmod(0o700)
|
|
|
|
private_key = ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE
|
|
public_key = ENGINE_CREDENTIAL_SINK_PUBLIC_KEY_FILE
|
|
private_exists = private_key.exists() or private_key.is_symlink()
|
|
public_exists = public_key.exists() or public_key.is_symlink()
|
|
if not private_exists and public_exists:
|
|
die("Engine credential issuer public key exists without its private key")
|
|
|
|
if private_exists:
|
|
private_stat = private_key.lstat()
|
|
if (stat.S_ISLNK(private_stat.st_mode)
|
|
or not stat.S_ISREG(private_stat.st_mode)
|
|
or private_stat.st_uid != EXTERNAL_DATA_PLANE_RUNTIME_UID
|
|
or private_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID
|
|
or stat.S_IMODE(private_stat.st_mode) != 0o400
|
|
or private_stat.st_size < 64
|
|
or private_stat.st_size > 1024):
|
|
die(f"Engine credential issuer private key is unsafe: {private_key}")
|
|
run_openssl(["pkey", "-in", str(private_key), "-noout"], "private key validation")
|
|
else:
|
|
private_tmp = private_key.with_name(f".{private_key.name}.{os.getpid()}.{time.time_ns()}.tmp")
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(str(private_tmp), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
run_openssl(["genpkey", "-algorithm", "ED25519", "-out", str(private_tmp)], "Ed25519 key generation")
|
|
os.chown(private_tmp, EXTERNAL_DATA_PLANE_RUNTIME_UID, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
private_tmp.chmod(0o400)
|
|
fsync_file(private_tmp)
|
|
os.replace(private_tmp, private_key)
|
|
fsync_directory(private_key.parent)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if private_tmp.exists():
|
|
private_tmp.unlink()
|
|
|
|
derived_public = state_dir / f".{public_key.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(str(derived_public), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
run_openssl(["pkey", "-in", str(private_key), "-pubout", "-out", str(derived_public)], "public key derivation")
|
|
validate_ed25519_public_key_file(derived_public)
|
|
if public_exists:
|
|
public_stat = public_key.lstat()
|
|
if (stat.S_ISLNK(public_stat.st_mode)
|
|
or not stat.S_ISREG(public_stat.st_mode)
|
|
or public_stat.st_uid != 0
|
|
or public_stat.st_gid != 0
|
|
or stat.S_IMODE(public_stat.st_mode) != 0o444
|
|
or public_stat.st_size < 64
|
|
or public_stat.st_size > 1024):
|
|
die(f"Engine credential issuer public key is unsafe: {public_key}")
|
|
validate_ed25519_public_key_file(public_key)
|
|
if derived_public.read_bytes() != public_key.read_bytes():
|
|
die("Engine credential issuer keypair mismatch")
|
|
else:
|
|
os.chown(derived_public, 0, 0)
|
|
derived_public.chmod(0o444)
|
|
fsync_file(derived_public)
|
|
os.replace(derived_public, public_key)
|
|
fsync_directory(state_dir)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if derived_public.exists():
|
|
derived_public.unlink()
|
|
return "reused" if private_exists else "created"
|
|
def ensure_engine_edp_managed_provisioner_keypair():
|
|
# Engine is the sole holder of the signing key. EDP receives only the
|
|
# matching public key from a separate trust directory. Neither file is
|
|
# copied into an artifact, shared .env, n8n container, or L2 graph.
|
|
openssl = shutil.which("openssl")
|
|
if not openssl:
|
|
die("openssl is required to manage the Engine EDP signing key")
|
|
|
|
try:
|
|
secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False)
|
|
secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
if stat.S_ISLNK(secret_parent_stat.st_mode) or not stat.S_ISDIR(secret_parent_stat.st_mode):
|
|
die("Engine EDP signing key parent directory is unsafe")
|
|
os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
MAP_GATEWAY_SECRET_DIR.chmod(0o710)
|
|
|
|
try:
|
|
private_dir_stat = ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR.mkdir(parents=False, exist_ok=False)
|
|
private_dir_stat = ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat()
|
|
if stat.S_ISLNK(private_dir_stat.st_mode) or not stat.S_ISDIR(private_dir_stat.st_mode):
|
|
die("Engine EDP signing key directory is unsafe")
|
|
os.chown(ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR, 0, 0)
|
|
ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR.chmod(0o700)
|
|
|
|
trust_parent = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.parent
|
|
try:
|
|
trust_parent_stat = trust_parent.lstat()
|
|
except FileNotFoundError:
|
|
trust_parent.mkdir(parents=True, exist_ok=False)
|
|
trust_parent_stat = trust_parent.lstat()
|
|
if stat.S_ISLNK(trust_parent_stat.st_mode) or not stat.S_ISDIR(trust_parent_stat.st_mode):
|
|
die("Engine EDP trust parent directory is unsafe")
|
|
os.chown(trust_parent, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
trust_parent.chmod(0o710)
|
|
|
|
try:
|
|
trust_dir_stat = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat()
|
|
except FileNotFoundError:
|
|
ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.mkdir(parents=False, exist_ok=False)
|
|
trust_dir_stat = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat()
|
|
if stat.S_ISLNK(trust_dir_stat.st_mode) or not stat.S_ISDIR(trust_dir_stat.st_mode):
|
|
die("Engine EDP trust directory is unsafe")
|
|
os.chown(ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.chmod(0o550)
|
|
|
|
try:
|
|
private_stat = ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat()
|
|
except FileNotFoundError:
|
|
private_stat = None
|
|
try:
|
|
public_stat = ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.lstat()
|
|
except FileNotFoundError:
|
|
public_stat = None
|
|
# A public-only state is ambiguous: replacing its missing private half
|
|
# would silently rotate trust. Fail before creating anything so recovery is
|
|
# an explicit operator decision. Private-only is recoverable by derivation.
|
|
if private_stat is None and public_stat is not None:
|
|
die("Engine EDP public key exists without its private key")
|
|
|
|
private_created = False
|
|
if private_stat is None:
|
|
temporary_private = (
|
|
ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR
|
|
/ f".private-key.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
try:
|
|
subprocess.run(
|
|
[openssl, "genpkey", "-algorithm", "ED25519", "-out", str(temporary_private)],
|
|
check=True,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
temporary_stat = temporary_private.lstat()
|
|
if (stat.S_ISLNK(temporary_stat.st_mode) or not stat.S_ISREG(temporary_stat.st_mode)
|
|
or temporary_stat.st_size < 80 or temporary_stat.st_size > 8192):
|
|
die("generated Engine EDP private key is invalid")
|
|
os.chown(temporary_private, 0, 0)
|
|
temporary_private.chmod(0o400)
|
|
with temporary_private.open("rb") as handle:
|
|
os.fsync(handle.fileno())
|
|
os.replace(temporary_private, ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE)
|
|
fsync_directory(ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR)
|
|
private_created = True
|
|
except (OSError, subprocess.CalledProcessError) as error:
|
|
die(f"failed to generate Engine EDP signing key: {type(error).__name__}")
|
|
finally:
|
|
if temporary_private.exists():
|
|
temporary_private.unlink()
|
|
private_stat = ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat()
|
|
|
|
if (stat.S_ISLNK(private_stat.st_mode) or not stat.S_ISREG(private_stat.st_mode)
|
|
or private_stat.st_uid != 0 or private_stat.st_gid != 0
|
|
or stat.S_IMODE(private_stat.st_mode) != 0o400
|
|
or private_stat.st_size < 80 or private_stat.st_size > 8192):
|
|
die("Engine EDP private key has unsafe ownership, mode, or size")
|
|
|
|
try:
|
|
derived = subprocess.run(
|
|
[openssl, "pkey", "-in", str(ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), "-pubout"],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
).stdout
|
|
except (OSError, subprocess.CalledProcessError) as error:
|
|
die(f"Engine EDP private key validation failed: {type(error).__name__}")
|
|
if (not derived.startswith(b"-----BEGIN PUBLIC KEY-----\n")
|
|
or not derived.rstrip().endswith(b"-----END PUBLIC KEY-----")
|
|
or len(derived) > 8192):
|
|
die("derived Engine EDP public key is invalid")
|
|
try:
|
|
derived_der = subprocess.run(
|
|
[
|
|
openssl,
|
|
"pkey",
|
|
"-in",
|
|
str(ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE),
|
|
"-pubout",
|
|
"-outform",
|
|
"DER",
|
|
],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
).stdout
|
|
except (OSError, subprocess.CalledProcessError) as error:
|
|
die(f"Engine EDP private key type validation failed: {type(error).__name__}")
|
|
# RFC 8410 Ed25519 SubjectPublicKeyInfo is exactly 44 bytes and carries the
|
|
# OID 1.3.101.112. Reject RSA/EC keys before either runtime can mount them.
|
|
if (len(derived_der) != 44
|
|
or not derived_der.startswith(bytes.fromhex("302a300506032b6570032100"))):
|
|
die("Engine EDP signing key must be Ed25519")
|
|
|
|
if public_stat is not None:
|
|
if (stat.S_ISLNK(public_stat.st_mode) or not stat.S_ISREG(public_stat.st_mode)
|
|
or public_stat.st_uid != 0
|
|
or public_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID
|
|
or stat.S_IMODE(public_stat.st_mode) != 0o440
|
|
or public_stat.st_size < 80 or public_stat.st_size > 8192):
|
|
die("Engine EDP public key has unsafe ownership, mode, or size")
|
|
installed = ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.read_bytes()
|
|
if installed != derived:
|
|
die("Engine EDP public key does not match the installed private key")
|
|
return "created" if private_created else "reused"
|
|
|
|
temporary_public = (
|
|
ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR
|
|
/ f".public-key.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(str(temporary_public), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o440)
|
|
os.write(descriptor, derived)
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
os.fchmod(descriptor, 0o440)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary_public, ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE)
|
|
fsync_directory(ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary_public.exists():
|
|
temporary_public.unlink()
|
|
return "created"
|
|
return "created"
|
|
|
|
|
|
def ensure_foundry_edp_managed_provisioner_keypair():
|
|
# Foundry receives its own signing identity. It must never reuse or read
|
|
# the Engine private key; EDP trusts the matching public key separately.
|
|
openssl = shutil.which("openssl")
|
|
if not openssl:
|
|
die("openssl is required to manage the Foundry EDP signing key")
|
|
|
|
try:
|
|
secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False)
|
|
secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
|
|
if stat.S_ISLNK(secret_parent_stat.st_mode) or not stat.S_ISDIR(secret_parent_stat.st_mode):
|
|
die("Foundry EDP signing key parent directory is unsafe")
|
|
os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID)
|
|
MAP_GATEWAY_SECRET_DIR.chmod(0o710)
|
|
|
|
try:
|
|
private_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.mkdir(parents=False, exist_ok=False)
|
|
private_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat()
|
|
if stat.S_ISLNK(private_dir_stat.st_mode) or not stat.S_ISDIR(private_dir_stat.st_mode):
|
|
die("Foundry EDP signing key directory is unsafe")
|
|
os.chown(FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR, 0, 0)
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.chmod(0o700)
|
|
|
|
trust_parent = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.parent
|
|
try:
|
|
trust_parent_stat = trust_parent.lstat()
|
|
except FileNotFoundError:
|
|
trust_parent.mkdir(parents=True, exist_ok=False)
|
|
trust_parent_stat = trust_parent.lstat()
|
|
if stat.S_ISLNK(trust_parent_stat.st_mode) or not stat.S_ISDIR(trust_parent_stat.st_mode):
|
|
die("Foundry EDP trust parent directory is unsafe")
|
|
os.chown(trust_parent, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
trust_parent.chmod(0o710)
|
|
|
|
try:
|
|
trust_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat()
|
|
except FileNotFoundError:
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.mkdir(parents=False, exist_ok=False)
|
|
trust_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat()
|
|
if stat.S_ISLNK(trust_dir_stat.st_mode) or not stat.S_ISDIR(trust_dir_stat.st_mode):
|
|
die("Foundry EDP trust directory is unsafe")
|
|
os.chown(FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.chmod(0o550)
|
|
|
|
try:
|
|
private_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat()
|
|
except FileNotFoundError:
|
|
private_stat = None
|
|
try:
|
|
public_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.lstat()
|
|
except FileNotFoundError:
|
|
public_stat = None
|
|
if private_stat is None and public_stat is not None:
|
|
die("Foundry EDP public key exists without its private key")
|
|
|
|
private_created = False
|
|
if private_stat is None:
|
|
temporary_private = (
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR
|
|
/ f".private-key.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
try:
|
|
subprocess.run(
|
|
[openssl, "genpkey", "-algorithm", "ED25519", "-out", str(temporary_private)],
|
|
check=True,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
temporary_stat = temporary_private.lstat()
|
|
if (stat.S_ISLNK(temporary_stat.st_mode) or not stat.S_ISREG(temporary_stat.st_mode)
|
|
or temporary_stat.st_size < 80 or temporary_stat.st_size > 8192):
|
|
die("generated Foundry EDP private key is invalid")
|
|
os.chown(temporary_private, 0, 0)
|
|
temporary_private.chmod(0o400)
|
|
with temporary_private.open("rb") as handle:
|
|
os.fsync(handle.fileno())
|
|
os.replace(temporary_private, FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE)
|
|
fsync_directory(FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR)
|
|
private_created = True
|
|
except (OSError, subprocess.CalledProcessError) as error:
|
|
die(f"failed to generate Foundry EDP signing key: {type(error).__name__}")
|
|
finally:
|
|
if temporary_private.exists():
|
|
temporary_private.unlink()
|
|
private_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat()
|
|
|
|
if (stat.S_ISLNK(private_stat.st_mode) or not stat.S_ISREG(private_stat.st_mode)
|
|
or private_stat.st_uid != 0 or private_stat.st_gid != 0
|
|
or stat.S_IMODE(private_stat.st_mode) != 0o400
|
|
or private_stat.st_size < 80 or private_stat.st_size > 8192):
|
|
die("Foundry EDP private key has unsafe ownership, mode, or size")
|
|
|
|
try:
|
|
derived = subprocess.run(
|
|
[openssl, "pkey", "-in", str(FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), "-pubout"],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
).stdout
|
|
derived_der = subprocess.run(
|
|
[
|
|
openssl,
|
|
"pkey",
|
|
"-in",
|
|
str(FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE),
|
|
"-pubout",
|
|
"-outform",
|
|
"DER",
|
|
],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
).stdout
|
|
except (OSError, subprocess.CalledProcessError) as error:
|
|
die(f"Foundry EDP private key validation failed: {type(error).__name__}")
|
|
if (not derived.startswith(b"-----BEGIN PUBLIC KEY-----\n")
|
|
or not derived.rstrip().endswith(b"-----END PUBLIC KEY-----")
|
|
or len(derived) > 8192
|
|
or len(derived_der) != 44
|
|
or not derived_der.startswith(bytes.fromhex("302a300506032b6570032100"))):
|
|
die("Foundry EDP signing key must be Ed25519")
|
|
|
|
if public_stat is not None:
|
|
if (stat.S_ISLNK(public_stat.st_mode) or not stat.S_ISREG(public_stat.st_mode)
|
|
or public_stat.st_uid != 0
|
|
or public_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID
|
|
or stat.S_IMODE(public_stat.st_mode) != 0o440
|
|
or public_stat.st_size < 80 or public_stat.st_size > 8192):
|
|
die("Foundry EDP public key has unsafe ownership, mode, or size")
|
|
if FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.read_bytes() != derived:
|
|
die("Foundry EDP public key does not match the installed private key")
|
|
return "created" if private_created else "reused"
|
|
|
|
temporary_public = (
|
|
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR
|
|
/ f".public-key.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(str(temporary_public), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o440)
|
|
os.write(descriptor, derived)
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
|
|
os.fchmod(descriptor, 0o440)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary_public, FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE)
|
|
fsync_directory(FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary_public.exists():
|
|
temporary_public.unlink()
|
|
return "created"
|
|
|
|
|
|
def ensure_engine_data_product_grant_private_state(include_reader=False):
|
|
# Compose must never create this bind source on our behalf: Docker's normal
|
|
# 0755 directory would either expose control-plane metadata or be rejected
|
|
# by the Engine's fail-closed PublishGrantStore.
|
|
engine_root = component_root("engine")
|
|
try:
|
|
engine_root_stat = engine_root.lstat()
|
|
except FileNotFoundError:
|
|
die("Engine root is missing before private state preparation")
|
|
if stat.S_ISLNK(engine_root_stat.st_mode) or not stat.S_ISDIR(engine_root_stat.st_mode):
|
|
die("Engine root is unsafe before private state preparation")
|
|
|
|
state_paths = [
|
|
(engine_root / ENGINE_CONTROL_PLANE_STATE_REL, "Engine control-plane state"),
|
|
(engine_root / ENGINE_PUBLISH_GRANT_STATE_REL, "Engine Publish grant state"),
|
|
]
|
|
if include_reader:
|
|
state_paths.append((
|
|
engine_root / ENGINE_READ_GRANT_STATE_REL,
|
|
"Engine Read grant state",
|
|
))
|
|
for state_path, label in state_paths:
|
|
try:
|
|
state_stat = state_path.lstat()
|
|
except FileNotFoundError:
|
|
state_path.mkdir(parents=False, exist_ok=False, mode=0o700)
|
|
state_stat = state_path.lstat()
|
|
if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISDIR(state_stat.st_mode):
|
|
die(f"{label} directory is unsafe: {state_path}")
|
|
os.chown(state_path, 0, 0)
|
|
state_path.chmod(0o700)
|
|
state_stat = state_path.lstat()
|
|
if (state_stat.st_uid != 0 or state_stat.st_gid != 0
|
|
or stat.S_IMODE(state_stat.st_mode) != 0o700):
|
|
die(f"{label} ownership or mode is unsafe: {state_path}")
|
|
|
|
|
|
def ensure_engine_publish_grant_private_state():
|
|
# Frozen compatibility seam for the established Publish transition.
|
|
return ensure_engine_data_product_grant_private_state(include_reader=False)
|
|
|
|
|
|
def fsync_directory(path):
|
|
descriptor = os.open(str(path), os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def require_root():
|
|
if os.geteuid() != 0:
|
|
die("run with sudo")
|
|
|
|
|
|
def verify_file_mode(path, require_root_owner):
|
|
try:
|
|
st = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"missing path: {path}")
|
|
|
|
if stat.S_ISLNK(st.st_mode):
|
|
die(f"path must not be a symlink: {path}")
|
|
|
|
if require_root_owner and (st.st_uid != 0 or st.st_gid != 0):
|
|
die(f"path must be root:root: {path}")
|
|
|
|
if st.st_mode & stat.S_IWGRP:
|
|
die(f"path must not be group-writable: {path}")
|
|
|
|
if st.st_mode & stat.S_IWOTH:
|
|
die(f"path must not be world-writable: {path}")
|
|
|
|
|
|
def verify_install():
|
|
invoked = Path(sys.argv[0]).absolute()
|
|
if invoked != EXPECTED_SELF:
|
|
die(f"runner must be invoked as {EXPECTED_SELF}, current: {invoked}")
|
|
|
|
verify_file_mode(EXPECTED_SELF, require_root_owner=True)
|
|
verify_file_mode(Path("/usr/local"), require_root_owner=True)
|
|
verify_file_mode(Path("/usr/local/sbin"), require_root_owner=True)
|
|
|
|
print(f"path={EXPECTED_SELF}")
|
|
print(f"sha256={sha256_file(EXPECTED_SELF)}")
|
|
print(f"python={sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")
|
|
print("verify-install-ok")
|
|
|
|
|
|
def validate_artifact_location(path):
|
|
if path.suffix != ".tgz":
|
|
die("artifact must have .tgz extension")
|
|
|
|
try:
|
|
st = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"artifact not found: {path}")
|
|
|
|
if stat.S_ISLNK(st.st_mode):
|
|
die(f"artifact must not be a symlink: {path}")
|
|
|
|
if not stat.S_ISREG(st.st_mode):
|
|
die(f"artifact must be a regular file: {path}")
|
|
|
|
if st.st_size > MAX_ARTIFACT_BYTES:
|
|
die(f"artifact too large: {st.st_size} bytes")
|
|
|
|
inbox_real = INBOX.resolve()
|
|
parent_real = path.parent.resolve()
|
|
if parent_real != inbox_real:
|
|
die(f"artifact must be inside {INBOX}")
|
|
|
|
name = path.name
|
|
if "/" in name or ".." in name or name.startswith("."):
|
|
die(f"unsafe artifact filename: {name}")
|
|
|
|
|
|
def validate_posix_path(value):
|
|
if not value:
|
|
die("empty path")
|
|
if "\\" in value:
|
|
die(f"backslash is not allowed in path: {value}")
|
|
pure = PurePosixPath(value)
|
|
if pure.is_absolute():
|
|
die(f"absolute path rejected: {value}")
|
|
if any(part in ("", ".", "..") for part in pure.parts):
|
|
die(f"path escape rejected: {value}")
|
|
if any(ord(ch) < 32 for ch in value):
|
|
die(f"control character rejected in path: {value!r}")
|
|
return pure
|
|
|
|
|
|
def tar_name_to_path(work_dir, name):
|
|
pure = validate_posix_path(name)
|
|
target = work_dir.joinpath(*pure.parts)
|
|
resolved = target.resolve(strict=False)
|
|
if not is_relative_to(resolved, work_dir.resolve()):
|
|
die(f"tar path escaped work dir: {name}")
|
|
return target
|
|
|
|
|
|
def validate_tar_member(member):
|
|
name = member.name
|
|
validate_posix_path(name)
|
|
|
|
if name not in ("manifest.env", "files.txt", "payload") and not name.startswith("payload/"):
|
|
die(f"unexpected tar member: {name}")
|
|
|
|
if not (member.isfile() or member.isdir()):
|
|
die(f"unsupported tar member type: {name}")
|
|
|
|
if member.mode & stat.S_ISUID or member.mode & stat.S_ISGID:
|
|
die(f"setuid/setgid tar member rejected: {name}")
|
|
|
|
if member.isfile() and member.size > MAX_FILE_BYTES:
|
|
die(f"file too large in artifact: {name}")
|
|
|
|
|
|
def scan_tar(artifact):
|
|
member_count = 0
|
|
payload_bytes = 0
|
|
names = set()
|
|
|
|
with tarfile.open(artifact, "r:gz") as tar:
|
|
for member in tar:
|
|
member_count += 1
|
|
if member_count > MAX_MEMBER_COUNT:
|
|
die("too many files in artifact")
|
|
|
|
validate_tar_member(member)
|
|
if member.name in names:
|
|
die(f"duplicate tar member rejected: {member.name}")
|
|
names.add(member.name)
|
|
|
|
if member.isfile() and member.name.startswith("payload/"):
|
|
payload_bytes += member.size
|
|
if payload_bytes > MAX_PAYLOAD_BYTES:
|
|
die("payload is too large")
|
|
|
|
if "manifest.env" not in names:
|
|
die("manifest.env missing")
|
|
if "files.txt" not in names:
|
|
die("files.txt missing")
|
|
if not any(name == "payload" or name.startswith("payload/") for name in names):
|
|
die("payload missing")
|
|
|
|
|
|
def safe_extract(artifact, work_dir):
|
|
scan_tar(artifact)
|
|
|
|
with tarfile.open(artifact, "r:gz") as tar:
|
|
for member in tar:
|
|
validate_tar_member(member)
|
|
target = tar_name_to_path(work_dir, member.name)
|
|
|
|
if member.isdir():
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
target.chmod(0o755)
|
|
continue
|
|
|
|
if target.exists() and target.is_dir():
|
|
die(f"file target already exists as directory: {member.name}")
|
|
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
source = tar.extractfile(member)
|
|
if source is None:
|
|
die(f"cannot read tar member: {member.name}")
|
|
|
|
tmp = target.with_name(f"{target.name}.extracting")
|
|
with source, tmp.open("wb") as out:
|
|
shutil.copyfileobj(source, out, 1024 * 1024)
|
|
tmp.chmod(0o644)
|
|
os.replace(tmp, target)
|
|
|
|
|
|
def parse_manifest(path):
|
|
data = {}
|
|
with path.open("r", encoding="utf-8") as f:
|
|
for lineno, line in enumerate(f, 1):
|
|
line = line.rstrip("\n")
|
|
if not line:
|
|
continue
|
|
if "=" not in line:
|
|
die(f"manifest line {lineno} must be key=value")
|
|
key, value = line.split("=", 1)
|
|
if key not in MANIFEST_KEYS:
|
|
die(f"unsupported manifest key: {key}")
|
|
if key in data:
|
|
die(f"duplicate manifest key: {key}")
|
|
if any(ord(ch) < 32 for ch in value):
|
|
die(f"control character in manifest value: {key}")
|
|
data[key] = value
|
|
|
|
missing = sorted(MANIFEST_KEYS - set(data))
|
|
if missing:
|
|
die(f"manifest keys missing: {', '.join(missing)}")
|
|
|
|
if not PATCH_ID_RE.match(data["id"]):
|
|
die("manifest id contains unsafe characters")
|
|
|
|
if data["type"] != "app-overlay":
|
|
die(f"unsupported artifact type: {data['type']}")
|
|
|
|
if data["component"] not in COMPONENTS:
|
|
die(f"unsupported component: {data['component']}")
|
|
|
|
return data
|
|
|
|
|
|
def parse_files_list(path):
|
|
entries = []
|
|
seen = set()
|
|
with path.open("r", encoding="utf-8") as f:
|
|
for lineno, raw in enumerate(f, 1):
|
|
rel = raw.rstrip("\n")
|
|
if not rel:
|
|
continue
|
|
if rel.strip() != rel:
|
|
die(f"files.txt line {lineno} has leading/trailing whitespace")
|
|
validate_posix_path(rel)
|
|
if rel in seen:
|
|
die(f"duplicate files.txt entry: {rel}")
|
|
seen.add(rel)
|
|
entries.append(rel)
|
|
|
|
if not entries:
|
|
die("files.txt is empty")
|
|
|
|
return entries
|
|
|
|
|
|
def denied_payload_path(component, rel):
|
|
lower = rel.lower()
|
|
parts = lower.split("/")
|
|
base = parts[-1]
|
|
|
|
if component == "engine" and rel in (
|
|
"docker-compose.yml",
|
|
"nodedc-source/services/n8n/private-extensions/n8n-nodes-ndc/Dockerfile",
|
|
):
|
|
pass
|
|
elif component == "platform" and rel in (
|
|
"platform/docker-compose.platform-http.yml",
|
|
"platform/notification-core/Dockerfile",
|
|
"platform/ai-workspace-hub/Dockerfile",
|
|
"platform/ai-workspace-assistant/Dockerfile",
|
|
"platform/ontology-core/Dockerfile",
|
|
"platform/gelios-gateway/Dockerfile",
|
|
"platform/services/map-gateway/Dockerfile",
|
|
"platform/services/external-data-plane/Dockerfile",
|
|
):
|
|
pass
|
|
elif component == "bim-viewer" and rel in (
|
|
"converter/Dockerfile",
|
|
):
|
|
pass
|
|
elif component == "dc-cms" and rel in (
|
|
"Dockerfile",
|
|
"infra/docker-compose.yml",
|
|
):
|
|
pass
|
|
elif component == "module-foundry" and rel in (
|
|
"Dockerfile",
|
|
"infra/docker-compose.module-foundry.yml",
|
|
):
|
|
pass
|
|
elif component == "device-plane" and rel in (
|
|
"docker-compose.device-plane.yml",
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
|
"services/device-control-core/Dockerfile",
|
|
"services/device-gateway/Dockerfile",
|
|
"services/device-manager/Dockerfile",
|
|
"services/device-backhaul-target/Dockerfile",
|
|
):
|
|
pass
|
|
elif component == "gitea" and rel == GITEA_COMPOSE_REL:
|
|
pass
|
|
elif component == "proxy-contur" and rel in (
|
|
"Dockerfile",
|
|
"docker-compose.yml",
|
|
):
|
|
pass
|
|
elif component == "dc-amd-proxy" and rel in (
|
|
"Dockerfile",
|
|
"docker-compose.yml",
|
|
):
|
|
pass
|
|
elif base in (".env", "dockerfile", "docker-compose.yml", "compose.yml"):
|
|
return "sensitive or infrastructure filename"
|
|
|
|
if base.endswith(".env") or base.endswith((".pem", ".crt", ".key", ".p12", ".pfx")):
|
|
return "secret-like file extension"
|
|
|
|
if base.endswith((".sh", ".bash", ".zsh")):
|
|
return "shell files are not allowed in app-overlay artifacts"
|
|
|
|
if base.endswith(".zip"):
|
|
return "zip backup files are not deployable artifacts"
|
|
|
|
if any(part in (".git", "node_modules") for part in parts):
|
|
return "repository/build dependency directory"
|
|
|
|
# `packages/tokens` is a versioned UI design-token package, not a secret
|
|
# store. Keep this narrowly scoped exception so the general filename guard
|
|
# remains in force for every other Module Foundry payload path.
|
|
if not (component == "module-foundry" and lower.startswith("packages/tokens/")) and any(
|
|
token in lower for token in ("secret", "token", "password")
|
|
):
|
|
return "secret-like path"
|
|
|
|
runtime_prefixes = ()
|
|
if component == "engine":
|
|
runtime_prefixes = (
|
|
"nodedc-source/server/data",
|
|
"nodedc-source/server/storage",
|
|
"nodedc-source/server/logs",
|
|
"nodedc-source/public/storage",
|
|
"nodedc-source/dist/storage",
|
|
)
|
|
if rel == "nodedc-source/server/.env":
|
|
return "server env file"
|
|
elif component == "launcher":
|
|
runtime_prefixes = (
|
|
"server/data",
|
|
"server/storage",
|
|
"server/logs",
|
|
"public/storage",
|
|
"dist/storage",
|
|
)
|
|
if rel == "server/.env":
|
|
return "server env file"
|
|
elif component == "platform":
|
|
runtime_prefixes = (
|
|
"backups",
|
|
"launcher",
|
|
"ops-agents",
|
|
"tasker",
|
|
"platform/.env.synology",
|
|
"platform/authentik",
|
|
"platform/docs",
|
|
"authentik/data",
|
|
"authentik/certs",
|
|
"authentik/media",
|
|
"authentik/postgresql",
|
|
)
|
|
if rel.startswith("platform/.env"):
|
|
return "platform env file"
|
|
if rel.startswith(("platform/Caddyfile.http.bak", "platform/docker-compose.platform-http.yml.bak")):
|
|
return "platform backup file"
|
|
elif component == "tasker":
|
|
runtime_prefixes = (
|
|
"plane-app/.local-web-root",
|
|
"plane-app/archive",
|
|
"plane-app/backup",
|
|
"plane-app/plane.env",
|
|
"plane-app/plane.env.example",
|
|
"plane-app/plane.env.staging.example",
|
|
"plane-app/plane.env.synology",
|
|
"plane-app/plane.env.synology.base",
|
|
"plane-src/.cache",
|
|
"plane-src/.next",
|
|
"plane-src/.pnpm-store",
|
|
"plane-src/.react-router",
|
|
"plane-src/.turbo",
|
|
"plane-src/build",
|
|
"plane-src/coverage",
|
|
"plane-src/dist",
|
|
"plane-src/playwright-report",
|
|
"plane-src/test-results",
|
|
)
|
|
if rel.startswith(("plane-app/docker-compose.yaml.bak", "plane-app/docker-compose.synology.override.yml.bak")):
|
|
return "tasker backup file"
|
|
elif component == "ops-agents":
|
|
runtime_prefixes = (
|
|
"dist",
|
|
)
|
|
if rel.startswith(".env"):
|
|
return "ops-agents env file"
|
|
elif component == "bim-viewer":
|
|
runtime_prefixes = (
|
|
"dist",
|
|
"frontend/dist",
|
|
"server/data",
|
|
"server/logs",
|
|
"server/storage",
|
|
"server/tmp",
|
|
)
|
|
if rel == ".env" or rel.startswith(".env."):
|
|
if rel != ".env.synology.example":
|
|
return "bim-viewer env file"
|
|
if rel.startswith(("docker-compose.beam.yml.bak",)):
|
|
return "bim-viewer backup file"
|
|
elif component == "module-foundry":
|
|
runtime_prefixes = (
|
|
"runtime-data",
|
|
"node_modules",
|
|
"apps/catalog/dist",
|
|
"server/data",
|
|
"server/logs",
|
|
"server/storage",
|
|
)
|
|
if rel == ".env" or rel.startswith(".env."):
|
|
if rel != ".env.example":
|
|
return "module-foundry env file"
|
|
if rel.startswith(("Dockerfile.bak", "infra/docker-compose.module-foundry.yml.bak")):
|
|
return "module-foundry backup file"
|
|
elif component == "device-plane":
|
|
runtime_prefixes = (
|
|
"runtime",
|
|
"secrets",
|
|
"data",
|
|
"logs",
|
|
"backups",
|
|
"node_modules",
|
|
)
|
|
if rel == ".env" or rel.startswith(".env."):
|
|
return "device-plane env file"
|
|
if "test" in parts:
|
|
return "device-plane test path"
|
|
if rel.startswith((
|
|
"docker-compose.device-plane.yml.bak",
|
|
"docker-compose.device-manager.yml.bak",
|
|
"services/device-control-core/Dockerfile.bak",
|
|
"services/device-gateway/Dockerfile.bak",
|
|
"services/device-manager/Dockerfile.bak",
|
|
)):
|
|
return "device-plane backup file"
|
|
elif component == "gitea":
|
|
runtime_prefixes = (
|
|
"data",
|
|
"config",
|
|
"secrets",
|
|
"repositories",
|
|
"users",
|
|
"tokens",
|
|
"hooks",
|
|
"logs",
|
|
"backups",
|
|
)
|
|
if rel == ".env" or rel.startswith(".env."):
|
|
return "gitea env file"
|
|
if rel.startswith((
|
|
"docker-compose.gitea.yml.bak",
|
|
"deployment/gitea-fresh-install-v1.json.bak",
|
|
"deployment/gitea-incident-salvage-v1.json.bak",
|
|
"deployment/gitea-incident-salvage/confirmed-disposition-v1.json.bak",
|
|
"deployment/gitea-incident-salvage/confirmed-decision.json.bak",
|
|
"deployment/gitea-incident-salvage/users.decisions.csv.bak",
|
|
"deployment/gitea-incident-salvage/repositories.decisions.csv.bak",
|
|
)):
|
|
return "gitea backup file"
|
|
elif component == "n8n-private-extension":
|
|
# An extension release is inert data at this boundary. Activation is
|
|
# Engine-owned and must never be smuggled into the artifact as a script,
|
|
# Compose file, environment file, pointer or mount configuration.
|
|
extension_parts = PurePosixPath(rel).parts
|
|
is_release_directory = (
|
|
len(extension_parts) == 3
|
|
and extension_parts[0] == "releases"
|
|
and extension_parts[1] == "n8n-nodes-ndc"
|
|
and N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(extension_parts[2])
|
|
)
|
|
if not is_release_directory and base not in ("package.tgz", "release.json", "rollback.json"):
|
|
return "unexpected private extension release member"
|
|
elif component == "proxy-contur":
|
|
runtime_prefixes = (
|
|
"node_modules",
|
|
)
|
|
if rel == ".env" or rel.startswith(".env."):
|
|
return "proxy-contur env file"
|
|
if rel.startswith(("Dockerfile.bak", "docker-compose.yml.bak")):
|
|
return "proxy-contur backup file"
|
|
elif component == "dc-amd-proxy":
|
|
runtime_prefixes = (
|
|
"node_modules",
|
|
"runtime",
|
|
)
|
|
if rel == ".env" or rel.startswith(".env."):
|
|
return "dc-amd-proxy env file"
|
|
if rel.startswith(("Dockerfile.bak", "docker-compose.yml.bak")):
|
|
return "dc-amd-proxy backup file"
|
|
elif component == "dc-cms":
|
|
runtime_prefixes = (
|
|
"admin/uploads",
|
|
"infra/authentik/certs",
|
|
"infra/authentik/data",
|
|
"infra/authentik/media",
|
|
"infra/authentik/postgresql",
|
|
"server/data",
|
|
"server/logs",
|
|
"server/storage",
|
|
"sites",
|
|
)
|
|
if rel == ".env" or rel.startswith(".env."):
|
|
return "dc-cms root env file"
|
|
if rel in ("infra/.env", "infra/.env.synology"):
|
|
return "dc-cms env file"
|
|
if rel.startswith("infra/.env.") and rel not in (
|
|
"infra/.env.example",
|
|
"infra/.env.synology.example",
|
|
):
|
|
return "dc-cms env file"
|
|
if rel.startswith(("Dockerfile.bak", "infra/docker-compose.yml.bak")):
|
|
return "dc-cms backup file"
|
|
elif component == "dc-cms-site-nodedc":
|
|
runtime_prefixes = (
|
|
"assets/.trash",
|
|
"assets/uploads/.trash",
|
|
"node_modules",
|
|
)
|
|
|
|
if any(rel == prefix or rel.startswith(prefix + "/") for prefix in runtime_prefixes):
|
|
return "runtime data path"
|
|
|
|
return None
|
|
|
|
|
|
def allowed_payload_path(component, rel):
|
|
validate_posix_path(rel)
|
|
reason = denied_payload_path(component, rel)
|
|
if reason:
|
|
die(f"path rejected: {rel}: {reason}")
|
|
|
|
if component == "mission-core-map-access":
|
|
if rel == "access.json":
|
|
return True
|
|
die("Mission Core Map Access accepts only access.json")
|
|
|
|
if component == "engine":
|
|
if rel in ("nodedc-source/package.json", "nodedc-source/package-lock.json"):
|
|
return True
|
|
if rel == "docker-compose.yml":
|
|
return True
|
|
if rel.startswith((
|
|
"nodedc-source/server/",
|
|
"nodedc-source/src/",
|
|
"nodedc-source/dist/",
|
|
"nodedc-source/public/",
|
|
"nodedc-source/workers/ai-workspace-bridge/",
|
|
)):
|
|
return True
|
|
if rel == "nodedc-source/services/n8n/private-extensions" or rel.startswith(
|
|
"nodedc-source/services/n8n/private-extensions/"
|
|
):
|
|
return True
|
|
if rel == ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL:
|
|
return True
|
|
if rel == ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL:
|
|
return True
|
|
if rel == ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL:
|
|
return True
|
|
if rel == ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL or rel.startswith(
|
|
ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL + "/"
|
|
):
|
|
return True
|
|
if rel == "nodedc-source/dist":
|
|
return True
|
|
|
|
if component == "launcher":
|
|
if rel in (
|
|
"index.html",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"tsconfig.json",
|
|
"tsconfig.app.json",
|
|
"tsconfig.node.json",
|
|
"vite.config.ts",
|
|
):
|
|
return True
|
|
if rel.startswith(("server/", "src/", "public/", "scripts/", "dc-ui-guideline/")):
|
|
return True
|
|
|
|
if component == "platform":
|
|
if rel in (
|
|
"platform/Caddyfile.http",
|
|
"platform/docker-compose.platform-http.yml",
|
|
"platform/docker-compose.external-data-plane.yml",
|
|
PLATFORM_DEVICE_CORE_HUB_TRUST_REL,
|
|
PLATFORM_DEVICE_MANAGER_PUBLIC_ROUTE_REL,
|
|
):
|
|
return True
|
|
if rel == "platform/notification-core" or rel.startswith("platform/notification-core/"):
|
|
return True
|
|
if rel == "platform/ai-workspace-hub" or rel.startswith("platform/ai-workspace-hub/"):
|
|
return True
|
|
if rel == "platform/ai-workspace-assistant" or rel.startswith("platform/ai-workspace-assistant/"):
|
|
return True
|
|
if rel == "platform/ontology-core" or rel.startswith("platform/ontology-core/"):
|
|
return True
|
|
if rel == "platform/gelios-gateway" or rel.startswith("platform/gelios-gateway/"):
|
|
return True
|
|
if rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/"):
|
|
return True
|
|
if rel == "platform/services/external-data-plane" or rel.startswith("platform/services/external-data-plane/"):
|
|
return True
|
|
if rel == "platform/packages/external-provider-contract" or rel.startswith("platform/packages/external-provider-contract/"):
|
|
return True
|
|
if rel == "authentik/custom-templates" or rel.startswith("authentik/custom-templates/"):
|
|
return True
|
|
|
|
if component == "tasker":
|
|
if rel in (
|
|
"plane-app/docker-compose.yaml",
|
|
"plane-app/docker-compose.synology.override.yml",
|
|
):
|
|
return True
|
|
if rel.startswith((
|
|
"plane-src/apps/api/",
|
|
"plane-src/apps/web/",
|
|
"plane-src/packages/",
|
|
)):
|
|
return True
|
|
|
|
if component == "ops-agents":
|
|
if rel in (
|
|
".dockerignore",
|
|
".env.synology.example",
|
|
".gitignore",
|
|
"Dockerfile",
|
|
"README.md",
|
|
"docker-compose.local.yml",
|
|
"docker-compose.synology.yml",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"tsconfig.json",
|
|
):
|
|
return True
|
|
if rel.startswith((
|
|
"docs/",
|
|
"migrations/",
|
|
"src/",
|
|
)):
|
|
return True
|
|
|
|
if component == "bim-viewer":
|
|
if rel in (
|
|
"converter",
|
|
"embeddedDemos",
|
|
"frontend",
|
|
"images",
|
|
"locales",
|
|
"processor",
|
|
"report",
|
|
"src",
|
|
):
|
|
return True
|
|
if rel in (
|
|
".env.synology.example",
|
|
".esdoc.json",
|
|
".gitattributes",
|
|
".gitignore",
|
|
".release-please-manifest.json",
|
|
"CHANGELOG.md",
|
|
"CODE_OF_CONDUCT.md",
|
|
"LICENSE",
|
|
"README.md",
|
|
"SECURITY.md",
|
|
"_config.yml",
|
|
"changelog-template.hbs",
|
|
"docker-compose.beam.yml",
|
|
"embeddedViewer.html",
|
|
"index.js",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"release-please-config.json",
|
|
"rollup.config.js",
|
|
"rollup.dev.config.js",
|
|
"webComponentExample.html",
|
|
"xeokit-bim-viewer.css",
|
|
):
|
|
return True
|
|
if rel.startswith((
|
|
"converter/",
|
|
"embeddedDemos/",
|
|
"frontend/",
|
|
"images/",
|
|
"locales/",
|
|
"processor/",
|
|
"report/",
|
|
"server/",
|
|
"src/",
|
|
)):
|
|
return True
|
|
|
|
if component == "module-foundry":
|
|
if rel in (
|
|
".dockerignore",
|
|
".env.example",
|
|
".gitignore",
|
|
"Dockerfile",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"tsconfig.base.json",
|
|
"infra/docker-compose.module-foundry.yml",
|
|
):
|
|
return True
|
|
if rel in ("apps", "packages", "registry", "runtime-seed", "scripts", "server"):
|
|
return True
|
|
if rel.startswith((
|
|
"apps/",
|
|
"packages/",
|
|
"registry/",
|
|
"runtime-seed/",
|
|
"scripts/",
|
|
"server/",
|
|
)):
|
|
return True
|
|
|
|
if component == "device-plane":
|
|
if rel in (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"docker-compose.device-plane.yml",
|
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL,
|
|
DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
|
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_REL,
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_REL,
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL,
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_REL,
|
|
DEVICE_PLANE_BACKHAUL_TARGET_REL,
|
|
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL,
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_REL,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL,
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_REL,
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
|
|
"packages/device-protocol-contract",
|
|
"packages/device-edge-channel-contract",
|
|
"packages/infrastructure-telemetry-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
"services/device-control-core",
|
|
"services/device-gateway",
|
|
"services/device-edge-relay/package.json",
|
|
"services/device-manager",
|
|
"services/device-backhaul-target",
|
|
):
|
|
return True
|
|
if rel.startswith((
|
|
"packages/device-protocol-contract/",
|
|
"packages/device-edge-channel-contract/",
|
|
"packages/infrastructure-telemetry-contract/",
|
|
"packages/arusnavi-b2-adapter/",
|
|
"services/device-control-core/",
|
|
"services/device-gateway/",
|
|
"services/device-manager/",
|
|
"services/device-backhaul-target/",
|
|
)):
|
|
return True
|
|
|
|
if component == "gitea":
|
|
if rel in (*GITEA_FRESH_INSTALL_ENTRIES, *GITEA_SALVAGE_ENTRIES):
|
|
return True
|
|
|
|
if component == "n8n-private-extension":
|
|
parts = PurePosixPath(rel).parts
|
|
if (
|
|
len(parts) == 3
|
|
and parts[0] == "releases"
|
|
and parts[1] == "n8n-nodes-ndc"
|
|
and N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(parts[2])
|
|
):
|
|
return True
|
|
if (
|
|
len(parts) == 4
|
|
and parts[0] == "releases"
|
|
and parts[1] == "n8n-nodes-ndc"
|
|
and N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(parts[2])
|
|
and parts[3] in ("package.tgz", "release.json", "rollback.json")
|
|
):
|
|
return True
|
|
|
|
if component == "proxy-contur":
|
|
if rel in (
|
|
"Dockerfile",
|
|
"README.md",
|
|
"docker-compose.yml",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"server.js",
|
|
):
|
|
return True
|
|
|
|
if component == "dc-amd-proxy":
|
|
if rel in (
|
|
"Dockerfile",
|
|
"README.md",
|
|
"docker-compose.yml",
|
|
"package.json",
|
|
"server.mjs",
|
|
):
|
|
return True
|
|
|
|
if component == "dc-cms":
|
|
if rel in (
|
|
".dockerignore",
|
|
".gitignore",
|
|
"Dockerfile",
|
|
"README.md",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"infra/.env.example",
|
|
"infra/.env.synology.example",
|
|
"infra/docker-compose.yml",
|
|
"infra/authentik/bootstrap-cms.py",
|
|
):
|
|
return True
|
|
if rel.startswith((
|
|
"admin/",
|
|
"infra/authentik/custom-templates/",
|
|
"infra/reverse-proxy/",
|
|
"projects/",
|
|
"server/",
|
|
)):
|
|
return True
|
|
|
|
if component == "dc-cms-site-nodedc":
|
|
if rel in (
|
|
".gitignore",
|
|
"README.md",
|
|
"asset-manifest.json",
|
|
"index.html",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"robots.txt",
|
|
"sitemap.xml",
|
|
):
|
|
return True
|
|
if rel.startswith((
|
|
"assets/",
|
|
"content/",
|
|
"knowledge/",
|
|
"templates/",
|
|
"tools/",
|
|
)):
|
|
return True
|
|
|
|
die(f"path allowlist rejected: {rel}")
|
|
|
|
|
|
def path_is_covered_by_files_list(rel, entries):
|
|
return any(rel == entry or rel.startswith(entry.rstrip("/") + "/") for entry in entries)
|
|
|
|
|
|
def validate_payload_tree(component, payload_dir, entries):
|
|
root = payload_dir.resolve()
|
|
for path in payload_dir.rglob("*"):
|
|
resolved = path.resolve(strict=False)
|
|
if not is_relative_to(resolved, root):
|
|
die(f"payload path escaped payload dir: {path}")
|
|
|
|
rel = path.relative_to(payload_dir).as_posix()
|
|
if path.is_symlink():
|
|
die(f"payload symlink rejected: {rel}")
|
|
if not path.is_file() and not path.is_dir():
|
|
die(f"payload special file rejected: {rel}")
|
|
|
|
if path.is_dir():
|
|
continue
|
|
|
|
allowed_payload_path(component, rel)
|
|
if not path_is_covered_by_files_list(rel, entries):
|
|
die(f"payload file is not covered by files.txt: {rel}")
|
|
|
|
|
|
def read_strict_json(path, label, max_bytes=64 * 1024):
|
|
try:
|
|
file_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"{label} missing: {path}")
|
|
if stat.S_ISLNK(file_stat.st_mode) or not stat.S_ISREG(file_stat.st_mode):
|
|
die(f"{label} must be a regular file")
|
|
if file_stat.st_size < 2 or file_stat.st_size > max_bytes:
|
|
die(f"{label} has invalid size")
|
|
|
|
def reject_duplicate_keys(pairs):
|
|
result = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
die(f"{label} has duplicate key: {key}")
|
|
result[key] = value
|
|
return result
|
|
|
|
try:
|
|
return json.loads(
|
|
path.read_text(encoding="utf-8"),
|
|
object_pairs_hook=reject_duplicate_keys,
|
|
)
|
|
except UnicodeDecodeError:
|
|
die(f"{label} is not utf-8")
|
|
except json.JSONDecodeError as exc:
|
|
die(f"{label} is invalid json: {exc.msg}")
|
|
|
|
|
|
def require_exact_json_keys(value, expected, label):
|
|
if not isinstance(value, dict):
|
|
die(f"{label} must be an object")
|
|
actual = set(value)
|
|
if actual != set(expected):
|
|
die(f"{label} keys mismatch: expected={sorted(expected)} actual={sorted(actual)}")
|
|
|
|
|
|
def expected_gitea_fresh_install_descriptor():
|
|
return {
|
|
"schemaVersion": "nodedc.gitea.fresh-install.v1",
|
|
"action": "fresh-install",
|
|
"component": "gitea",
|
|
"installRoot": str(GITEA_ROOT),
|
|
"compose": {
|
|
"file": GITEA_COMPOSE_REL,
|
|
"project": GITEA_COMPOSE_PROJECT,
|
|
"service": GITEA_SERVICE,
|
|
"sha256": GITEA_COMPOSE_SHA256,
|
|
},
|
|
"runtime": {
|
|
"image": GITEA_IMAGE,
|
|
"platform": "linux/amd64",
|
|
"pullPolicy": "never",
|
|
"minimumComposeVersion": "2.20.1",
|
|
"transport": "unix:/run/gitea/gitea.sock",
|
|
"socketBind": (
|
|
"/volume1/docker/nodedc-gitea/socket:/run/gitea"
|
|
),
|
|
"ssh": "disabled-no-published-port",
|
|
"database": "fresh-sqlite-only",
|
|
"lfs": "disabled-pending-reviewed-restore-transition",
|
|
"networkMode": "none",
|
|
"logging": "bounded-json-file-10m-x3",
|
|
"stopGracePeriod": "30s",
|
|
},
|
|
"trust": {
|
|
"artifactSecrets": "forbidden",
|
|
"runtimeSecrets": "runner-managed-file-mounts",
|
|
"legacyRootAccess": "forbidden",
|
|
"legacyDatabaseImport": "forbidden",
|
|
"legacyRepositoryImport": "forbidden",
|
|
},
|
|
"reverseProxyPrerequisite": {
|
|
"managedOutsideArtifact": True,
|
|
"requiredDsmUpstream": "127.0.0.1:3000",
|
|
"requiredNginxBridge": str(GITEA_NGINX_BRIDGE_CONFIG),
|
|
"requiredNginxBridgeSha256": GITEA_NGINX_BRIDGE_SHA256,
|
|
"requiredUnixUpstream": str(GITEA_SOCKET_FILE),
|
|
"mustBeCompletedBeforeApply": True,
|
|
},
|
|
"rollback": (
|
|
"stop-candidate-preserve-fresh-runtime-state-and-restore-source"
|
|
),
|
|
}
|
|
|
|
|
|
def validate_gitea_fresh_install_payload(payload_dir, entries):
|
|
if tuple(entries) != GITEA_FRESH_INSTALL_ENTRIES:
|
|
die("Gitea fresh-install artifact entry set mismatch")
|
|
descriptor = read_strict_json(
|
|
payload_dir / GITEA_FRESH_INSTALL_DESCRIPTOR_REL,
|
|
"Gitea fresh-install descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_gitea_fresh_install_descriptor():
|
|
die("Gitea fresh-install descriptor mismatch")
|
|
|
|
compose = payload_dir / GITEA_COMPOSE_REL
|
|
try:
|
|
compose_stat = compose.lstat()
|
|
except FileNotFoundError:
|
|
die("Gitea fresh-install Compose file is missing")
|
|
if stat.S_ISLNK(compose_stat.st_mode) or not stat.S_ISREG(compose_stat.st_mode):
|
|
die("Gitea fresh-install Compose file is unsafe")
|
|
if sha256_file(compose) != GITEA_COMPOSE_SHA256:
|
|
die("Gitea fresh-install Compose digest mismatch")
|
|
try:
|
|
compose_text = compose.read_text(encoding="utf-8")
|
|
except UnicodeDecodeError:
|
|
die("Gitea fresh-install Compose file is not utf-8")
|
|
if compose_text.count(GITEA_IMAGE) != 1:
|
|
die("Gitea fresh-install image identity mismatch")
|
|
for required in (
|
|
"platform: linux/amd64",
|
|
"pull_policy: never",
|
|
"network_mode: none",
|
|
'user: "1000:1000"',
|
|
"stop_grace_period: 30s",
|
|
"driver: json-file",
|
|
'max-size: "10m"',
|
|
'max-file: "3"',
|
|
"GITEA__server__PROTOCOL: http+unix",
|
|
"GITEA__server__HTTP_ADDR: /run/gitea/gitea.sock",
|
|
'GITEA__server__UNIX_SOCKET_PERMISSION: "0666"',
|
|
"GITEA__server__LOCAL_ROOT_URL: http://unix/",
|
|
'GITEA__server__DISABLE_SSH: "true"',
|
|
'GITEA__server__LFS_START_SERVER: "false"',
|
|
'GITEA__server__LFS_ALLOW_PURE_SSH: "false"',
|
|
"GITEA__security__SECRET_KEY_URI: file:/run/secrets/gitea_secret_key",
|
|
"GITEA__security__INTERNAL_TOKEN_URI: file:/run/secrets/gitea_internal_token",
|
|
"GITEA__security__TWO_FACTOR_AUTH: enforced",
|
|
'GITEA__security__REVERSE_PROXY_LIMIT: "1"',
|
|
"GITEA__security__ALLOWED_HOST_LIST: loopback",
|
|
"GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: 127.0.0.0/8,::1/128",
|
|
'GITEA__service__DISABLE_REGISTRATION: "true"',
|
|
'GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION: "false"',
|
|
'GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION_API: "false"',
|
|
'GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION: "false"',
|
|
'GITEA__service__ENABLE_BASIC_AUTHENTICATION: "false"',
|
|
'GITEA__admin__DISABLE_REGULAR_ORG_CREATION: "true"',
|
|
"GITEA__admin__USER_DISABLED_FEATURES: deletion,manage_ssh_keys,manage_gpg_keys,change_username",
|
|
'GITEA__security__DISABLE_GIT_HOOKS: "true"',
|
|
'GITEA__security__DISABLE_WEBHOOKS: "true"',
|
|
'GITEA__repository__DISABLE_MIGRATIONS: "true"',
|
|
'GITEA__packages__ENABLED: "false"',
|
|
'GITEA__oauth2__ENABLED: "false"',
|
|
'GITEA__openid__ENABLE_OPENID_SIGNIN: "false"',
|
|
'GITEA__cron.update_checker__ENABLED: "false"',
|
|
"source: /volume1/docker/nodedc-gitea/socket",
|
|
"target: /run/gitea",
|
|
"create_host_path: false",
|
|
"read_only: true",
|
|
"no-new-privileges:true",
|
|
):
|
|
if required not in compose_text:
|
|
die(f"Gitea fresh-install Compose boundary missing: {required}")
|
|
for forbidden in (
|
|
"4022",
|
|
"2222:2222",
|
|
"0.0.0.0:3000",
|
|
"ports:",
|
|
"networks:",
|
|
"/var/run/docker.sock",
|
|
"/volume1/docker/gitea",
|
|
"privileged: true",
|
|
"pull_policy: always",
|
|
"__FILE",
|
|
"GITEA__security__SECRET_KEY:",
|
|
"GITEA__security__INTERNAL_TOKEN:",
|
|
"GITEA__server__LFS_JWT_SECRET:",
|
|
"GITEA__server__LFS_JWT_SECRET_URI",
|
|
"gitea_lfs_jwt_secret",
|
|
"lfs-jwt-secret",
|
|
"GITEA__server__REVERSE_PROXY_LIMIT",
|
|
"GITEA__server__REVERSE_PROXY_TRUSTED_PROXIES",
|
|
"GITEA__security__ENABLE_REVERSE_PROXY_AUTHENTICATION",
|
|
"GITEA__security__ENABLE_REVERSE_PROXY_AUTHENTICATION_API",
|
|
"GITEA__security__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION",
|
|
"GITEA__service__DISABLE_REGULAR_ORG_CREATION",
|
|
"GITEA__service__USER_DISABLED_FEATURES",
|
|
):
|
|
if forbidden in compose_text:
|
|
die(f"Gitea fresh-install Compose boundary violation: {forbidden}")
|
|
return descriptor
|
|
|
|
|
|
def canonical_gitea_salvage_record_sha256(record):
|
|
encoded = json.dumps(
|
|
record,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
).encode("utf-8")
|
|
return hashlib.sha256(encoded).hexdigest()
|
|
|
|
|
|
def canonical_gitea_salvage_evidence(value, label):
|
|
try:
|
|
encoded = json.dumps(
|
|
value,
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
except (TypeError, ValueError):
|
|
die(f"Gitea salvage {label} is not canonical JSON")
|
|
if len(encoded) > GITEA_SALVAGE_EVIDENCE_MAX_BYTES:
|
|
die(f"Gitea salvage {label} exceeds the evidence byte limit")
|
|
return {
|
|
"bytes": len(encoded),
|
|
"json": encoded.decode("utf-8"),
|
|
"sha256": hashlib.sha256(encoded).hexdigest(),
|
|
}
|
|
|
|
|
|
def read_gitea_salvage_csv(path, expected_header, expected_rows, label):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"{label} is missing")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_size < len(",".join(expected_header))
|
|
or path_stat.st_size > 8 * 1024 * 1024
|
|
):
|
|
die(f"{label} is unsafe")
|
|
try:
|
|
raw = path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die(f"{label} is not readable utf-8")
|
|
if raw.startswith("\ufeff") or "\x00" in raw or not raw.endswith("\n"):
|
|
die(f"{label} has invalid text framing")
|
|
try:
|
|
reader = csv.DictReader(io.StringIO(raw, newline=""), strict=True)
|
|
if tuple(reader.fieldnames or ()) != tuple(expected_header):
|
|
die(f"{label} header mismatch")
|
|
rows = list(reader)
|
|
except csv.Error as exc:
|
|
die(f"{label} is invalid csv: {exc}")
|
|
if len(rows) != expected_rows:
|
|
die(f"{label} row count mismatch")
|
|
for index, row in enumerate(rows, 2):
|
|
if None in row or set(row) != set(expected_header):
|
|
die(f"{label} row {index} shape mismatch")
|
|
for key, value in row.items():
|
|
if (
|
|
not isinstance(value, str)
|
|
or len(value.encode("utf-8")) > 1024
|
|
or any(ord(char) < 0x20 for char in value)
|
|
):
|
|
die(f"{label} row {index} field {key} is unsafe")
|
|
return rows
|
|
|
|
|
|
def validate_gitea_salvage_incident_disposition(payload_dir):
|
|
path = payload_dir / GITEA_SALVAGE_DISPOSITION_REL
|
|
try:
|
|
path_stat = path.lstat()
|
|
raw = path.read_bytes()
|
|
except (FileNotFoundError, OSError):
|
|
die("Gitea salvage confirmed disposition is missing or unreadable")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_size < 2
|
|
or path_stat.st_size > 64 * 1024
|
|
):
|
|
die("Gitea salvage confirmed disposition is unsafe")
|
|
if sha256_file(path) != GITEA_SALVAGE_DISPOSITION_SHA256:
|
|
die("Gitea salvage confirmed disposition digest mismatch")
|
|
disposition = read_strict_json(
|
|
path,
|
|
"Gitea salvage confirmed disposition",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
try:
|
|
canonical = (
|
|
json.dumps(
|
|
disposition,
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
+ b"\n"
|
|
)
|
|
except (TypeError, ValueError):
|
|
die("Gitea salvage confirmed disposition is not canonical JSON")
|
|
if raw != canonical:
|
|
die("Gitea salvage confirmed disposition byte framing is not canonical")
|
|
|
|
require_exact_json_keys(
|
|
disposition,
|
|
{
|
|
"activation",
|
|
"confirmation",
|
|
"incidentId",
|
|
"referencePolicy",
|
|
"remainingBlockers",
|
|
"repositoryStatePolicy",
|
|
"schemaVersion",
|
|
"scope",
|
|
"sourceEvidence",
|
|
},
|
|
"Gitea salvage confirmed disposition",
|
|
)
|
|
if (
|
|
disposition.get("schemaVersion")
|
|
!= "nodedc.gitea.incident-disposition.v1"
|
|
or disposition.get("incidentId") != "gitea-20260814"
|
|
or disposition.get("confirmation")
|
|
!= {
|
|
"confirmationRecordedAt": "2026-08-14T15:16:04Z",
|
|
"confirmationScope": (
|
|
"exact-10-users-45-repositories;heads-tags-live;"
|
|
"pull-remote-sealed-archive;non-git-state-policy;"
|
|
"no-2fa-change"
|
|
),
|
|
"confirmationSource": "owner-instruction-in-current-incident-thread",
|
|
"confirmedBy": "dctouch",
|
|
}
|
|
or disposition.get("scope")
|
|
!= {
|
|
"keptRepositories": 45,
|
|
"keptUsers": 10,
|
|
"referenceDecisionRows": 105,
|
|
"repositoryStores": 49,
|
|
}
|
|
):
|
|
die("Gitea salvage confirmed disposition identity mismatch")
|
|
|
|
source = disposition.get("sourceEvidence")
|
|
expected_source = {
|
|
"databaseSha256": GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256,
|
|
"identityDecisionManifestSha256": (
|
|
GITEA_SALVAGE_DECISION_MANIFEST_SHA256
|
|
),
|
|
"referenceManifestBytes": 36010,
|
|
"referenceManifestReviewState": "observed-unreviewed",
|
|
"referenceManifestSha256": (
|
|
GITEA_SALVAGE_DISPOSITION_REFERENCE_MANIFEST_SHA256
|
|
),
|
|
"snapshotUuid": GITEA_SALVAGE_SNAPSHOT_UUID,
|
|
"unsupportedRepositoryReportBytes": 119468,
|
|
"unsupportedRepositoryReportSha256": (
|
|
GITEA_SALVAGE_EXPECTED_UNSUPPORTED_REPORT_SHA256
|
|
),
|
|
"unsupportedSchemaCatalogReviewState": "observed-unreviewed",
|
|
"unsupportedSchemaCatalogSha256": (
|
|
GITEA_SALVAGE_DISPOSITION_UNSUPPORTED_SCHEMA_SHA256
|
|
),
|
|
}
|
|
if source != expected_source:
|
|
die("Gitea salvage confirmed disposition source mismatch")
|
|
|
|
reference_policy = disposition.get("referencePolicy")
|
|
require_exact_json_keys(
|
|
reference_policy,
|
|
{
|
|
"archiveOnly",
|
|
"exactDecisions",
|
|
"executionAuthority",
|
|
"forensicScope",
|
|
"headInvariants",
|
|
"liveRestore",
|
|
"unknownNamespacePolicy",
|
|
},
|
|
"Gitea salvage confirmed reference policy",
|
|
)
|
|
if (
|
|
reference_policy.get("executionAuthority")
|
|
!= "exact-decisions-one-to-one-observed-reference-manifest"
|
|
or reference_policy.get("unknownNamespacePolicy") != "reject"
|
|
or reference_policy.get("forensicScope")
|
|
!= {"allDiscoveredRefs": 105, "preserveNameOidEvidence": True}
|
|
or reference_policy.get("liveRestore")
|
|
!= {
|
|
"copyLegacyHeadOrRefFiles": False,
|
|
"mainRepositoryHeads": 85,
|
|
"mainRepositoryTags": 4,
|
|
"totalRefs": 93,
|
|
"wikiHeads": 4,
|
|
}
|
|
or reference_policy.get("archiveOnly")
|
|
!= {
|
|
"immutableArchiveRequired": True,
|
|
"neverAutoPromote": True,
|
|
"pullRefs": 5,
|
|
"remoteRefs": 7,
|
|
"totalRefs": 12,
|
|
}
|
|
or reference_policy.get("headInvariants")
|
|
!= {
|
|
"allowedMissingTargets": [
|
|
{
|
|
"head": "refs/heads/main",
|
|
"oldRepositoryId": 70,
|
|
"repositoryPath": "silver/nodedc_aegis.git",
|
|
"wiki": False,
|
|
},
|
|
{
|
|
"head": "refs/heads/main",
|
|
"oldRepositoryId": 2119,
|
|
"repositoryPath": "silver/nodedc_device_core.git",
|
|
"wiki": False,
|
|
},
|
|
],
|
|
"objectFormat": "sha1",
|
|
"stores": 49,
|
|
"symbolicHeads": 49,
|
|
"targetsPresentAmongLiveRefs": 47,
|
|
}
|
|
):
|
|
die("Gitea salvage confirmed reference policy mismatch")
|
|
|
|
exact_decisions = reference_policy.get("exactDecisions")
|
|
if not isinstance(exact_decisions, list) or len(exact_decisions) != 105:
|
|
die("Gitea salvage exact reference decision count mismatch")
|
|
previous_identity = None
|
|
reference_identities = set()
|
|
store_identities = set()
|
|
reference_counts = {
|
|
"main_heads": 0,
|
|
"main_tags": 0,
|
|
"pull": 0,
|
|
"remote": 0,
|
|
"wiki_heads": 0,
|
|
}
|
|
for record in exact_decisions:
|
|
require_exact_json_keys(
|
|
record,
|
|
{
|
|
"disposition",
|
|
"name",
|
|
"oid",
|
|
"oldRepositoryId",
|
|
"repositoryPath",
|
|
"wiki",
|
|
},
|
|
"Gitea salvage exact reference decision",
|
|
)
|
|
old_repo_id = record.get("oldRepositoryId")
|
|
repository_path = record.get("repositoryPath")
|
|
name = record.get("name")
|
|
oid = record.get("oid")
|
|
wiki = record.get("wiki")
|
|
if not isinstance(repository_path, str):
|
|
die("Gitea salvage exact reference path is invalid")
|
|
try:
|
|
validate_posix_path(repository_path)
|
|
except DeployError:
|
|
die("Gitea salvage exact reference path is invalid")
|
|
identity = (
|
|
old_repo_id,
|
|
int(wiki) if isinstance(wiki, bool) else -1,
|
|
repository_path,
|
|
name,
|
|
oid,
|
|
)
|
|
if (
|
|
not isinstance(old_repo_id, int)
|
|
or old_repo_id <= 0
|
|
or not isinstance(wiki, bool)
|
|
or len(PurePosixPath(repository_path).parts) != 2
|
|
or not isinstance(name, str)
|
|
or not gitea_salvage_refname_is_safe(name)
|
|
or not isinstance(oid, str)
|
|
or not re.fullmatch(r"[a-f0-9]{40}", oid)
|
|
or previous_identity is not None
|
|
and identity <= previous_identity
|
|
or identity in reference_identities
|
|
):
|
|
die("Gitea salvage exact reference decision is invalid")
|
|
previous_identity = identity
|
|
reference_identities.add(identity)
|
|
store_identities.add((old_repo_id, repository_path, wiki))
|
|
|
|
if wiki and name.startswith("refs/heads/"):
|
|
reference_counts["wiki_heads"] += 1
|
|
expected_disposition = "LIVE_RESTORE"
|
|
elif not wiki and name.startswith("refs/heads/"):
|
|
reference_counts["main_heads"] += 1
|
|
expected_disposition = "LIVE_RESTORE"
|
|
elif not wiki and name.startswith("refs/tags/"):
|
|
reference_counts["main_tags"] += 1
|
|
expected_disposition = "LIVE_RESTORE"
|
|
elif not wiki and name.startswith("refs/pull/"):
|
|
reference_counts["pull"] += 1
|
|
expected_disposition = "SEALED_ARCHIVE_ONLY"
|
|
elif not wiki and name.startswith("refs/remotes/"):
|
|
reference_counts["remote"] += 1
|
|
expected_disposition = "SEALED_ARCHIVE_ONLY"
|
|
else:
|
|
die("Gitea salvage exact reference namespace is rejected")
|
|
if record.get("disposition") != expected_disposition:
|
|
die("Gitea salvage exact reference disposition mismatch")
|
|
if (
|
|
reference_counts
|
|
!= {
|
|
"main_heads": 85,
|
|
"main_tags": 4,
|
|
"pull": 5,
|
|
"remote": 7,
|
|
"wiki_heads": 4,
|
|
}
|
|
or len(store_identities) != 47
|
|
or {
|
|
old_repo_id
|
|
for old_repo_id, _path, wiki in store_identities
|
|
if wiki
|
|
}
|
|
!= {1, 16, 38, 51}
|
|
):
|
|
die("Gitea salvage exact reference partition mismatch")
|
|
|
|
state_policy = disposition.get("repositoryStatePolicy")
|
|
require_exact_json_keys(
|
|
state_policy,
|
|
{
|
|
"attachments",
|
|
"directRelations",
|
|
"legacySecretsCredentialsSessionsKeysIntegrations",
|
|
"lfs",
|
|
"metadataArchive",
|
|
"numericHints",
|
|
"relationSemantics",
|
|
"schemaOnlyDependencyGroups",
|
|
"sourceNonzeroCategories",
|
|
"textMetadata",
|
|
"topics",
|
|
"units",
|
|
},
|
|
"Gitea salvage confirmed repository-state policy",
|
|
)
|
|
expected_nonzero = [
|
|
"access_grants",
|
|
"attachments",
|
|
"collaborators",
|
|
"issues",
|
|
"labels",
|
|
"lfs_objects",
|
|
"pull_requests_base",
|
|
"pull_requests_head",
|
|
"releases",
|
|
"repo_units",
|
|
"repository_hint:lfs_size",
|
|
"repository_hint:num_issues",
|
|
"repository_hint:num_pulls",
|
|
"repository_hint:num_watches",
|
|
"repository_metadata:description",
|
|
"repository_metadata:topics",
|
|
"watches",
|
|
]
|
|
if state_policy.get("sourceNonzeroCategories") != expected_nonzero:
|
|
die("Gitea salvage source nonzero-category partition mismatch")
|
|
|
|
direct_counts = {
|
|
"access_grants": 32,
|
|
"action_runners": 0,
|
|
"action_runs": 0,
|
|
"action_schedules": 0,
|
|
"action_secrets": 0,
|
|
"action_variables": 0,
|
|
"attachments": 230,
|
|
"collaborators": 32,
|
|
"deploy_keys": 0,
|
|
"issues": 867,
|
|
"labels": 7,
|
|
"lfs_locks": 0,
|
|
"lfs_objects": 573941,
|
|
"milestones": 0,
|
|
"mirrors": 0,
|
|
"packages": 0,
|
|
"protected_branches": 0,
|
|
"pull_requests_base": 5,
|
|
"pull_requests_head": 5,
|
|
"push_mirrors": 0,
|
|
"releases": 4,
|
|
"stars": 0,
|
|
"topics": 0,
|
|
"watches": 45,
|
|
"webhooks": 0,
|
|
}
|
|
nonzero_dispositions = {
|
|
"access_grants": "SANITIZED_ARCHIVE_THEN_RECOMPUTE",
|
|
"attachments": "PHYSICAL_VERIFY_THEN_SANITIZED_ARCHIVE",
|
|
"collaborators": (
|
|
"SANITIZED_ARCHIVE_THEN_RECREATE_AFTER_KEPT_USER_MAPPING"
|
|
),
|
|
"issues": "SANITIZED_ARCHIVE_ONLY",
|
|
"labels": "SANITIZED_ARCHIVE_ONLY",
|
|
"lfs_objects": "REACHABILITY_AND_PHYSICAL_VERIFY_THEN_RESTORE",
|
|
"pull_requests_base": (
|
|
"SANITIZED_ARCHIVE_ONLY_DIRECTIONAL_PROJECTION"
|
|
),
|
|
"pull_requests_head": (
|
|
"SANITIZED_ARCHIVE_ONLY_DIRECTIONAL_PROJECTION"
|
|
),
|
|
"releases": "SANITIZED_ARCHIVE_ONLY",
|
|
"watches": "DROP_AND_REGENERATE",
|
|
}
|
|
expected_direct_relations = [
|
|
{
|
|
"disposition": (
|
|
"ASSERT_ZERO_AND_DROP"
|
|
if source_count == 0
|
|
else nonzero_dispositions[label]
|
|
),
|
|
"label": label,
|
|
"sourceCount": source_count,
|
|
}
|
|
for label, source_count in sorted(direct_counts.items())
|
|
]
|
|
if state_policy.get("directRelations") != expected_direct_relations:
|
|
die("Gitea salvage direct-relation disposition partition mismatch")
|
|
|
|
expected_schema_groups = [
|
|
{
|
|
"disposition": (
|
|
"SANITIZED_ARCHIVE_ONLY_AFTER_SCHEMA_AND_JOIN_VERIFIER"
|
|
),
|
|
"group": "issue-pr-release-project",
|
|
"tables": [
|
|
"comment",
|
|
"issue_assignees",
|
|
"issue_content_history",
|
|
"issue_dependency",
|
|
"issue_label",
|
|
"issue_user",
|
|
"issue_watch",
|
|
"notification",
|
|
"project",
|
|
"project_board",
|
|
"project_issue",
|
|
"pull_auto_merge",
|
|
"reaction",
|
|
"review",
|
|
"review_state",
|
|
"stopwatch",
|
|
"tracked_time",
|
|
],
|
|
},
|
|
{
|
|
"disposition": (
|
|
"ASSERT_ZERO_AND_DROP_AFTER_SCHEMA_AND_PHYSICAL_CLOSURE_VERIFIER"
|
|
),
|
|
"group": "packages",
|
|
"tables": [
|
|
"package_blob",
|
|
"package_file",
|
|
"package_property",
|
|
"package_version",
|
|
],
|
|
},
|
|
{
|
|
"disposition": (
|
|
"ASSERT_ZERO_AND_DROP_AFTER_SCHEMA_AND_PHYSICAL_CLOSURE_VERIFIER"
|
|
),
|
|
"group": "actions",
|
|
"tables": [
|
|
"action_artifact",
|
|
"action_run_index",
|
|
"action_run_job",
|
|
"action_task",
|
|
],
|
|
},
|
|
]
|
|
if state_policy.get("schemaOnlyDependencyGroups") != expected_schema_groups:
|
|
die("Gitea salvage schema-only dependency disposition mismatch")
|
|
covered_tables = [
|
|
table
|
|
for group in expected_schema_groups
|
|
for table in group["tables"]
|
|
]
|
|
if len(covered_tables) != 25 or len(set(covered_tables)) != 25:
|
|
die("Gitea salvage schema-only dependency partition is not total")
|
|
|
|
expected_units = {
|
|
"actionsGloballyDisabled": True,
|
|
"legacyConfigImported": False,
|
|
"legacyRowsImported": False,
|
|
"packagesGloballyDisabled": True,
|
|
"rows": [
|
|
{
|
|
"disposition": disposition_name,
|
|
"name": name,
|
|
"sourceCount": source_count,
|
|
"targetCount": target_count,
|
|
"type": unit_type,
|
|
}
|
|
for (
|
|
unit_type,
|
|
name,
|
|
source_count,
|
|
target_count,
|
|
disposition_name,
|
|
) in (
|
|
(1, "Code", 45, 45, "CREATE_CLEAN_DEFAULT"),
|
|
(2, "Issues", 45, 45, "CREATE_CLEAN_DEFAULT"),
|
|
(3, "Pull Requests", 45, 45, "CREATE_CLEAN_DEFAULT"),
|
|
(4, "Releases", 45, 45, "CREATE_CLEAN_DEFAULT"),
|
|
(5, "Wiki", 45, 45, "CREATE_CLEAN_DEFAULT"),
|
|
(
|
|
6,
|
|
"External Wiki",
|
|
0,
|
|
0,
|
|
"ASSERT_SOURCE_ZERO_AND_FORBID_TARGET",
|
|
),
|
|
(
|
|
7,
|
|
"External Tracker",
|
|
0,
|
|
0,
|
|
"ASSERT_SOURCE_ZERO_AND_FORBID_TARGET",
|
|
),
|
|
(8, "Projects", 45, 45, "CREATE_CLEAN_DEFAULT"),
|
|
(9, "Packages", 45, 0, "DROP_AND_DISABLE_TARGET"),
|
|
(
|
|
10,
|
|
"Actions",
|
|
0,
|
|
0,
|
|
"ASSERT_SOURCE_ZERO_AND_DISABLE_TARGET",
|
|
),
|
|
)
|
|
],
|
|
"targetRowTotal": 270,
|
|
"targetUniquePerRepositoryAndType": True,
|
|
}
|
|
if state_policy.get("units") != expected_units:
|
|
die("Gitea salvage repo-unit disposition mismatch")
|
|
|
|
expected_numeric_hints = [
|
|
{
|
|
"disposition": "DROP_AND_RECOMPUTE",
|
|
"name": name,
|
|
"sourceNonzero": name
|
|
in {"lfs_size", "num_issues", "num_pulls", "num_watches"},
|
|
}
|
|
for name in (
|
|
"lfs_size",
|
|
"num_action_runs",
|
|
"num_issues",
|
|
"num_milestones",
|
|
"num_projects",
|
|
"num_pulls",
|
|
"num_stars",
|
|
"num_watches",
|
|
)
|
|
]
|
|
if (
|
|
state_policy.get("attachments")
|
|
!= {
|
|
"declaredRows": 230,
|
|
"disposition": "PHYSICAL_VERIFY_THEN_SANITIZED_ARCHIVE",
|
|
"legacyRowsImported": False,
|
|
"physicalPresenceClaimed": False,
|
|
}
|
|
or state_policy.get("lfs")
|
|
!= {
|
|
"associationRows": 573941,
|
|
"disposition": (
|
|
"DISABLED_UNTIL_REACHABLE_POINTER_PHYSICAL_SHA_VERIFIER"
|
|
),
|
|
"legacyRowsImported": False,
|
|
"physicalPresenceClaimed": False,
|
|
}
|
|
or state_policy.get("metadataArchive")
|
|
!= {
|
|
"categories": [
|
|
"issues",
|
|
"pull-requests",
|
|
"comments",
|
|
"releases",
|
|
"labels",
|
|
"projects",
|
|
"repository-description",
|
|
],
|
|
"legacyRowsImported": False,
|
|
"mode": "SANITIZED_IMMUTABLE_ARCHIVE_ONLY",
|
|
}
|
|
or state_policy.get("numericHints") != expected_numeric_hints
|
|
or state_policy.get("relationSemantics")
|
|
!= {
|
|
"attachmentLinkCountsMayOverlap": True,
|
|
"pullBaseAndHeadAreDirectionalProjections": True,
|
|
}
|
|
or state_policy.get("textMetadata")
|
|
!= [
|
|
{
|
|
"disposition": "ASSERT_ZERO_AND_DROP",
|
|
"name": "avatar",
|
|
"sourceRepositories": 0,
|
|
},
|
|
{
|
|
"disposition": "SANITIZED_ARCHIVE_ONLY",
|
|
"name": "description",
|
|
"sourceRepositories": 1,
|
|
},
|
|
{
|
|
"disposition": "ASSERT_ZERO_AND_DROP",
|
|
"name": "original_url",
|
|
"sourceRepositories": 0,
|
|
},
|
|
{
|
|
"disposition": (
|
|
"VERIFY_SEMANTIC_EMPTY_THEN_REBUILD_FROM_RELATIONS"
|
|
),
|
|
"name": "topics",
|
|
"sourceRepositories": 45,
|
|
},
|
|
{
|
|
"disposition": "ASSERT_ZERO_AND_DROP",
|
|
"name": "website",
|
|
"sourceRepositories": 0,
|
|
},
|
|
]
|
|
or state_policy.get("topics")
|
|
!= {
|
|
"acceptedLegacyEncoding": (
|
|
"canonical-json-null-or-canonical-json-array"
|
|
),
|
|
"canonicalLowercaseSortedUnique": True,
|
|
"expectedMaterialRepositories": 0,
|
|
"expectedRelationalRows": 0,
|
|
"expectedSerializedArrays": 0,
|
|
"expectedSerializedNulls": 45,
|
|
"expectedTopics": 0,
|
|
"legacyRowsImported": False,
|
|
"maxUtf8BytesPerTopic": 35,
|
|
"relationalAuthority": "repo_topic-join-topic",
|
|
"targetCache": "rebuild-from-relations",
|
|
"topicNamePattern": r"^[a-z0-9][-.a-z0-9]*$",
|
|
}
|
|
or state_policy.get(
|
|
"legacySecretsCredentialsSessionsKeysIntegrations"
|
|
)
|
|
!= "ZERO_NEVER_IMPORT"
|
|
):
|
|
die("Gitea salvage repository-state disposition mismatch")
|
|
|
|
if disposition.get("activation") != {
|
|
"allowedOperation": "canonical-plan-only",
|
|
"applyFrozen": True,
|
|
"freezeBoundary": "before-candidate-root-creation",
|
|
}:
|
|
die("Gitea salvage disposition activation policy mismatch")
|
|
if disposition.get("remainingBlockers") != list(
|
|
GITEA_SALVAGE_DISPOSITION_REMAINING_BLOCKERS
|
|
):
|
|
die("Gitea salvage disposition blocker contract mismatch")
|
|
return disposition
|
|
|
|
|
|
def validate_gitea_salvage_closure_disposition(payload_dir):
|
|
path = payload_dir / GITEA_SALVAGE_CLOSURE_DISPOSITION_REL
|
|
try:
|
|
path_stat = path.lstat()
|
|
raw = path.read_bytes()
|
|
except (FileNotFoundError, OSError):
|
|
die("Gitea salvage closure disposition is missing or unreadable")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_size < 2
|
|
or path_stat.st_size > 64 * 1024
|
|
or sha256_file(path) != GITEA_SALVAGE_CLOSURE_DISPOSITION_SHA256
|
|
):
|
|
die("Gitea salvage closure disposition identity is unsafe")
|
|
disposition = read_strict_json(
|
|
path,
|
|
"Gitea salvage closure disposition",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
try:
|
|
canonical = (
|
|
json.dumps(
|
|
disposition,
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
+ b"\n"
|
|
)
|
|
except (TypeError, ValueError):
|
|
die("Gitea salvage closure disposition is not canonical JSON")
|
|
if raw != canonical:
|
|
die("Gitea salvage closure disposition byte framing is not canonical")
|
|
require_exact_json_keys(
|
|
disposition,
|
|
{
|
|
"activation",
|
|
"authority",
|
|
"closureReport",
|
|
"incidentId",
|
|
"policies",
|
|
"predecessor",
|
|
"remainingBlockers",
|
|
"schemaVersion",
|
|
"scope",
|
|
"sourceEvidence",
|
|
},
|
|
"Gitea salvage closure disposition",
|
|
)
|
|
if (
|
|
disposition.get("schemaVersion")
|
|
!= "nodedc.gitea.incident-closure-disposition.v1"
|
|
or disposition.get("incidentId") != "gitea-20260814"
|
|
or disposition.get("activation")
|
|
!= {
|
|
"allowedOperation": "canonical-plan-only",
|
|
"applyFrozen": True,
|
|
"freezeBoundary": "before-candidate-root-creation",
|
|
}
|
|
or disposition.get("authority")
|
|
!= {
|
|
"policyScope": (
|
|
"access-collaboration-issue-pr-attachment-release-label-"
|
|
"project-unit-package-action-closure"
|
|
),
|
|
"source": "owner-instruction-in-current-incident-thread",
|
|
"state": "confirmed-policy-evidence-review-pending",
|
|
}
|
|
or disposition.get("scope")
|
|
!= {
|
|
"deletedRepositories": 2013,
|
|
"deletedUsers": 962,
|
|
"keptRepositories": 45,
|
|
"keptUsers": 10,
|
|
}
|
|
or disposition.get("predecessor")
|
|
!= {
|
|
"artifactSha256": GITEA_SALVAGE_CLOSURE_PREDECESSOR_ARTIFACT_SHA256,
|
|
"dispositionFile": GITEA_SALVAGE_DISPOSITION_REL,
|
|
"dispositionSha256": (
|
|
GITEA_SALVAGE_CLOSURE_PREDECESSOR_DISPOSITION_SHA256
|
|
),
|
|
}
|
|
or disposition.get("sourceEvidence")
|
|
!= {
|
|
"databaseSha256": GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256,
|
|
"identityDecisionManifestSha256": (
|
|
GITEA_SALVAGE_DECISION_MANIFEST_SHA256
|
|
),
|
|
"referenceManifestSha256": (
|
|
GITEA_SALVAGE_DISPOSITION_REFERENCE_MANIFEST_SHA256
|
|
),
|
|
"semanticTopicsSha256": GITEA_SALVAGE_DISPOSITION_TOPICS_SHA256,
|
|
"snapshotUuid": GITEA_SALVAGE_SNAPSHOT_UUID,
|
|
"unsupportedRepositoryReportSha256": (
|
|
GITEA_SALVAGE_EXPECTED_UNSUPPORTED_REPORT_SHA256
|
|
),
|
|
"unsupportedSchemaCatalogSha256": (
|
|
GITEA_SALVAGE_DISPOSITION_UNSUPPORTED_SCHEMA_SHA256
|
|
),
|
|
}
|
|
or disposition.get("closureReport")
|
|
!= {
|
|
"expectedBytes": None,
|
|
"expectedSha256": None,
|
|
"reviewState": "canonical-plan-output-unreviewed",
|
|
"schema": "nodedc.gitea.salvage-closure-inventory/v1",
|
|
}
|
|
or disposition.get("remainingBlockers")
|
|
!= list(GITEA_SALVAGE_CLOSURE_REMAINING_BLOCKERS)
|
|
):
|
|
die("Gitea salvage closure disposition contract mismatch")
|
|
|
|
policies = disposition.get("policies")
|
|
require_exact_json_keys(
|
|
policies,
|
|
{
|
|
"accessCollaboration",
|
|
"attachments",
|
|
"issuesPullRequestsMetadata",
|
|
"packagesActions",
|
|
"releasesLabelsProjects",
|
|
"repositoryState",
|
|
"units",
|
|
},
|
|
"Gitea salvage closure policies",
|
|
)
|
|
if (
|
|
policies["accessCollaboration"].get("accessCache")
|
|
!= "DROP_RESET_RECOMPUTE"
|
|
or policies["accessCollaboration"].get("collaboration")
|
|
!= "RECREATE_ONLY_KEPT_ACTORS_AFTER_REVIEWED_OLD_TO_NEW_ID_MAP"
|
|
or policies["accessCollaboration"].get("legacyRowsImported") is not False
|
|
or policies["attachments"].get("databaseManifest")
|
|
!= "ID_UUID_RELATIONS_DECLARED_SIZE_ONLY"
|
|
or policies["attachments"].get("legacyRowsImported") is not False
|
|
or policies["issuesPullRequestsMetadata"].get("archive")
|
|
!= "SANITIZED_IMMUTABLE_ARCHIVE_ONLY"
|
|
or policies["issuesPullRequestsMetadata"].get("legacyRowsImported")
|
|
is not False
|
|
or policies["issuesPullRequestsMetadata"].get("subrelationClosure")
|
|
!= {
|
|
"commentHistoryMerger": (
|
|
"SCHEMA_BOUND_EXACT_RELATION_COUNTS_AND_CLASSES"
|
|
),
|
|
"externalAuthors": (
|
|
"PRESENCE_AND_NAME_BYTE_LENGTHS_ONLY_NO_LOCAL_USER_MAPPING"
|
|
),
|
|
"legacyRowsImported": False,
|
|
"teamRelations": (
|
|
"EXACT_ROW_TEAM_ORG_IDS_SEALED_HOLD_AND_BLOCK_IF_PRESENT"
|
|
),
|
|
}
|
|
or policies["packagesActions"].get("actionsTarget") != "DISABLED"
|
|
or policies["packagesActions"].get("packageTarget") != "DISABLED"
|
|
or policies["packagesActions"].get("legacyPayloadSecretTokenLogImported")
|
|
is not False
|
|
or policies["repositoryState"].get(
|
|
"legacyHooksWebhooksKeysTokensSessionsCredentialsSecrets"
|
|
)
|
|
!= "IMPORT_ZERO"
|
|
or policies["units"].get("enabledCleanTypes") != [1, 2, 3, 4, 5, 8]
|
|
or policies["units"].get("disabledTypes") != [6, 7, 9, 10]
|
|
or policies["units"].get("legacyConfigImported") is not False
|
|
or policies["units"].get("legacyRowsImported") is not False
|
|
):
|
|
die("Gitea salvage closure policy mismatch")
|
|
return disposition
|
|
|
|
def validate_gitea_salvage_decision_bundle(payload_dir):
|
|
manifest_path = payload_dir / GITEA_SALVAGE_DECISION_MANIFEST_REL
|
|
users_path = payload_dir / GITEA_SALVAGE_USERS_REL
|
|
repositories_path = payload_dir / GITEA_SALVAGE_REPOSITORIES_REL
|
|
for path, expected, label in (
|
|
(
|
|
manifest_path,
|
|
GITEA_SALVAGE_DECISION_MANIFEST_SHA256,
|
|
"Gitea salvage decision manifest",
|
|
),
|
|
(users_path, GITEA_SALVAGE_USERS_SHA256, "Gitea salvage user decisions"),
|
|
(
|
|
repositories_path,
|
|
GITEA_SALVAGE_REPOSITORIES_SHA256,
|
|
"Gitea salvage repository decisions",
|
|
),
|
|
):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"{label} is missing")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"{label} is unsafe")
|
|
if sha256_file(path) != expected:
|
|
die(f"{label} digest mismatch")
|
|
|
|
manifest = read_strict_json(
|
|
manifest_path,
|
|
"Gitea salvage decision manifest",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
require_exact_json_keys(
|
|
manifest,
|
|
{
|
|
"confirmation_scope",
|
|
"confirmed_at",
|
|
"confirmed_by",
|
|
"counts",
|
|
"execution_policy",
|
|
"repositories_file",
|
|
"repositories_file_sha256",
|
|
"schema",
|
|
"source",
|
|
"users_file",
|
|
"users_file_sha256",
|
|
},
|
|
"Gitea salvage decision manifest",
|
|
)
|
|
if (
|
|
manifest.get("schema") != "nodedc.gitea.incident-decision/v2"
|
|
or manifest.get("confirmed_by") != "dctouch"
|
|
or manifest.get("repositories_file") != "repositories.decisions.csv"
|
|
or manifest.get("users_file") != "users.decisions.csv"
|
|
or manifest.get("repositories_file_sha256")
|
|
!= GITEA_SALVAGE_REPOSITORIES_SHA256
|
|
or manifest.get("users_file_sha256") != GITEA_SALVAGE_USERS_SHA256
|
|
or manifest.get("counts")
|
|
!= {
|
|
"repositories_delete": 2013,
|
|
"repositories_keep": 45,
|
|
"repositories_total": 2058,
|
|
"users_delete": 962,
|
|
"users_keep_active": 2,
|
|
"users_keep_locked": 8,
|
|
"users_total": 972,
|
|
}
|
|
or manifest.get("execution_policy")
|
|
!= {
|
|
"explicit_rows_only": True,
|
|
"two_factor_authentication": (
|
|
"not changed; owner will configure it manually"
|
|
),
|
|
"wildcard_or_owner_only_execution_forbidden": True,
|
|
}
|
|
):
|
|
die("Gitea salvage decision manifest contract mismatch")
|
|
source = manifest.get("source")
|
|
if (
|
|
not isinstance(source, dict)
|
|
or source.get("snapshot_uuid") != GITEA_SALVAGE_SNAPSHOT_UUID
|
|
or source.get("database_sha256")
|
|
!= GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256
|
|
):
|
|
die("Gitea salvage decision source mismatch")
|
|
|
|
user_header = (
|
|
"decision",
|
|
"user_id",
|
|
"owner",
|
|
"lower_owner",
|
|
"desired_active",
|
|
"desired_restricted",
|
|
"desired_admin",
|
|
"current_active",
|
|
"current_restricted",
|
|
"current_admin",
|
|
"repository_count",
|
|
"record_sha256",
|
|
)
|
|
users = read_gitea_salvage_csv(
|
|
users_path,
|
|
user_header,
|
|
972,
|
|
"Gitea salvage user decisions",
|
|
)
|
|
user_ids = set()
|
|
user_counts = {name: 0 for name in GITEA_SALVAGE_USER_COUNTS}
|
|
previous_user_id = 0
|
|
for row in users:
|
|
try:
|
|
user_id = int(row["user_id"])
|
|
current_values = tuple(
|
|
int(row[key])
|
|
for key in (
|
|
"current_active",
|
|
"current_restricted",
|
|
"current_admin",
|
|
"repository_count",
|
|
)
|
|
)
|
|
except ValueError:
|
|
die("Gitea salvage user decision has a non-integer field")
|
|
if (
|
|
user_id <= previous_user_id
|
|
or user_id in user_ids
|
|
or not re.fullmatch(r"[A-Za-z0-9_.-]{1,40}", row["owner"])
|
|
or row["lower_owner"] != row["owner"].casefold()
|
|
or row["record_sha256"]
|
|
!= row["record_sha256"].lower()
|
|
or not re.fullmatch(r"[a-f0-9]{64}", row["record_sha256"])
|
|
or any(value < 0 for value in current_values)
|
|
):
|
|
die("Gitea salvage user decision identity mismatch")
|
|
previous_user_id = user_id
|
|
user_ids.add(user_id)
|
|
expected_name = GITEA_SALVAGE_ACTIVE_USERS.get(
|
|
user_id,
|
|
GITEA_SALVAGE_LOCKED_USERS.get(user_id),
|
|
)
|
|
if user_id in GITEA_SALVAGE_ACTIVE_USERS:
|
|
expected = (
|
|
"KEEP_ACTIVE",
|
|
"1",
|
|
"0",
|
|
"1" if user_id == 1 else "0",
|
|
)
|
|
elif user_id in GITEA_SALVAGE_LOCKED_USERS:
|
|
expected = ("KEEP_LOCKED", "0", "1", "0")
|
|
else:
|
|
expected = ("DELETE", "", "", "")
|
|
if (
|
|
(row["decision"], row["desired_active"], row["desired_restricted"], row["desired_admin"])
|
|
!= expected
|
|
or expected_name is not None
|
|
and row["owner"] != expected_name
|
|
):
|
|
die("Gitea salvage user disposition mismatch")
|
|
user_counts[row["decision"]] += 1
|
|
if user_counts != GITEA_SALVAGE_USER_COUNTS:
|
|
die("Gitea salvage user disposition count mismatch")
|
|
|
|
repository_header = (
|
|
"decision",
|
|
"repo_id",
|
|
"owner_id",
|
|
"owner",
|
|
"lower_owner",
|
|
"slug",
|
|
"lower_slug",
|
|
"repo_relative_path",
|
|
"wiki_relative_path",
|
|
"record_sha256",
|
|
)
|
|
repositories = read_gitea_salvage_csv(
|
|
repositories_path,
|
|
repository_header,
|
|
2058,
|
|
"Gitea salvage repository decisions",
|
|
)
|
|
repository_ids = set()
|
|
repository_paths = set()
|
|
repository_counts = {name: 0 for name in GITEA_SALVAGE_REPOSITORY_COUNTS}
|
|
previous_repository_id = 0
|
|
kept_by_owner = {}
|
|
for row in repositories:
|
|
try:
|
|
repo_id = int(row["repo_id"])
|
|
owner_id = int(row["owner_id"])
|
|
except ValueError:
|
|
die("Gitea salvage repository decision has a non-integer field")
|
|
expected_repo_path = f"{row['lower_owner']}/{row['lower_slug']}.git"
|
|
expected_wiki_path = f"{row['lower_owner']}/{row['lower_slug']}.wiki.git"
|
|
for relative in (row["repo_relative_path"], row["wiki_relative_path"]):
|
|
validate_posix_path(relative)
|
|
if len(PurePosixPath(relative).parts) != 2:
|
|
die("Gitea salvage repository path depth mismatch")
|
|
if (
|
|
repo_id <= previous_repository_id
|
|
or repo_id in repository_ids
|
|
or not re.fullmatch(r"[A-Za-z0-9_.-]{1,100}", row["slug"])
|
|
or row["lower_owner"] != row["owner"].casefold()
|
|
or row["lower_slug"] != row["slug"].casefold()
|
|
or row["repo_relative_path"] != expected_repo_path
|
|
or row["wiki_relative_path"] != expected_wiki_path
|
|
or row["repo_relative_path"] in repository_paths
|
|
or row["wiki_relative_path"] in repository_paths
|
|
or not re.fullmatch(r"[a-f0-9]{64}", row["record_sha256"])
|
|
):
|
|
die("Gitea salvage repository identity mismatch")
|
|
previous_repository_id = repo_id
|
|
repository_ids.add(repo_id)
|
|
repository_paths.update((row["repo_relative_path"], row["wiki_relative_path"]))
|
|
expected_owner = GITEA_SALVAGE_ACTIVE_USERS.get(owner_id)
|
|
expected_decision = "KEEP" if expected_owner == row["owner"] else "DELETE"
|
|
if row["decision"] != expected_decision:
|
|
die("Gitea salvage repository disposition mismatch")
|
|
repository_counts[row["decision"]] += 1
|
|
if row["decision"] == "KEEP":
|
|
kept_by_owner[row["owner"]] = kept_by_owner.get(row["owner"], 0) + 1
|
|
if (
|
|
repository_counts != GITEA_SALVAGE_REPOSITORY_COUNTS
|
|
or kept_by_owner != {"dctouch": 32, "SILVER": 13}
|
|
):
|
|
die("Gitea salvage repository disposition count mismatch")
|
|
return {
|
|
"manifest": manifest,
|
|
"users": users,
|
|
"repositories": repositories,
|
|
"kept_users": [row for row in users if row["decision"] != "DELETE"],
|
|
"kept_repositories": [
|
|
row for row in repositories if row["decision"] == "KEEP"
|
|
],
|
|
}
|
|
|
|
|
|
def validate_gitea_incident_salvage_payload(payload_dir, entries):
|
|
if tuple(entries) != GITEA_SALVAGE_ENTRIES:
|
|
die("Gitea incident-salvage artifact entry set mismatch")
|
|
descriptor_path = payload_dir / GITEA_SALVAGE_DESCRIPTOR_REL
|
|
if sha256_file(descriptor_path) != GITEA_SALVAGE_DESCRIPTOR_SHA256:
|
|
die("Gitea incident-salvage descriptor digest mismatch")
|
|
descriptor = read_strict_json(
|
|
descriptor_path,
|
|
"Gitea incident-salvage descriptor",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
if (
|
|
descriptor.get("schemaVersion")
|
|
!= "nodedc.gitea.incident-salvage.v3"
|
|
or descriptor.get("action") != "clean-state-salvage"
|
|
or descriptor.get("component") != "gitea"
|
|
or descriptor.get("incidentId") != "gitea-20260814"
|
|
or (descriptor.get("compose") or {}).get("sha256")
|
|
!= GITEA_SALVAGE_COMPOSE_SHA256
|
|
or (descriptor.get("runtime") or {}).get("image")
|
|
!= GITEA_SALVAGE_IMAGE
|
|
or (descriptor.get("runtime") or {}).get("imageId")
|
|
!= GITEA_SALVAGE_IMAGE_ID
|
|
or (descriptor.get("runtime") or {}).get("repoDigest")
|
|
!= GITEA_SALVAGE_REPO_DIGEST
|
|
or (descriptor.get("disposition") or {}).get("schema")
|
|
!= "nodedc.gitea.incident-disposition.v1"
|
|
or (descriptor.get("disposition") or {}).get("file")
|
|
!= GITEA_SALVAGE_DISPOSITION_REL
|
|
or (descriptor.get("disposition") or {}).get("sha256")
|
|
!= GITEA_SALVAGE_DISPOSITION_SHA256
|
|
or (descriptor.get("closureDisposition") or {}).get("schema")
|
|
!= "nodedc.gitea.incident-closure-disposition.v1"
|
|
or (descriptor.get("closureDisposition") or {}).get("file")
|
|
!= GITEA_SALVAGE_CLOSURE_DISPOSITION_REL
|
|
or (descriptor.get("closureDisposition") or {}).get("sha256")
|
|
!= GITEA_SALVAGE_CLOSURE_DISPOSITION_SHA256
|
|
or (descriptor.get("closureDisposition") or {}).get(
|
|
"predecessorArtifactSha256"
|
|
)
|
|
!= GITEA_SALVAGE_CLOSURE_PREDECESSOR_ARTIFACT_SHA256
|
|
or (descriptor.get("identity") or {}).get("twoFactorAuthentication")
|
|
!= "not-configured-by-transition"
|
|
or (descriptor.get("identity") or {}).get("preserveNumericUserIds")
|
|
is not False
|
|
or (descriptor.get("identity") or {}).get("preserveNumericRepositoryIds")
|
|
is not False
|
|
):
|
|
die("Gitea incident-salvage descriptor contract mismatch")
|
|
|
|
compose = payload_dir / GITEA_COMPOSE_REL
|
|
try:
|
|
compose_stat = compose.lstat()
|
|
compose_text = compose.read_text(encoding="utf-8")
|
|
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
die("Gitea incident-salvage Compose file is missing or invalid")
|
|
if (
|
|
stat.S_ISLNK(compose_stat.st_mode)
|
|
or not stat.S_ISREG(compose_stat.st_mode)
|
|
or sha256_file(compose) != GITEA_SALVAGE_COMPOSE_SHA256
|
|
or compose_text.count(GITEA_SALVAGE_IMAGE) != 1
|
|
):
|
|
die("Gitea incident-salvage Compose identity mismatch")
|
|
for required in (
|
|
"platform: linux/amd64",
|
|
"pull_policy: never",
|
|
"network_mode: none",
|
|
'user: "1000:1000"',
|
|
"- /usr/local/bin/gitea",
|
|
"- /etc/gitea/app.ini",
|
|
'restart: "no"',
|
|
"read_only: true",
|
|
"- ALL",
|
|
"no-new-privileges:true",
|
|
"source: /volume1/docker/nodedc-gitea/data",
|
|
"target: /data",
|
|
"source: /volume1/docker/nodedc-gitea/config",
|
|
"target: /etc/gitea",
|
|
"source: /volume1/docker/nodedc-gitea/socket",
|
|
"target: /run/gitea",
|
|
"create_host_path: false",
|
|
):
|
|
if required not in compose_text:
|
|
die(f"Gitea incident-salvage Compose boundary missing: {required}")
|
|
for forbidden in (
|
|
"ports:",
|
|
"networks:",
|
|
"/var/run/docker.sock",
|
|
"/volume1/docker/gitea",
|
|
"privileged: true",
|
|
"4022",
|
|
"TWO_FACTOR_AUTH",
|
|
"LFS_JWT_SECRET",
|
|
"restart: unless-stopped",
|
|
):
|
|
if forbidden in compose_text:
|
|
die(f"Gitea incident-salvage Compose boundary violation: {forbidden}")
|
|
disposition = validate_gitea_salvage_incident_disposition(payload_dir)
|
|
closure_disposition = validate_gitea_salvage_closure_disposition(payload_dir)
|
|
decisions = validate_gitea_salvage_decision_bundle(payload_dir)
|
|
return {
|
|
"closure_disposition": closure_disposition,
|
|
"descriptor": descriptor,
|
|
"disposition": disposition,
|
|
**decisions,
|
|
}
|
|
|
|
|
|
def validate_n8n_package_tarball(package_path, release):
|
|
package_stat = package_path.lstat()
|
|
if stat.S_ISLNK(package_stat.st_mode) or not stat.S_ISREG(package_stat.st_mode):
|
|
die("private extension package must be a regular file")
|
|
if package_stat.st_size < 1024 or package_stat.st_size > 32 * 1024 * 1024:
|
|
die("private extension package has invalid size")
|
|
if sha256_file(package_path) != release["package"]["sha256"]:
|
|
die("private extension package sha256 mismatch")
|
|
if package_stat.st_size != release["package"]["bytes"]:
|
|
die("private extension package byte count mismatch")
|
|
|
|
names = set()
|
|
package_bytes = 0
|
|
package_json_bytes = None
|
|
packed_node_sources = {}
|
|
with tarfile.open(package_path, "r:gz") as archive:
|
|
for member_count, member in enumerate(archive, 1):
|
|
if member_count > 512:
|
|
die("private extension package has too many members")
|
|
validate_posix_path(member.name)
|
|
if member.name in names:
|
|
die(f"duplicate private extension package member: {member.name}")
|
|
names.add(member.name)
|
|
if member.name != "package" and not member.name.startswith("package/"):
|
|
die(f"private extension package member escaped package root: {member.name}")
|
|
if not (member.isfile() or member.isdir()):
|
|
die(f"unsupported private extension package member: {member.name}")
|
|
if member.name == "package":
|
|
if not member.isdir():
|
|
die("private extension package root must be a directory")
|
|
else:
|
|
package_rel = PurePosixPath(member.name).relative_to("package")
|
|
if any(part.startswith(".") or part.startswith("._") for part in package_rel.parts):
|
|
die(f"private extension package hidden member rejected: {member.name}")
|
|
if not (
|
|
package_rel.as_posix() in ("README.md", "package.json")
|
|
or package_rel.as_posix().startswith("dist/")
|
|
):
|
|
die(f"private extension package member is not allowlisted: {member.name}")
|
|
expected_mode = 0o644 if member.isfile() else 0o755
|
|
if stat.S_IMODE(member.mode) != expected_mode:
|
|
die(f"unsafe private extension package mode: {member.name}")
|
|
if member.isfile():
|
|
package_bytes += member.size
|
|
if member.size > 4 * 1024 * 1024 or package_bytes > 64 * 1024 * 1024:
|
|
die("private extension package payload is too large")
|
|
if member.name == "package/package.json":
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die("private extension package.json cannot be read")
|
|
package_json_bytes = source.read(64 * 1024 + 1)
|
|
if member.name.startswith("package/dist/nodes/") and member.name.endswith(".node.js"):
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die(f"private extension packed node cannot be read: {member.name}")
|
|
packed_node_sources[member.name[len("package/"):]] = source.read(4 * 1024 * 1024 + 1)
|
|
|
|
if package_json_bytes is None or len(package_json_bytes) > 64 * 1024:
|
|
die("private extension package.json missing or too large")
|
|
try:
|
|
package_json = json.loads(package_json_bytes.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
die("private extension package.json is invalid")
|
|
if package_json.get("name") != "n8n-nodes-ndc":
|
|
die("private extension package name mismatch")
|
|
if package_json.get("version") != release["package"]["version"]:
|
|
die("private extension package version mismatch")
|
|
if package_json.get("private") is not True:
|
|
die("private extension package must remain private")
|
|
if package_json.get("dependencies") not in (None, {}):
|
|
die("private extension runtime dependencies are forbidden")
|
|
scripts = package_json.get("scripts") or {}
|
|
if not isinstance(scripts, dict):
|
|
die("private extension package scripts must be an object")
|
|
for lifecycle in ("preinstall", "install", "postinstall", "prepack", "prepare", "postpack"):
|
|
if lifecycle in scripts:
|
|
die(f"private extension lifecycle script is forbidden: {lifecycle}")
|
|
|
|
n8n = package_json.get("n8n")
|
|
if not isinstance(n8n, dict):
|
|
die("private extension n8n registration missing")
|
|
registered = []
|
|
expected_registrations = {
|
|
"nodes": N8N_PRIVATE_EXTENSION_NODES,
|
|
"credentials": N8N_PRIVATE_EXTENSION_CREDENTIALS,
|
|
}
|
|
for field, expected in expected_registrations.items():
|
|
values = n8n.get(field)
|
|
if not isinstance(values, list) or not values:
|
|
die(f"private extension n8n.{field} registration missing")
|
|
if values != list(expected):
|
|
die(f"private extension n8n.{field} registration mismatch")
|
|
for value in values:
|
|
if not isinstance(value, str) or not value.startswith("dist/") or value not in names and f"package/{value}" not in names:
|
|
die(f"private extension n8n.{field} member missing: {value}")
|
|
registered.append(value)
|
|
if len(registered) != len(set(registered)):
|
|
die("private extension n8n registration contains duplicates")
|
|
packed_nodes = sorted(
|
|
name[len("package/"):]
|
|
for name in names
|
|
if name.startswith("package/dist/nodes/") and name.endswith(".node.js")
|
|
)
|
|
packed_credentials = sorted(
|
|
name[len("package/"):]
|
|
for name in names
|
|
if name.startswith("package/dist/credentials/") and name.endswith(".credentials.js")
|
|
)
|
|
if packed_nodes != sorted(N8N_PRIVATE_EXTENSION_NODES):
|
|
die("private extension packed node set mismatch")
|
|
if packed_credentials != sorted(N8N_PRIVATE_EXTENSION_CREDENTIALS):
|
|
die("private extension packed credential set mismatch")
|
|
for node_path in N8N_PRIVATE_EXTENSION_NODES:
|
|
source = packed_node_sources.get(node_path)
|
|
if source is None or len(source) > 4 * 1024 * 1024:
|
|
die(f"private extension packed node source missing or too large: {node_path}")
|
|
if b"usableAsTool" in source:
|
|
die(f"private extension packed node would generate a tool variant: {node_path}")
|
|
|
|
|
|
def validate_n8n_private_extension_release(payload_dir, entries):
|
|
if len(entries) != 1:
|
|
die("private extension artifact must contain exactly one immutable release entry")
|
|
release_rel = entries[0]
|
|
parts = PurePosixPath(release_rel).parts
|
|
if (
|
|
len(parts) != 3
|
|
or parts[0] != "releases"
|
|
or parts[1] != "n8n-nodes-ndc"
|
|
or not N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(parts[2])
|
|
):
|
|
die("private extension files.txt entry must be a digest-bound release directory")
|
|
|
|
release_dir = payload_dir / release_rel
|
|
expected_files = {"package.tgz", "release.json", "rollback.json"}
|
|
actual_files = {path.name for path in release_dir.iterdir() if path.is_file()}
|
|
actual_dirs = [path.name for path in release_dir.iterdir() if path.is_dir()]
|
|
if actual_dirs or actual_files != expected_files:
|
|
die("private extension release must contain only package.tgz, release.json and rollback.json")
|
|
|
|
release = read_strict_json(release_dir / "release.json", "private extension release manifest")
|
|
require_exact_json_keys(release, ("schemaVersion", "releaseId", "package", "storage", "activation"), "private extension release manifest")
|
|
require_exact_json_keys(release["package"], ("name", "version", "sha256", "bytes", "runtimeTypePrefix"), "private extension package manifest")
|
|
require_exact_json_keys(release["storage"], ("relativePath", "immutable"), "private extension storage manifest")
|
|
require_exact_json_keys(
|
|
release["activation"],
|
|
(
|
|
"owner",
|
|
"status",
|
|
"requiredCommunityPackagePath",
|
|
"requiresAtomicReleaseSwitch",
|
|
"requiresAllN8nProcessesRestart",
|
|
"requiresMcpSchemaAcceptance",
|
|
"rollbackBaselinePolicy",
|
|
),
|
|
"private extension activation manifest",
|
|
)
|
|
require_exact_json_keys(
|
|
release["activation"]["rollbackBaselinePolicy"],
|
|
("allowed", "firstActivation", "requiresPreActivationVerification"),
|
|
"private extension rollback baseline policy",
|
|
)
|
|
|
|
release_id = parts[2]
|
|
package = release["package"]
|
|
activation = release["activation"]
|
|
if release["schemaVersion"] != "nodedc.n8n-private-extension-release/v2":
|
|
die("private extension release schema mismatch")
|
|
if release["releaseId"] != release_id:
|
|
die("private extension release id mismatch")
|
|
if package["name"] != "n8n-nodes-ndc" or package["runtimeTypePrefix"] != "n8n-nodes-ndc.":
|
|
die("private extension package identity mismatch")
|
|
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", str(package["version"])):
|
|
die("private extension package version is invalid")
|
|
if not re.fullmatch(r"[a-f0-9]{64}", str(package["sha256"])):
|
|
die("private extension package sha256 is invalid")
|
|
if not isinstance(package["bytes"], int) or isinstance(package["bytes"], bool):
|
|
die("private extension package byte count is invalid")
|
|
if release_id != f"{package['version']}-{package['sha256'][:16]}":
|
|
die("private extension release id is not digest-bound")
|
|
if release["storage"] != {"relativePath": release_rel, "immutable": True}:
|
|
die("private extension storage manifest mismatch")
|
|
rollback_baseline_policy = {
|
|
"allowed": [
|
|
"previous_verified_immutable_release",
|
|
"verified_inactive",
|
|
],
|
|
"firstActivation": "verified_inactive",
|
|
"requiresPreActivationVerification": True,
|
|
}
|
|
if activation != {
|
|
"owner": "engine",
|
|
"status": "blocked_pending_engine_owned_mount",
|
|
"requiredCommunityPackagePath": "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc",
|
|
"requiresAtomicReleaseSwitch": True,
|
|
"requiresAllN8nProcessesRestart": True,
|
|
"requiresMcpSchemaAcceptance": True,
|
|
"rollbackBaselinePolicy": rollback_baseline_policy,
|
|
}:
|
|
die("private extension activation boundary mismatch")
|
|
|
|
rollback = read_strict_json(release_dir / "rollback.json", "private extension rollback manifest")
|
|
require_exact_json_keys(
|
|
rollback,
|
|
("schemaVersion", "releaseId", "packageSha256", "mode", "baselinePolicy", "steps", "forbidden"),
|
|
"private extension rollback manifest",
|
|
)
|
|
require_exact_json_keys(
|
|
rollback["baselinePolicy"],
|
|
("allowed", "firstActivation", "requiresPreActivationVerification"),
|
|
"private extension rollback manifest baseline policy",
|
|
)
|
|
if rollback != {
|
|
"schemaVersion": "nodedc.n8n-private-extension-rollback/v2",
|
|
"releaseId": release_id,
|
|
"packageSha256": package["sha256"],
|
|
"mode": "engine-owned-atomic-release-switch",
|
|
"baselinePolicy": rollback_baseline_policy,
|
|
"steps": [
|
|
"select_verified_previous_release_or_preverified_inactive_baseline",
|
|
"switch_engine_owned_mount_atomically",
|
|
"restart_all_n8n_processes",
|
|
"verify_mcp_schema_state_matches_selected_baseline",
|
|
],
|
|
"forbidden": [
|
|
"delete_active_release",
|
|
"mutate_engine_core",
|
|
"live_npm_install",
|
|
],
|
|
}:
|
|
die("private extension rollback manifest mismatch")
|
|
|
|
validate_n8n_package_tarball(release_dir / "package.tgz", release)
|
|
|
|
|
|
def is_engine_n8n_transition(component, entries):
|
|
return component == "engine" and entries is not None and ENGINE_N8N_TRANSITION_DESCRIPTOR_REL in entries
|
|
|
|
|
|
def engine_n8n_credential_types_for_release(release_id):
|
|
expected = ENGINE_N8N_CREDENTIAL_TYPES_BY_RELEASE.get(release_id)
|
|
if expected is None:
|
|
die("Engine n8n credential catalog release is not registered")
|
|
return expected
|
|
|
|
|
|
def read_engine_n8n_transition_descriptor(path, label="Engine n8n transition descriptor"):
|
|
descriptor = read_strict_json(path, label)
|
|
require_exact_json_keys(
|
|
descriptor,
|
|
(
|
|
"schemaVersion",
|
|
"action",
|
|
"releaseId",
|
|
"packageVersion",
|
|
"packageSha256",
|
|
"n8nVersion",
|
|
"baseImage",
|
|
"baseImageArchitecture",
|
|
"baseImageIdentityPolicy",
|
|
"sealedReleaseRelativePath",
|
|
"composeOverride",
|
|
"runtimePackagePath",
|
|
"topologyServices",
|
|
"expectedCurrent",
|
|
"expectedNodeTypes",
|
|
"expectedCredentialTypes",
|
|
"rollbackBaseline",
|
|
),
|
|
label,
|
|
)
|
|
action = descriptor.get("action")
|
|
if descriptor.get("schemaVersion") != "nodedc.engine-n8n-private-extension-transition/v1":
|
|
die("Engine n8n transition schema mismatch")
|
|
if action not in ("activate", "rollback-inactive"):
|
|
die("Engine n8n transition action mismatch")
|
|
release_id = descriptor.get("releaseId")
|
|
package_version = descriptor.get("packageVersion")
|
|
package_sha256 = descriptor.get("packageSha256")
|
|
if not isinstance(release_id, str) or not N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(release_id):
|
|
die("Engine n8n transition release id is invalid")
|
|
if not isinstance(package_version, str) or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", package_version):
|
|
die("Engine n8n transition package version is invalid")
|
|
if not isinstance(package_sha256, str) or not re.fullmatch(r"[a-f0-9]{64}", package_sha256):
|
|
die("Engine n8n transition package sha256 is invalid")
|
|
if release_id != f"{package_version}-{package_sha256[:16]}":
|
|
die("Engine n8n transition release identity mismatch")
|
|
expected_sealed_path = f"n8n-private-extensions/releases/n8n-nodes-ndc/{release_id}/package"
|
|
exact_common = {
|
|
"n8nVersion": ENGINE_N8N_VERSION,
|
|
"baseImage": ENGINE_N8N_BASE_IMAGE,
|
|
"baseImageArchitecture": ENGINE_N8N_BASE_ARCHITECTURE,
|
|
"baseImageIdentityPolicy": "running-container-and-local-tag-must-match",
|
|
"sealedReleaseRelativePath": expected_sealed_path,
|
|
"composeOverride": ENGINE_N8N_COMPOSE_OVERRIDE_REL,
|
|
"runtimePackagePath": ENGINE_N8N_RUNTIME_PACKAGE_PATH,
|
|
"topologyServices": ["n8n"],
|
|
}
|
|
for key, expected in exact_common.items():
|
|
if descriptor.get(key) != expected:
|
|
die(f"Engine n8n transition {key} mismatch")
|
|
if action == "activate":
|
|
expected_current = descriptor.get("expectedCurrent")
|
|
if (
|
|
expected_current != "verified_inactive"
|
|
and expected_current not in ENGINE_N8N_RELEASE_CATALOG_JSON_SHA256
|
|
):
|
|
die("Engine n8n activation expected-current release is not registered")
|
|
if expected_current == release_id:
|
|
die("Engine n8n activation target already equals expected current")
|
|
if descriptor.get("rollbackBaseline") != expected_current:
|
|
die("Engine n8n activation rollback baseline mismatch")
|
|
if descriptor.get("expectedNodeTypes") != list(ENGINE_N8N_NODE_TYPES):
|
|
die("Engine n8n activation node type set mismatch")
|
|
if descriptor.get("expectedCredentialTypes") != list(
|
|
engine_n8n_credential_types_for_release(release_id)
|
|
):
|
|
die("Engine n8n activation credential type set mismatch")
|
|
else:
|
|
if descriptor.get("expectedCurrent") != release_id:
|
|
die("Engine n8n rollback expected-current release mismatch")
|
|
if descriptor.get("rollbackBaseline") != "verified_inactive":
|
|
die("Engine n8n inactive rollback baseline mismatch")
|
|
if descriptor.get("expectedNodeTypes") != [] or descriptor.get("expectedCredentialTypes") != []:
|
|
die("Engine n8n rollback must select the inactive catalog")
|
|
return descriptor
|
|
|
|
|
|
def expected_engine_n8n_compose_override(descriptor):
|
|
health_script = (
|
|
"const http=require('http');const req=http.get('http://127.0.0.1:5678/healthz/readiness',"
|
|
"r=>{r.resume();process.exit(r.statusCode===200?0:1)});req.on('error',()=>process.exit(1));"
|
|
"req.setTimeout(4000,()=>{req.destroy();process.exit(1)});"
|
|
)
|
|
health_test = json.dumps(["CMD", "node", "-e", health_script], separators=(",", ":"))
|
|
mount_source = f"/volume2/nodedc-demo/{descriptor['sealedReleaseRelativePath']}"
|
|
return "\n".join((
|
|
"services:",
|
|
" n8n:",
|
|
f" image: {ENGINE_N8N_BASE_IMAGE}",
|
|
" platform: linux/amd64",
|
|
" pull_policy: never",
|
|
" environment:",
|
|
" N8N_USER_FOLDER: /home/node",
|
|
' N8N_COMMUNITY_PACKAGES_ENABLED: "true"',
|
|
' N8N_COMMUNITY_PACKAGES_PREVENT_LOADING: "false"',
|
|
' N8N_REINSTALL_MISSING_PACKAGES: "false"',
|
|
" volumes:",
|
|
f" - {mount_source}:{ENGINE_N8N_RUNTIME_PACKAGE_PATH}:ro",
|
|
" healthcheck:",
|
|
f" test: {health_test}",
|
|
" interval: 10s",
|
|
" timeout: 5s",
|
|
" retries: 30",
|
|
" start_period: 30s",
|
|
" labels:",
|
|
f" nodedc.n8n-private-extension.release: {descriptor['releaseId']}",
|
|
f" nodedc.n8n-private-extension.package-sha256: {descriptor['packageSha256']}",
|
|
"",
|
|
))
|
|
|
|
|
|
def validate_engine_n8n_catalog_payload(payload_dir, descriptor):
|
|
action = descriptor["action"]
|
|
nodes_path = payload_dir / ENGINE_N8N_NODES_CATALOG_REL
|
|
credentials_path = payload_dir / ENGINE_N8N_CREDENTIALS_CATALOG_REL
|
|
meta_path = payload_dir / ENGINE_N8N_SCHEMA_META_REL
|
|
nodes = read_strict_json(
|
|
nodes_path,
|
|
"Engine n8n node catalog",
|
|
max_bytes=32 * 1024 * 1024,
|
|
)
|
|
credentials = read_strict_json(
|
|
credentials_path,
|
|
"Engine n8n credential catalog",
|
|
max_bytes=4 * 1024 * 1024,
|
|
)
|
|
meta = read_strict_json(meta_path, "Engine n8n schema metadata")
|
|
if not isinstance(nodes, list) or not isinstance(credentials, list):
|
|
die("Engine n8n schema catalogs must be arrays")
|
|
private_nodes = [
|
|
item for item in nodes
|
|
if isinstance(item, dict) and str(item.get("name") or "").startswith("n8n-nodes-ndc.")
|
|
]
|
|
private_credentials = [
|
|
item for item in credentials
|
|
if isinstance(item, dict) and str(item.get("name") or "") in ENGINE_N8N_CREDENTIAL_TYPES
|
|
]
|
|
expected_node_types = list(ENGINE_N8N_NODE_TYPES) if action == "activate" else []
|
|
expected_credential_types = (
|
|
list(engine_n8n_credential_types_for_release(descriptor["releaseId"]))
|
|
if action == "activate"
|
|
else []
|
|
)
|
|
if [item.get("name") for item in private_nodes] != expected_node_types:
|
|
die("Engine n8n pinned node catalog exact set mismatch")
|
|
if [item.get("name") for item in private_credentials] != expected_credential_types:
|
|
die("Engine n8n pinned credential catalog exact set mismatch")
|
|
if any("usableAsTool" in item for item in private_nodes):
|
|
die("Engine n8n pinned node catalog would generate tool variants")
|
|
if action == "activate":
|
|
if len(nodes) != 437 or len(credentials) != 385 + len(expected_credential_types):
|
|
die("Engine n8n activation catalog count mismatch")
|
|
if any(not str(item.get("displayName") or "").startswith("NDC ") for item in private_nodes):
|
|
die("Engine n8n private node visible name mismatch")
|
|
if any(not str(item.get("displayName") or "").startswith("NDC ") for item in private_credentials):
|
|
die("Engine n8n private credential visible name mismatch")
|
|
private_catalog_text = json.dumps(
|
|
{"nodes": private_nodes, "credentials": private_credentials},
|
|
ensure_ascii=False,
|
|
).lower()
|
|
if "gelios" in private_catalog_text or "robot2b" in private_catalog_text:
|
|
die("provider business identity is forbidden in the Engine n8n extension catalog")
|
|
else:
|
|
if len(nodes) != 434 or len(credentials) != 385:
|
|
die("Engine n8n inactive baseline catalog count mismatch")
|
|
if not isinstance(meta, dict):
|
|
die("Engine n8n schema metadata must be an object")
|
|
if meta.get("n8nVersion") != ENGINE_N8N_VERSION:
|
|
die("Engine n8n schema metadata version mismatch")
|
|
if meta.get("nodeCount") != len(nodes) or meta.get("credentialCount") != len(credentials):
|
|
die("Engine n8n schema metadata count mismatch")
|
|
if action == "activate":
|
|
if meta.get("source") != f"n8n-core+n8n-nodes-ndc@{descriptor['packageVersion']}":
|
|
die("Engine n8n schema metadata source mismatch")
|
|
if sha256_file(payload_dir / ENGINE_N8N_ICON_REL) != ENGINE_N8N_ICON_SHA256:
|
|
die("Engine n8n light icon sha256 mismatch")
|
|
if sha256_file(payload_dir / ENGINE_N8N_DARK_ICON_REL) != ENGINE_N8N_DARK_ICON_SHA256:
|
|
die("Engine n8n dark icon sha256 mismatch")
|
|
release_hashes = ENGINE_N8N_RELEASE_CATALOG_JSON_SHA256.get(descriptor["releaseId"])
|
|
if release_hashes is None:
|
|
die("Engine n8n activation release catalog is not registered")
|
|
expected_catalog_hashes = (
|
|
(nodes, release_hashes["nodes"], "node"),
|
|
(credentials, release_hashes["credentials"], "credential"),
|
|
(meta, release_hashes["meta"], "metadata"),
|
|
)
|
|
else:
|
|
expected_catalog_hashes = (
|
|
(nodes, ENGINE_N8N_INACTIVE_NODES_CATALOG_JSON_SHA256, "node"),
|
|
(credentials, ENGINE_N8N_INACTIVE_CREDENTIALS_CATALOG_JSON_SHA256, "credential"),
|
|
(meta, ENGINE_N8N_INACTIVE_META_JSON_SHA256, "metadata"),
|
|
)
|
|
for catalog, expected_sha256, label in expected_catalog_hashes:
|
|
if sha256_json_value(catalog) != expected_sha256:
|
|
die(f"Engine n8n {action} {label} catalog sha256 mismatch")
|
|
|
|
|
|
def validate_engine_n8n_transition(payload_dir, entries):
|
|
descriptor = read_engine_n8n_transition_descriptor(payload_dir / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL)
|
|
activation_entries = [
|
|
ENGINE_N8N_TRANSITION_DESCRIPTOR_REL,
|
|
ENGINE_N8N_COMPOSE_OVERRIDE_REL,
|
|
ENGINE_N8N_NODES_CATALOG_REL,
|
|
ENGINE_N8N_CREDENTIALS_CATALOG_REL,
|
|
ENGINE_N8N_SCHEMA_META_REL,
|
|
ENGINE_N8N_ICON_REL,
|
|
ENGINE_N8N_DARK_ICON_REL,
|
|
]
|
|
rollback_entries = [
|
|
ENGINE_N8N_TRANSITION_DESCRIPTOR_REL,
|
|
ENGINE_N8N_NODES_CATALOG_REL,
|
|
ENGINE_N8N_CREDENTIALS_CATALOG_REL,
|
|
ENGINE_N8N_SCHEMA_META_REL,
|
|
]
|
|
expected_entries = activation_entries if descriptor["action"] == "activate" else rollback_entries
|
|
if entries != expected_entries:
|
|
die("Engine n8n transition files.txt exact set/order mismatch")
|
|
if descriptor["action"] == "activate":
|
|
override = payload_dir / ENGINE_N8N_COMPOSE_OVERRIDE_REL
|
|
try:
|
|
override_text = override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine n8n Compose override cannot be read")
|
|
if override_text != expected_engine_n8n_compose_override(descriptor):
|
|
die("Engine n8n Compose override mismatch")
|
|
if "N8N_CUSTOM_EXTENSIONS" in override_text or "CUSTOM." in override_text:
|
|
die("Engine n8n custom-extension loader is forbidden")
|
|
validate_engine_n8n_catalog_payload(payload_dir, descriptor)
|
|
return descriptor
|
|
|
|
|
|
def is_engine_node_intelligence_transition(component, entries):
|
|
if component != "engine" or entries is None:
|
|
return False
|
|
return tuple(entries) in (
|
|
ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
|
|
ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES,
|
|
)
|
|
|
|
|
|
def expected_engine_node_intelligence_compose_override():
|
|
return "\n".join((
|
|
"services:",
|
|
f" {ENGINE_NODE_INTELLIGENCE_SERVICE}:",
|
|
f" image: {ENGINE_NODE_INTELLIGENCE_IMAGE}",
|
|
" pull_policy: never",
|
|
" restart: unless-stopped",
|
|
f' user: "{ENGINE_NODE_INTELLIGENCE_RUNTIME_UID}:{ENGINE_NODE_INTELLIGENCE_RUNTIME_GID}"',
|
|
" read_only: true",
|
|
" environment:",
|
|
" HOME: /tmp",
|
|
" NODE_ENV: production",
|
|
" MCP_MODE: http",
|
|
' PORT: "3000"',
|
|
f" AUTH_TOKEN_FILE: {ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH}",
|
|
" NODE_DB_PATH: /app/data/nodes.db",
|
|
' REBUILD_ON_START: "false"',
|
|
' N8N_MCP_TELEMETRY_DISABLED: "true"',
|
|
" LOG_LEVEL: warn",
|
|
" volumes:",
|
|
" - type: bind",
|
|
f" source: {ENGINE_NODE_INTELLIGENCE_SECRET_FILE}",
|
|
f" target: {ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH}",
|
|
" read_only: true",
|
|
" expose:",
|
|
' - "3000"',
|
|
" tmpfs:",
|
|
" - /tmp:mode=1777",
|
|
" - /app/logs:mode=0755",
|
|
" cap_drop:",
|
|
" - ALL",
|
|
" security_opt:",
|
|
" - no-new-privileges:true",
|
|
" healthcheck:",
|
|
" test:",
|
|
" - CMD-SHELL",
|
|
" - curl -fsS http://127.0.0.1:3000/health >/dev/null",
|
|
" interval: 15s",
|
|
" timeout: 5s",
|
|
" retries: 10",
|
|
" start_period: 30s",
|
|
"",
|
|
" nodedc-backend:",
|
|
" depends_on:",
|
|
f" {ENGINE_NODE_INTELLIGENCE_SERVICE}:",
|
|
" condition: service_healthy",
|
|
" environment:",
|
|
f" ENGINE_NODE_INTELLIGENCE_MCP_URL: http://{ENGINE_NODE_INTELLIGENCE_SERVICE}:3000/mcp",
|
|
f" ENGINE_NODE_INTELLIGENCE_AUTH_TOKEN_FILE: {ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH}",
|
|
f" ENGINE_NODE_INTELLIGENCE_ASSET_ID: nodedc-node-intelligence@{ENGINE_NODE_INTELLIGENCE_RELEASE_ID.replace('-', '+', 1)}",
|
|
" volumes:",
|
|
" - type: bind",
|
|
f" source: {ENGINE_NODE_INTELLIGENCE_SECRET_FILE}",
|
|
f" target: {ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH}",
|
|
" read_only: true",
|
|
"",
|
|
))
|
|
|
|
|
|
def read_engine_node_intelligence_descriptor(path, label="Engine node-intelligence descriptor"):
|
|
descriptor = read_strict_json(path, label)
|
|
require_exact_json_keys(
|
|
descriptor,
|
|
(
|
|
"schemaVersion",
|
|
"action",
|
|
"releaseId",
|
|
"expectedCurrent",
|
|
"upstream",
|
|
"image",
|
|
"source",
|
|
"predecessor",
|
|
),
|
|
label,
|
|
)
|
|
if descriptor.get("schemaVersion") != "nodedc.engine-node-intelligence-transition/v1":
|
|
die("Engine node-intelligence transition schema mismatch")
|
|
action = descriptor.get("action")
|
|
if action not in ("activate", "rollback-inactive"):
|
|
die("Engine node-intelligence transition action mismatch")
|
|
if descriptor.get("releaseId") != ENGINE_NODE_INTELLIGENCE_RELEASE_ID:
|
|
die("Engine node-intelligence release mismatch")
|
|
require_exact_json_keys(
|
|
descriptor.get("upstream"),
|
|
("package", "version", "commit"),
|
|
f"{label} upstream",
|
|
)
|
|
if descriptor["upstream"] != {
|
|
"package": "n8n-mcp",
|
|
"version": "2.33.2",
|
|
"commit": ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT,
|
|
}:
|
|
die("Engine node-intelligence upstream pin mismatch")
|
|
require_exact_json_keys(
|
|
descriptor.get("predecessor"),
|
|
("gatewaySha256", "composeSha256", "backendRuntime"),
|
|
f"{label} predecessor",
|
|
)
|
|
predecessor = descriptor["predecessor"]
|
|
if predecessor.get("composeSha256") != ENGINE_NODE_INTELLIGENCE_PREDECESSOR_COMPOSE_SHA256:
|
|
die("Engine node-intelligence predecessor Compose mismatch")
|
|
if predecessor.get("backendRuntime") != "verified-derived-retry":
|
|
die("Engine node-intelligence backend predecessor mismatch")
|
|
|
|
if action == "activate":
|
|
if descriptor.get("expectedCurrent") != "inactive":
|
|
die("Engine node-intelligence activation expected-current mismatch")
|
|
if predecessor.get("gatewaySha256") != ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256:
|
|
die("Engine node-intelligence activation gateway predecessor mismatch")
|
|
require_exact_json_keys(
|
|
descriptor.get("image"),
|
|
(
|
|
"tag",
|
|
"archiveRelativePath",
|
|
"archiveSha256",
|
|
"configSha256",
|
|
"architecture",
|
|
"os",
|
|
),
|
|
f"{label} image",
|
|
)
|
|
image = descriptor["image"]
|
|
if (
|
|
image.get("tag") != ENGINE_NODE_INTELLIGENCE_IMAGE
|
|
or image.get("archiveRelativePath") != ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL
|
|
or not re.fullmatch(r"[a-f0-9]{64}", str(image.get("archiveSha256") or ""))
|
|
or not re.fullmatch(r"[a-f0-9]{64}", str(image.get("configSha256") or ""))
|
|
or image.get("architecture") != "amd64"
|
|
or image.get("os") != "linux"
|
|
):
|
|
die("Engine node-intelligence image descriptor mismatch")
|
|
require_exact_json_keys(
|
|
descriptor.get("source"),
|
|
(
|
|
"gatewaySha256",
|
|
"catalogSha256",
|
|
"upstreamClientSha256",
|
|
"upstreamProjectionSha256",
|
|
"composeOverrideSha256",
|
|
"readmeSha256",
|
|
),
|
|
f"{label} source",
|
|
)
|
|
for value in descriptor["source"].values():
|
|
if not re.fullmatch(r"[a-f0-9]{64}", str(value or "")):
|
|
die("Engine node-intelligence source digest is invalid")
|
|
else:
|
|
if descriptor.get("expectedCurrent") != ENGINE_NODE_INTELLIGENCE_RELEASE_ID:
|
|
die("Engine node-intelligence rollback expected-current mismatch")
|
|
require_exact_json_keys(
|
|
descriptor.get("image"),
|
|
("tag", "configSha256", "architecture", "os"),
|
|
f"{label} image",
|
|
)
|
|
image = descriptor["image"]
|
|
if (
|
|
image.get("tag") != ENGINE_NODE_INTELLIGENCE_IMAGE
|
|
or not re.fullmatch(r"[a-f0-9]{64}", str(image.get("configSha256") or ""))
|
|
or image.get("architecture") != "amd64"
|
|
or image.get("os") != "linux"
|
|
):
|
|
die("Engine node-intelligence rollback image descriptor mismatch")
|
|
require_exact_json_keys(
|
|
descriptor.get("source"),
|
|
("gatewaySha256", "readmeSha256"),
|
|
f"{label} source",
|
|
)
|
|
if descriptor["source"].get("gatewaySha256") != ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256:
|
|
die("Engine node-intelligence rollback gateway target mismatch")
|
|
if any(
|
|
not re.fullmatch(r"[a-f0-9]{64}", str(value or ""))
|
|
for value in descriptor["source"].values()
|
|
):
|
|
die("Engine node-intelligence rollback source digest is invalid")
|
|
if not re.fullmatch(r"[a-f0-9]{64}", str(predecessor.get("gatewaySha256") or "")):
|
|
die("Engine node-intelligence rollback predecessor gateway is invalid")
|
|
return descriptor
|
|
|
|
|
|
def validate_engine_node_intelligence_image_archive(path, descriptor):
|
|
image = descriptor["image"]
|
|
try:
|
|
archive_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die("Engine node-intelligence image archive is missing")
|
|
if stat.S_ISLNK(archive_stat.st_mode) or not stat.S_ISREG(archive_stat.st_mode):
|
|
die("Engine node-intelligence image archive is unsafe")
|
|
if archive_stat.st_size < 1024 or archive_stat.st_size > MAX_ARTIFACT_BYTES:
|
|
die("Engine node-intelligence image archive size is invalid")
|
|
if sha256_file(path) != image["archiveSha256"]:
|
|
die("Engine node-intelligence image archive sha256 mismatch")
|
|
|
|
names = set()
|
|
manifest_raw = None
|
|
config_raw = None
|
|
total = 0
|
|
try:
|
|
with tarfile.open(path, "r:") as archive:
|
|
members = archive.getmembers()
|
|
if not members or len(members) > MAX_MEMBER_COUNT:
|
|
die("Engine node-intelligence image archive member count is invalid")
|
|
for member in members:
|
|
validate_posix_path(member.name.rstrip("/"))
|
|
if member.name in names:
|
|
die(f"Engine node-intelligence image archive duplicate member: {member.name}")
|
|
names.add(member.name)
|
|
if not (member.isfile() or member.isdir()):
|
|
die(f"Engine node-intelligence image archive special member: {member.name}")
|
|
if member.isdir():
|
|
continue
|
|
if member.size > MAX_FILE_BYTES:
|
|
die(f"Engine node-intelligence image archive member too large: {member.name}")
|
|
total += member.size
|
|
if total > MAX_ARTIFACT_BYTES:
|
|
die("Engine node-intelligence image archive expanded size is too large")
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die(f"Engine node-intelligence image archive member unreadable: {member.name}")
|
|
data = source.read(MAX_FILE_BYTES + 1)
|
|
if len(data) != member.size:
|
|
die(f"Engine node-intelligence image archive member size mismatch: {member.name}")
|
|
if member.name.startswith("blobs/sha256/"):
|
|
digest = member.name.rsplit("/", 1)[-1]
|
|
if not re.fullmatch(r"[a-f0-9]{64}", digest):
|
|
die("Engine node-intelligence image blob name is invalid")
|
|
if hashlib.sha256(data).hexdigest() != digest:
|
|
die("Engine node-intelligence image blob digest mismatch")
|
|
elif member.name not in ("index.json", "manifest.json", "oci-layout"):
|
|
die(f"Engine node-intelligence image archive unexpected member: {member.name}")
|
|
if member.name == "manifest.json":
|
|
manifest_raw = data
|
|
if manifest_raw is None:
|
|
die("Engine node-intelligence image manifest is missing")
|
|
try:
|
|
manifest = json.loads(manifest_raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
die("Engine node-intelligence image manifest is invalid")
|
|
if not isinstance(manifest, list) or len(manifest) != 1:
|
|
die("Engine node-intelligence image manifest exact set mismatch")
|
|
record = manifest[0]
|
|
if not isinstance(record, dict) or set(record) != {"Config", "RepoTags", "Layers"}:
|
|
die("Engine node-intelligence image manifest record mismatch")
|
|
if record.get("RepoTags") != [ENGINE_NODE_INTELLIGENCE_IMAGE]:
|
|
die("Engine node-intelligence image tag mismatch")
|
|
config_name = record.get("Config")
|
|
expected_config_name = f"blobs/sha256/{image['configSha256']}"
|
|
if config_name != expected_config_name:
|
|
die("Engine node-intelligence image config digest mismatch")
|
|
try:
|
|
config_member = archive.getmember(config_name)
|
|
config_file = archive.extractfile(config_member)
|
|
config_raw = config_file.read(MAX_FILE_BYTES + 1) if config_file else None
|
|
except KeyError:
|
|
config_raw = None
|
|
except (tarfile.TarError, OSError):
|
|
die("Engine node-intelligence image archive cannot be read")
|
|
if config_raw is None:
|
|
die("Engine node-intelligence image config is missing")
|
|
try:
|
|
config = json.loads(config_raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
die("Engine node-intelligence image config is invalid")
|
|
if config.get("architecture") != "amd64" or config.get("os") != "linux":
|
|
die("Engine node-intelligence image platform mismatch")
|
|
runtime_config = config.get("config") or {}
|
|
labels = runtime_config.get("Labels") or {}
|
|
if labels.get("org.opencontainers.image.revision") != ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT:
|
|
die("Engine node-intelligence image revision label mismatch")
|
|
if runtime_config.get("Entrypoint") != ["/usr/local/bin/docker-entrypoint.sh"]:
|
|
die("Engine node-intelligence image entrypoint mismatch")
|
|
if runtime_config.get("Cmd") != ["node", "dist/mcp/index.js"]:
|
|
die("Engine node-intelligence image command mismatch")
|
|
env = runtime_config.get("Env") or []
|
|
if any(
|
|
str(value).startswith(("AUTH_TOKEN=", "N8N_API_URL=", "N8N_API_KEY="))
|
|
for value in env
|
|
):
|
|
die("Engine node-intelligence image contains runtime authority")
|
|
return image["configSha256"]
|
|
|
|
|
|
def validate_engine_node_intelligence_transition(payload_dir, entries):
|
|
service_root = payload_dir / ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL
|
|
descriptor = read_engine_node_intelligence_descriptor(
|
|
service_root / "activation.json"
|
|
)
|
|
expected_entries = (
|
|
ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES
|
|
if descriptor["action"] == "activate"
|
|
else ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES
|
|
)
|
|
if tuple(entries) != expected_entries:
|
|
die("Engine node-intelligence files.txt exact set/order mismatch")
|
|
gateway = payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
|
if sha256_file(gateway) != descriptor["source"]["gatewaySha256"]:
|
|
die("Engine node-intelligence gateway sha256 mismatch")
|
|
readme = service_root / "README.md"
|
|
if sha256_file(readme) != descriptor["source"]["readmeSha256"]:
|
|
die("Engine node-intelligence README sha256 mismatch")
|
|
|
|
if descriptor["action"] == "activate":
|
|
expected_service_files = {
|
|
"README.md",
|
|
"activation.json",
|
|
"docker-compose.immutable-runtime.yml",
|
|
"image/engine-node-intelligence.tar",
|
|
}
|
|
actual_service_files = {
|
|
child.relative_to(service_root).as_posix()
|
|
for child in service_root.rglob("*")
|
|
if child.is_file()
|
|
}
|
|
if actual_service_files != expected_service_files:
|
|
die("Engine node-intelligence service payload file set mismatch")
|
|
source_root = payload_dir / ENGINE_NODE_INTELLIGENCE_SOURCE_REL
|
|
expected_source_files = {
|
|
"catalog.js",
|
|
"upstreamMcpClient.js",
|
|
"upstreamProjection.js",
|
|
}
|
|
actual_source_files = {
|
|
child.relative_to(source_root).as_posix()
|
|
for child in source_root.rglob("*")
|
|
if child.is_file()
|
|
}
|
|
if actual_source_files != expected_source_files:
|
|
die("Engine node-intelligence source file set mismatch")
|
|
source_digests = {
|
|
"catalogSha256": sha256_file(source_root / "catalog.js"),
|
|
"upstreamClientSha256": sha256_file(source_root / "upstreamMcpClient.js"),
|
|
"upstreamProjectionSha256": sha256_file(source_root / "upstreamProjection.js"),
|
|
}
|
|
if any(descriptor["source"][key] != value for key, value in source_digests.items()):
|
|
die("Engine node-intelligence source digest mismatch")
|
|
override = service_root / "docker-compose.immutable-runtime.yml"
|
|
try:
|
|
override_text = override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine node-intelligence Compose override is unreadable")
|
|
if override_text != expected_engine_node_intelligence_compose_override():
|
|
die("Engine node-intelligence Compose override mismatch")
|
|
if sha256_file(override) != descriptor["source"]["composeOverrideSha256"]:
|
|
die("Engine node-intelligence Compose override sha256 mismatch")
|
|
gateway_text = gateway.read_text(encoding="utf-8")
|
|
for required in (
|
|
"engine_get_node_intelligence_status",
|
|
"engine_get_node_guidance",
|
|
"engine_validate_node_configuration",
|
|
"engine_validate_l2_deep",
|
|
):
|
|
if required not in gateway_text:
|
|
die(f"Engine node-intelligence gateway contract missing: {required}")
|
|
if not any(
|
|
version in gateway_text
|
|
for version in (
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.3.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.4.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.5.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.6.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.7.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.8.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.9.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.10.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.11.0'",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.12.0'",
|
|
)
|
|
):
|
|
die("Engine node-intelligence gateway MCP version is not registered")
|
|
validate_engine_node_intelligence_image_archive(
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL,
|
|
descriptor,
|
|
)
|
|
else:
|
|
expected_service_files = {"README.md", "activation.json"}
|
|
actual_service_files = {
|
|
child.relative_to(service_root).as_posix()
|
|
for child in service_root.rglob("*")
|
|
if child.is_file()
|
|
}
|
|
if actual_service_files != expected_service_files:
|
|
die("Engine node-intelligence rollback payload file set mismatch")
|
|
if sha256_file(gateway) != ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256:
|
|
die("Engine node-intelligence rollback gateway baseline mismatch")
|
|
return descriptor
|
|
|
|
|
|
def is_engine_mcp_control_plane_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_control_plane_payload(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES:
|
|
die("Engine MCP control-plane files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256:
|
|
die(f"Engine MCP control-plane target sha256 mismatch: {rel}")
|
|
|
|
gateway = (payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL).read_text(encoding="utf-8")
|
|
graph_route = (payload_dir / "nodedc-source/server/routes/ndcAgentMcp.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
installer = (
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs"
|
|
).read_text(encoding="utf-8")
|
|
package = read_strict_json(
|
|
payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json",
|
|
"Engine MCP Codex installer package",
|
|
)
|
|
if package.get("name") != "@nodedc/engine-codex-agent" or package.get("version") != "0.1.4":
|
|
die("Engine MCP Codex installer package identity mismatch")
|
|
if any(
|
|
marker not in gateway
|
|
for marker in (
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.5.0'",
|
|
"engine_open_l2_change_session",
|
|
"engine_get_l2_change_session",
|
|
"engine_close_l2_change_session",
|
|
"never_infer_expiry",
|
|
"three_identical_failures_without_new_evidence",
|
|
"engine_plan_data_product_read_grant",
|
|
"engine_apply_data_product_read_grant",
|
|
"engine_accept_data_product_read_grant",
|
|
"engine_rollback_data_product_read_grant",
|
|
)
|
|
):
|
|
die("Engine MCP bounded-change-session contract is incomplete")
|
|
if any(
|
|
marker not in graph_route
|
|
for marker in (
|
|
"update_node_no_effect",
|
|
"subworkflow_post_write_equality_failed",
|
|
"verifiedWrite: true",
|
|
"graphDigest: expectedDigest",
|
|
)
|
|
):
|
|
die("Engine MCP graph post-write equality contract is incomplete")
|
|
if any(
|
|
marker not in installer
|
|
for marker in (
|
|
"Do not interrupt the user after ordinary recoverable errors",
|
|
"Never infer that a provider or managed capability token expired",
|
|
"Three identical failures with no new evidence",
|
|
"engine_plan_data_product_*_grant",
|
|
"durable until an explicit rollback or revoke",
|
|
)
|
|
):
|
|
die("Engine MCP Codex bounded-autonomy policy is incomplete")
|
|
combined = "\n".join((gateway, graph_route, installer)).lower()
|
|
if "gelios" in combined or "robot2b" in combined:
|
|
die("provider identity is forbidden in Engine MCP control-plane source")
|
|
|
|
archive_path = payload_dir / "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.4.tgz"
|
|
expected_members = {
|
|
"package/bin/nodedc-engine-codex-agent.mjs": (
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs"
|
|
).read_bytes(),
|
|
"package/package.json": (
|
|
payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json"
|
|
).read_bytes(),
|
|
}
|
|
observed_members = {}
|
|
try:
|
|
with tarfile.open(archive_path, "r:gz") as archive:
|
|
for member in archive:
|
|
if not (member.isfile() or member.isdir()) or member.name.startswith("/"):
|
|
die("Engine MCP Codex installer archive member is unsafe")
|
|
if member.isfile():
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die("Engine MCP Codex installer archive member is unreadable")
|
|
observed_members[member.name] = source.read(MAX_FILE_BYTES + 1)
|
|
except (OSError, tarfile.TarError):
|
|
die("Engine MCP Codex installer archive is invalid")
|
|
if observed_members != expected_members:
|
|
die("Engine MCP Codex installer archive/source equality mismatch")
|
|
|
|
n8n_route = (payload_dir / "nodedc-source/server/routes/n8n.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
if "engineDataProductReadGrantN8nAdapter" not in n8n_route:
|
|
die("Engine MCP managed reader native adapter is missing")
|
|
if "ndcDataProductReaderApi" not in n8n_route or "ndc_edprb_" not in n8n_route:
|
|
die("Engine MCP managed reader credential contract is incomplete")
|
|
if "read_grant_must_be_durable" not in n8n_route:
|
|
die("Engine MCP managed reader durable lifecycle is missing")
|
|
reader_service = (
|
|
payload_dir / "nodedc-source/server/dataProductReadGrant/service.js"
|
|
).read_text(encoding="utf-8")
|
|
if (
|
|
"readerGrantLifetime: 'explicit-revoke'" not in reader_service
|
|
or "expiresAt: null" not in reader_service
|
|
or "ENGINE_READ_GRANT_TTL_DAYS" in reader_service
|
|
):
|
|
die("Engine MCP managed reader explicit-revoke lifecycle is incomplete")
|
|
publish_service = (
|
|
payload_dir / "nodedc-source/server/dataProductPublishGrant/service.js"
|
|
).read_text(encoding="utf-8")
|
|
if (
|
|
"writerGrantLifetime: 'explicit-revoke'" not in publish_service
|
|
or "migrateCurrentGrantToDurable" not in publish_service
|
|
or "expiresAt: null" not in publish_service
|
|
or "ENGINE_PUBLISH_GRANT_TTL_DAYS" in publish_service
|
|
):
|
|
die("Engine MCP managed writer explicit-revoke lifecycle is incomplete")
|
|
reader_override = payload_dir / ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL
|
|
try:
|
|
reader_override_text = reader_override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine MCP managed reader runtime override is unreadable")
|
|
if reader_override_text != expected_engine_data_product_read_grant_override():
|
|
die("Engine MCP managed reader runtime override mismatch")
|
|
agent_store = (payload_dir / "nodedc-source/server/engineAgents/store.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
for scope in (
|
|
"'engine:l2:data-product-read-grant:plan'",
|
|
"'engine:l2:data-product-read-grant:write'",
|
|
):
|
|
if scope not in agent_store:
|
|
die(f"Engine MCP managed reader scope is missing: {scope}")
|
|
|
|
descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL,
|
|
"Engine MCP control-plane node-intelligence descriptor",
|
|
)
|
|
if (
|
|
descriptor.get("action") != "activate"
|
|
or descriptor["source"].get("gatewaySha256")
|
|
!= ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
|
|
or descriptor["source"].get("upstreamProjectionSha256")
|
|
!= ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js"
|
|
]
|
|
):
|
|
die("Engine MCP control-plane descriptor target mismatch")
|
|
return descriptor
|
|
|
|
|
|
def preflight_engine_mcp_control_plane_predecessor(payload_dir):
|
|
candidate = validate_engine_mcp_control_plane_payload(
|
|
payload_dir,
|
|
ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES,
|
|
)
|
|
installed = current_engine_node_intelligence_descriptor()
|
|
validate_installed_engine_node_intelligence_source(installed)
|
|
if installed is None or installed.get("action") != "activate":
|
|
die("Engine MCP control-plane requires active node intelligence")
|
|
expected_candidate = json.loads(json.dumps(installed))
|
|
expected_candidate["source"]["gatewaySha256"] = (
|
|
ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
|
|
)
|
|
expected_candidate["source"]["upstreamProjectionSha256"] = (
|
|
ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js"
|
|
]
|
|
)
|
|
if candidate != expected_candidate:
|
|
die("Engine MCP control-plane descriptor crosses node-intelligence source boundary")
|
|
root = component_root("engine")
|
|
for rel, expected_sha256 in ENGINE_MCP_CONTROL_PLANE_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256:
|
|
die(f"Engine MCP control-plane predecessor drift detected: {rel}")
|
|
for rel in ENGINE_MCP_CONTROL_PLANE_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(f"Engine MCP control-plane new path already exists: {rel}")
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine MCP control-plane requires the active immutable backend runtime")
|
|
return {
|
|
"descriptor": candidate,
|
|
"predecessor_gateway_sha256": installed["source"]["gatewaySha256"],
|
|
"target_gateway_sha256": candidate["source"]["gatewaySha256"],
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_control_plane_runtime():
|
|
root = component_root("engine")
|
|
descriptor = validate_engine_mcp_control_plane_payload(
|
|
root,
|
|
ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES,
|
|
)
|
|
installed = current_engine_node_intelligence_descriptor()
|
|
if installed != descriptor:
|
|
die("Engine MCP control-plane installed descriptor equality failed")
|
|
validate_installed_engine_node_intelligence_source(installed)
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine MCP control-plane backend runtime acceptance failed")
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const module=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const store=await import('file:///app/server/engineAgents/store.js');
|
|
const names=new Set(module.engineAgentTools.map((tool)=>tool.name));
|
|
const required=['engine_plan_data_product_read_grant','engine_apply_data_product_read_grant','engine_accept_data_product_read_grant','engine_rollback_data_product_read_grant'];
|
|
const scopes=['engine:l2:data-product-read-grant:plan','engine:l2:data-product-read-grant:write'];
|
|
if(!required.every((name)=>names.has(name))||!scopes.every((scope)=>store.ENGINE_AGENT_SCOPES.includes(scope)))process.exit(2);
|
|
process.stdout.write('engine-mcp-reader-grant:0.5.0:'+required.length+':'+scopes.length);
|
|
""".strip(),
|
|
),
|
|
"Engine MCP managed reader capability",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
if live != "engine-mcp-reader-grant:0.5.0:4:2":
|
|
die("Engine MCP managed reader live capability acceptance mismatch")
|
|
return {
|
|
"gateway_sha256": descriptor["source"]["gatewaySha256"],
|
|
"backend_mode": backend["mode"],
|
|
"reader_grant": live,
|
|
}
|
|
|
|
|
|
def is_engine_mcp_ontology_sdk_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_ontology_sdk_payload(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES:
|
|
die("Engine MCP Ontology/SDK files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256:
|
|
die(f"Engine MCP Ontology/SDK target sha256 mismatch: {rel}")
|
|
|
|
gateway = (payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL).read_text(
|
|
encoding="utf-8"
|
|
)
|
|
installer_path = (
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs"
|
|
)
|
|
installer = installer_path.read_text(encoding="utf-8")
|
|
package_path = payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json"
|
|
package = read_strict_json(package_path, "Engine MCP Codex installer package")
|
|
if package.get("name") != "@nodedc/engine-codex-agent" or package.get("version") != "0.1.5":
|
|
die("Engine MCP Ontology/SDK installer package identity mismatch")
|
|
if any(
|
|
marker not in gateway
|
|
for marker in (
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.6.0'",
|
|
"authenticateEngineAgentOntologyToken",
|
|
"apiRouter.post('/ontology-mcp'",
|
|
"process.env.NODEDC_INTERNAL_ACCESS_TOKEN",
|
|
"serverName: 'nodedc-engine-agent'",
|
|
"serverName: 'nodedc_ontology'",
|
|
)
|
|
):
|
|
die("Engine MCP separate Ontology gateway contract is incomplete")
|
|
if any(
|
|
marker not in installer
|
|
for marker in (
|
|
"const ENGINE_SERVER_NAME = 'nodedc-engine-agent'",
|
|
"const ONTOLOGY_SERVER_NAME = 'nodedc_ontology'",
|
|
"Ontology tools/catalog are never embedded or multiplexed into Engine MCP",
|
|
"version: '0.1.5'",
|
|
)
|
|
):
|
|
die("Engine MCP separate Ontology installer contract is incomplete")
|
|
|
|
archive_path = payload_dir / "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz"
|
|
expected_members = {
|
|
"package/bin/nodedc-engine-codex-agent.mjs": installer_path.read_bytes(),
|
|
"package/package.json": package_path.read_bytes(),
|
|
}
|
|
observed_members = {}
|
|
try:
|
|
with tarfile.open(archive_path, "r:gz") as archive:
|
|
for member in archive:
|
|
if not (member.isfile() or member.isdir()) or member.name.startswith("/"):
|
|
die("Engine MCP Ontology/SDK installer archive member is unsafe")
|
|
if member.isfile():
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die("Engine MCP Ontology/SDK installer archive member is unreadable")
|
|
observed_members[member.name] = source.read(MAX_FILE_BYTES + 1)
|
|
except (OSError, tarfile.TarError):
|
|
die("Engine MCP Ontology/SDK installer archive is invalid")
|
|
if observed_members != expected_members:
|
|
die("Engine MCP Ontology/SDK installer archive/source equality mismatch")
|
|
|
|
catalog = read_strict_json(
|
|
payload_dir / "nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"Engine provider security catalog",
|
|
)
|
|
packages = catalog.get("packages") if isinstance(catalog, dict) else None
|
|
if not isinstance(packages, list) or len(packages) != 1:
|
|
die("Engine provider security catalog must contain one active package")
|
|
provider = packages[0]
|
|
capabilities = provider.get("capabilities") if isinstance(provider, dict) else None
|
|
capability = capabilities[0] if isinstance(capabilities, list) and len(capabilities) == 1 else None
|
|
request = capability.get("request") if isinstance(capability, dict) else None
|
|
if (
|
|
provider.get("id") != "gelios.provider.v2"
|
|
or provider.get("version") != "2.0.0"
|
|
or (provider.get("providerCredential") or {}).get("credentialType") != "httpQueryAuth"
|
|
or not isinstance(request, dict)
|
|
or request.get("method") != "GET"
|
|
or request.get("url") != "https://admin.geliospro.com/sdk/?svc=get_units¶ms=%7B%7D"
|
|
):
|
|
die("Engine Gelios SDK/query-auth security projection mismatch")
|
|
resolver = (
|
|
payload_dir / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js"
|
|
).read_text(encoding="utf-8")
|
|
if "new Set(['httpBearerAuth', 'httpQueryAuth'])" not in resolver:
|
|
die("Engine provider credential type allowlist mismatch")
|
|
store = (payload_dir / "nodedc-source/server/engineAgents/store.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
if any(
|
|
marker not in store
|
|
for marker in (
|
|
"const STORE_VERSION = 3",
|
|
"ontologyTokenHash",
|
|
"`ndc_eao_${",
|
|
"authenticateEngineAgentOntologyToken",
|
|
)
|
|
):
|
|
die("Engine separate Ontology token store migration is incomplete")
|
|
|
|
descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL,
|
|
"Engine MCP Ontology/SDK node-intelligence descriptor",
|
|
)
|
|
if (
|
|
descriptor.get("action") != "activate"
|
|
or descriptor["source"].get("gatewaySha256")
|
|
!= ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
|
|
or descriptor["source"].get("upstreamProjectionSha256")
|
|
!= ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js"
|
|
]
|
|
):
|
|
die("Engine MCP Ontology/SDK descriptor target mismatch")
|
|
return descriptor
|
|
|
|
|
|
def preflight_engine_mcp_ontology_sdk_predecessor(payload_dir):
|
|
candidate = validate_engine_mcp_ontology_sdk_payload(
|
|
payload_dir,
|
|
ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES,
|
|
)
|
|
installed = current_engine_node_intelligence_descriptor()
|
|
validate_installed_engine_node_intelligence_source(installed)
|
|
if installed is None or installed.get("action") != "activate":
|
|
die("Engine MCP Ontology/SDK requires active node intelligence")
|
|
expected_candidate = json.loads(json.dumps(installed))
|
|
expected_candidate["source"]["gatewaySha256"] = (
|
|
ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
|
|
)
|
|
if candidate != expected_candidate:
|
|
die("Engine MCP Ontology/SDK descriptor crosses node-intelligence source boundary")
|
|
root = component_root("engine")
|
|
for rel, expected_sha256 in ENGINE_MCP_ONTOLOGY_SDK_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256:
|
|
die(f"Engine MCP Ontology/SDK predecessor drift detected: {rel}")
|
|
for rel in ENGINE_MCP_ONTOLOGY_SDK_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(f"Engine MCP Ontology/SDK new path already exists: {rel}")
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine MCP Ontology/SDK requires the active immutable backend runtime")
|
|
return {
|
|
"descriptor": candidate,
|
|
"predecessor_gateway_sha256": installed["source"]["gatewaySha256"],
|
|
"target_gateway_sha256": candidate["source"]["gatewaySha256"],
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_ontology_sdk_runtime():
|
|
root = component_root("engine")
|
|
descriptor = validate_engine_mcp_ontology_sdk_payload(
|
|
root,
|
|
ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES,
|
|
)
|
|
installed = current_engine_node_intelligence_descriptor()
|
|
if installed != descriptor:
|
|
die("Engine MCP Ontology/SDK installed descriptor equality failed")
|
|
validate_installed_engine_node_intelligence_source(installed)
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine MCP Ontology/SDK backend runtime acceptance failed")
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const store=await import('file:///app/server/engineAgents/store.js');
|
|
const catalog=JSON.parse(await (await import('node:fs/promises')).readFile('/app/server/assets/provider-packages/v1/catalog.json','utf8'));
|
|
const provider=catalog.packages?.[0];
|
|
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.6.0')process.exit(2);
|
|
if(typeof store.authenticateEngineAgentOntologyToken!=='function')process.exit(3);
|
|
if(provider?.id!=='gelios.provider.v2'||provider?.providerCredential?.credentialType!=='httpQueryAuth')process.exit(4);
|
|
process.stdout.write('engine-mcp-ontology-sdk:0.6.0:0.1.5:gelios.provider.v2:httpQueryAuth');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP Ontology/SDK capability",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = "engine-mcp-ontology-sdk:0.6.0:0.1.5:gelios.provider.v2:httpQueryAuth"
|
|
if live != expected:
|
|
die("Engine MCP Ontology/SDK live capability acceptance mismatch")
|
|
return {
|
|
"gateway_sha256": descriptor["source"]["gatewaySha256"],
|
|
"backend_mode": backend["mode"],
|
|
"capability": live,
|
|
}
|
|
|
|
|
|
def is_engine_mcp_autonomy_provider_v5_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_autonomy_provider_v5_payload(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES:
|
|
die("Engine MCP autonomy/provider v5 files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256:
|
|
die(f"Engine MCP autonomy/provider v5 target sha256 mismatch: {rel}")
|
|
|
|
gateway = (payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL).read_text(
|
|
encoding="utf-8"
|
|
)
|
|
installer_path = (
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs"
|
|
)
|
|
installer = installer_path.read_text(encoding="utf-8")
|
|
package_path = payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json"
|
|
package = read_strict_json(package_path, "Engine MCP Codex installer package")
|
|
if package.get("name") != "@nodedc/engine-codex-agent" or package.get("version") != "0.1.6":
|
|
die("Engine MCP autonomy/provider v5 installer package identity mismatch")
|
|
if any(
|
|
marker not in gateway
|
|
for marker in (
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.6.0'",
|
|
"authenticateEngineAgentOntologyToken",
|
|
"apiRouter.post('/ontology-mcp'",
|
|
"'nodedc-engine-codex-agent-0.1.6.tgz'",
|
|
)
|
|
):
|
|
die("Engine MCP autonomy/provider v5 gateway contract is incomplete")
|
|
if any(
|
|
marker not in installer
|
|
for marker in (
|
|
"MCP tool availability establishes capability authority",
|
|
"the user's requested objective establishes intent scope",
|
|
"machine safety barrier, not a permission ceremony",
|
|
"Never repeat an identical read, validation, apply or run",
|
|
"Three identical failures with no new evidence or state change are a critical stop",
|
|
"const ONTOLOGY_SERVER_NAME = 'nodedc_ontology'",
|
|
"version: '0.1.6'",
|
|
)
|
|
) or "only with explicit user confirmation" in installer:
|
|
die("Engine MCP capability-scoped autonomy policy is incomplete")
|
|
|
|
archive_path = payload_dir / "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz"
|
|
expected_members = {
|
|
"package/bin/nodedc-engine-codex-agent.mjs": installer_path.read_bytes(),
|
|
"package/package.json": package_path.read_bytes(),
|
|
}
|
|
observed_members = {}
|
|
try:
|
|
with tarfile.open(archive_path, "r:gz") as archive:
|
|
for member in archive:
|
|
if not (member.isfile() or member.isdir()) or member.name.startswith("/"):
|
|
die("Engine MCP autonomy/provider v5 installer archive member is unsafe")
|
|
if member.isfile():
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die("Engine MCP autonomy/provider v5 installer archive member is unreadable")
|
|
observed_members[member.name] = source.read(MAX_FILE_BYTES + 1)
|
|
except (OSError, tarfile.TarError):
|
|
die("Engine MCP autonomy/provider v5 installer archive is invalid")
|
|
if observed_members != expected_members:
|
|
die("Engine MCP autonomy/provider v5 installer archive/source equality mismatch")
|
|
|
|
catalog = read_strict_json(
|
|
payload_dir / "nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"Engine provider security catalog",
|
|
)
|
|
packages = catalog.get("packages") if isinstance(catalog, dict) else None
|
|
if (
|
|
catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1"
|
|
or not isinstance(packages, list)
|
|
or [item.get("id") for item in packages if isinstance(item, dict)]
|
|
!= ["gelios.provider.v4", "gelios.provider.v5"]
|
|
):
|
|
die("Engine Gelios provider v4/v5 catalog identity mismatch")
|
|
expected_products = {
|
|
"gelios.provider.v4": "fleet.positions.current.v3",
|
|
"gelios.provider.v5": "fleet.positions.current.v4",
|
|
}
|
|
expected_requests = [
|
|
("gelios.monitoring_config.current.read", "https://api.geliospro.com/api/v1/users/me/monitoring-config"),
|
|
("gelios.units.current.read", "https://api.geliospro.com/api/v1/units?incltrip=true"),
|
|
]
|
|
for provider in packages:
|
|
provider_id = provider.get("id")
|
|
credential = provider.get("providerCredential") or {}
|
|
capabilities = provider.get("capabilities")
|
|
observed_requests = [
|
|
(item.get("id"), (item.get("request") or {}).get("url"))
|
|
for item in capabilities
|
|
] if isinstance(capabilities, list) else None
|
|
if (
|
|
provider_id not in expected_products
|
|
or credential.get("credentialType") != "ndcProviderRotatingAccessApi"
|
|
or credential.get("authModeId") != "gelios.rest-rotating-bearer.v3"
|
|
or observed_requests != expected_requests
|
|
or any(item.get("dataProductIds") != [expected_products[provider_id]] for item in capabilities)
|
|
):
|
|
die("Engine Gelios provider v4/v5 security projection mismatch")
|
|
|
|
store = (payload_dir / "nodedc-source/server/engineAgents/store.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
if any(
|
|
marker not in store
|
|
for marker in (
|
|
"const STORE_VERSION = 3",
|
|
"ontologyTokenHash",
|
|
"authenticateEngineAgentOntologyToken",
|
|
)
|
|
):
|
|
die("Engine MCP autonomy/provider v5 Ontology token store migration is incomplete")
|
|
|
|
descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL,
|
|
"Engine MCP autonomy/provider v5 node-intelligence descriptor",
|
|
)
|
|
if (
|
|
descriptor.get("action") != "activate"
|
|
or descriptor["source"].get("gatewaySha256")
|
|
!= ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
|
|
or descriptor["source"].get("upstreamProjectionSha256")
|
|
!= ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js"
|
|
]
|
|
):
|
|
die("Engine MCP autonomy/provider v5 descriptor target mismatch")
|
|
return descriptor
|
|
|
|
|
|
def preflight_engine_mcp_autonomy_provider_v5_predecessor(payload_dir):
|
|
candidate = validate_engine_mcp_autonomy_provider_v5_payload(
|
|
payload_dir,
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES,
|
|
)
|
|
installed = current_engine_node_intelligence_descriptor()
|
|
validate_installed_engine_node_intelligence_source(installed)
|
|
if installed is None or installed.get("action") != "activate":
|
|
die("Engine MCP autonomy/provider v5 requires active node intelligence")
|
|
expected_candidate = json.loads(json.dumps(installed))
|
|
expected_candidate["source"]["gatewaySha256"] = (
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
|
|
)
|
|
if candidate != expected_candidate:
|
|
die("Engine MCP autonomy/provider v5 crosses node-intelligence source boundary")
|
|
root = component_root("engine")
|
|
for rel, expected_sha256 in ENGINE_MCP_AUTONOMY_PROVIDER_V5_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256:
|
|
die(f"Engine MCP autonomy/provider v5 predecessor drift detected: {rel}")
|
|
for rel in ENGINE_MCP_AUTONOMY_PROVIDER_V5_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(f"Engine MCP autonomy/provider v5 new path already exists: {rel}")
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine MCP autonomy/provider v5 requires the active immutable backend runtime")
|
|
return {
|
|
"descriptor": candidate,
|
|
"predecessor_gateway_sha256": installed["source"]["gatewaySha256"],
|
|
"target_gateway_sha256": candidate["source"]["gatewaySha256"],
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def engine_mcp_autonomy_provider_v5_installed_state():
|
|
root = component_root("engine")
|
|
target = all(
|
|
(root / rel).is_file() and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256.items()
|
|
)
|
|
descriptor = current_engine_node_intelligence_descriptor()
|
|
if descriptor is not None:
|
|
validate_installed_engine_node_intelligence_source(descriptor)
|
|
if target and descriptor and descriptor.get("source", {}).get("gatewaySha256") == (
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
|
|
):
|
|
return "target"
|
|
predecessor = all(
|
|
(root / rel).is_file() and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_MCP_AUTONOMY_PROVIDER_V5_PREDECESSOR_SHA256.items()
|
|
)
|
|
new_paths_absent = all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_AUTONOMY_PROVIDER_V5_NEW_PATHS
|
|
)
|
|
if predecessor and new_paths_absent and descriptor and descriptor.get("source", {}).get("gatewaySha256") == (
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_PREDECESSOR_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
|
|
):
|
|
return "predecessor"
|
|
die("Engine MCP autonomy/provider v5 installed state is neither target nor rollback predecessor")
|
|
|
|
|
|
def accept_engine_mcp_autonomy_provider_v5_runtime():
|
|
root = component_root("engine")
|
|
descriptor = validate_engine_mcp_autonomy_provider_v5_payload(
|
|
root,
|
|
ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES,
|
|
)
|
|
installed = current_engine_node_intelligence_descriptor()
|
|
if installed != descriptor:
|
|
die("Engine MCP autonomy/provider v5 installed descriptor equality failed")
|
|
validate_installed_engine_node_intelligence_source(installed)
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine MCP autonomy/provider v5 backend runtime acceptance failed")
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const store=await import('file:///app/server/engineAgents/store.js');
|
|
const fs=await import('node:fs/promises');
|
|
const catalog=JSON.parse(await fs.readFile('/app/server/assets/provider-packages/v1/catalog.json','utf8'));
|
|
const pkg=JSON.parse(await fs.readFile('/app/server/assets/engine-agent-npm/package.json','utf8'));
|
|
const ids=catalog.packages?.map((item)=>item.id).join(',');
|
|
const target=catalog.packages?.find((item)=>item.id==='gelios.provider.v5');
|
|
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.6.0')process.exit(2);
|
|
if(typeof store.authenticateEngineAgentOntologyToken!=='function')process.exit(3);
|
|
if(pkg.version!=='0.1.6'||ids!=='gelios.provider.v4,gelios.provider.v5')process.exit(4);
|
|
if(target?.capabilities?.some((item)=>item.dataProductIds?.[0]!=='fleet.positions.current.v4'))process.exit(5);
|
|
process.stdout.write('engine-mcp-autonomy-provider-v5:0.6.0:0.1.6:'+ids);
|
|
""".strip(),
|
|
),
|
|
"Engine MCP autonomy/provider v5 capability",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = "engine-mcp-autonomy-provider-v5:0.6.0:0.1.6:gelios.provider.v4,gelios.provider.v5"
|
|
if live != expected:
|
|
die("Engine MCP autonomy/provider v5 live capability acceptance mismatch")
|
|
return {
|
|
"gateway_sha256": descriptor["source"]["gatewaySha256"],
|
|
"backend_mode": backend["mode"],
|
|
"capability": live,
|
|
}
|
|
|
|
|
|
def is_engine_provider_security_catalog_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def validate_engine_provider_security_catalog_payload(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES:
|
|
die("Engine provider security catalog files.txt exact set/order mismatch")
|
|
catalog_path = payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL
|
|
if (
|
|
catalog_path.is_symlink()
|
|
or not catalog_path.is_file()
|
|
or sha256_file(catalog_path) != ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256
|
|
):
|
|
die("Engine provider security catalog target sha256 mismatch")
|
|
catalog = read_strict_json(catalog_path, "Engine provider security catalog")
|
|
packages = catalog.get("packages") if isinstance(catalog, dict) else None
|
|
package_ids = [item.get("id") for item in packages if isinstance(item, dict)] \
|
|
if isinstance(packages, list) else None
|
|
provider = next(
|
|
(
|
|
item for item in packages
|
|
if isinstance(item, dict) and item.get("id") == "gelios.provider.v8"
|
|
),
|
|
None,
|
|
) if isinstance(packages, list) else None
|
|
previous_provider = next(
|
|
(
|
|
item for item in packages
|
|
if isinstance(item, dict) and item.get("id") == "gelios.provider.v7"
|
|
),
|
|
None,
|
|
) if isinstance(packages, list) else None
|
|
capabilities = provider.get("capabilities") if isinstance(provider, dict) else None
|
|
capability = capabilities[0] if isinstance(capabilities, list) and len(capabilities) == 1 else None
|
|
request = capability.get("request") if isinstance(capability, dict) else None
|
|
credential = provider.get("providerCredential") if isinstance(provider, dict) else None
|
|
publisher = provider.get("publisher") if isinstance(provider, dict) else None
|
|
if (
|
|
catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1"
|
|
or package_ids != [
|
|
"gelios.provider.v1",
|
|
"gelios.provider.v4",
|
|
"gelios.provider.v5",
|
|
"moscow-department-of-transport.pmd-slow-zones.v1",
|
|
"gelios.provider.v7",
|
|
"gelios.provider.v8",
|
|
]
|
|
or not isinstance(provider, dict)
|
|
or provider.get("version") != "8.0.0"
|
|
or provider.get("providerId") != "gelios"
|
|
or credential != {
|
|
"authModeId": "gelios.rest-rotating-bearer.v3",
|
|
"credentialType": "ndcProviderRotatingAccessApi",
|
|
}
|
|
or not isinstance(capability, dict)
|
|
or capability.get("id") != "gelios.units.current.read"
|
|
or capability.get("classification") != "read"
|
|
or capability.get("status") != "implemented"
|
|
or request != {
|
|
"method": "GET",
|
|
"url": (
|
|
"https://api.geliospro.com/api/v1/units?incltrip=true&inclcntrs=true"
|
|
"&inclsnsrs=true&incllsv=true"
|
|
),
|
|
}
|
|
or capability.get("dataProductIds") != ["fleet.units.profile.current.v1"]
|
|
or publisher != {
|
|
"nodeType": "n8n-nodes-ndc.ndcDataProductPublish",
|
|
"credentialType": "ndcDataProductWriterApi",
|
|
}
|
|
or previous_provider != {
|
|
"id": "gelios.provider.v7",
|
|
"version": "7.0.0",
|
|
"providerId": "gelios",
|
|
"providerCredential": {
|
|
"authModeId": "gelios.rest-rotating-bearer.v3",
|
|
"credentialType": "ndcProviderRotatingAccessApi",
|
|
},
|
|
"capabilities": [
|
|
{
|
|
"id": "gelios.units.current.read",
|
|
"classification": "read",
|
|
"status": "implemented",
|
|
"request": {
|
|
"method": "GET",
|
|
"url": (
|
|
"https://api.geliospro.com/api/v1/units?incltrip=true"
|
|
"&inclcntrs=true&inclsnsrs=true&incllsv=true"
|
|
),
|
|
},
|
|
"dataProductIds": ["fleet.positions.current.v5"],
|
|
},
|
|
],
|
|
"publisher": {
|
|
"nodeType": "n8n-nodes-ndc.ndcDataProductPublish",
|
|
"credentialType": "ndcDataProductWriterApi",
|
|
},
|
|
}
|
|
):
|
|
die("Engine provider security catalog v7/v8 projection mismatch")
|
|
return catalog
|
|
|
|
|
|
def preflight_engine_provider_security_catalog_predecessor():
|
|
catalog_path = component_root("engine") / ENGINE_PROVIDER_SECURITY_CATALOG_REL
|
|
try:
|
|
path_stat = catalog_path.lstat()
|
|
except FileNotFoundError:
|
|
die("Engine provider security catalog predecessor is missing")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die("Engine provider security catalog predecessor is unsafe")
|
|
actual_sha256 = sha256_file(catalog_path)
|
|
if actual_sha256 != ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256:
|
|
die(
|
|
"Engine provider security catalog predecessor drift detected: "
|
|
f"expected={ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine provider security catalog requires the active immutable backend")
|
|
return {"catalog_sha256": actual_sha256, "backend_mode": backend["mode"]}
|
|
|
|
|
|
def accept_engine_provider_security_catalog_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_provider_security_catalog_payload(
|
|
root,
|
|
ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js');
|
|
const catalog=await module.loadProviderSecurityCatalog();
|
|
const validation=module.validateProviderSecurityCatalog(catalog);
|
|
const provider=catalog.packages?.find((item)=>item.id==='gelios.provider.v8');
|
|
const capability=provider?.capabilities?.[0];
|
|
const slot='ndcProviderRotatingAccessApi';
|
|
const credential={id:'native-provider',name:'provider',nodeDcCredentialId:'provider-ref'};
|
|
const policy={kind:'httpRequest',allowedHosts:['api.geliospro.com'],requireHttps:true,disableRedirects:true};
|
|
const queryParameters={parameters:[{name:'incltrip',value:'true'},{name:'inclcntrs',value:'true'},{name:'inclsnsrs',value:'true'},{name:'incllsv',value:'true'}]};
|
|
const source={id:'units',data:{n8n:{id:'units',name:'units',type:'n8n-nodes-base.httpRequest',parameters:{method:'GET',url:'https://api.geliospro.com/api/v1/units',sendQuery:true,specifyQuery:'keypair',queryParameters},credentials:{[slot]:credential}},nodedcAgentCredentialPolicies:{[slot]:policy}}};
|
|
const profilePublish={id:'profile-publish',data:{n8n:{id:'profile-publish',name:'profile-publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.units.profile.current.v1'},credentials:{}}}};
|
|
const positionsPublish={id:'positions-publish',data:{n8n:{id:'positions-publish',name:'positions-publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.positions.current.v5'},credentials:{}}}};
|
|
const profileGraph={nodes:[source,profilePublish],edges:[{source:'units',target:'profile-publish'}]};
|
|
const positionsGraph={nodes:[source,positionsPublish],edges:[{source:'units',target:'positions-publish'}]};
|
|
const profileResolved=module.resolveProviderConnectionFromGraph({graph:profileGraph,targetNodeId:'profile-publish',catalog});
|
|
const positionsResolved=module.resolveProviderConnectionFromGraph({graph:positionsGraph,targetNodeId:'positions-publish',catalog});
|
|
const drift=structuredClone(profileGraph);drift.nodes[0].data.n8n.parameters.queryParameters.parameters.pop();
|
|
const rejected=module.resolveProviderConnectionFromGraph({graph:drift,targetNodeId:'profile-publish',catalog});
|
|
if(!validation.ok||provider?.providerCredential?.credentialType!==slot||capability?.request?.url!=='https://api.geliospro.com/api/v1/units?incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true'||capability?.dataProductIds?.[0]!=='fleet.units.profile.current.v1'||!profileResolved.ok||profileResolved.descriptor?.packageId!=='gelios.provider.v8'||profileResolved.descriptor?.providerCredentialRef!=='provider-ref'||!positionsResolved.ok||positionsResolved.descriptor?.packageId!=='gelios.provider.v7'||positionsResolved.descriptor?.dataProductId!=='fleet.positions.current.v5'||rejected.blockers?.join(',')!=='publish_grant_provider_request_not_exact')process.exit(2);
|
|
process.stdout.write('engine-provider-catalog:gelios.provider.v7:fleet.positions.current.v5|gelios.provider.v8:fleet.units.profile.current.v1');
|
|
""".strip(),
|
|
),
|
|
"Engine provider security catalog v8 unit profile",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-provider-catalog:gelios.provider.v7:fleet.positions.current.v5|"
|
|
"gelios.provider.v8:fleet.units.profile.current.v1"
|
|
)
|
|
if live != expected:
|
|
die("Engine provider security catalog live acceptance mismatch")
|
|
return {"catalog_sha256": ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256, "live": live}
|
|
|
|
|
|
def accept_engine_composite_provider_runtime():
|
|
catalog_path = component_root("engine") / ENGINE_PROVIDER_SECURITY_CATALOG_REL
|
|
if (
|
|
catalog_path.is_symlink()
|
|
or not catalog_path.is_file()
|
|
or sha256_file(catalog_path)
|
|
!= ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256
|
|
):
|
|
return None
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js');
|
|
const catalog=await module.loadProviderSecurityCatalog();
|
|
const validation=module.validateProviderSecurityCatalog(catalog);
|
|
const credential={id:'native-provider',name:'provider',nodeDcCredentialId:'provider-ref'};
|
|
const policy={kind:'httpRequest',allowedHosts:['api.geliospro.com'],requireHttps:true,disableRedirects:true};
|
|
const slot='ndcProviderRotatingAccessApi';
|
|
const read=(id,url,parameters={})=>({id,data:{n8n:{id,name:id,type:'n8n-nodes-base.httpRequest',parameters:{method:'GET',url,...parameters},credentials:{[slot]:credential}},nodedcAgentCredentialPolicies:{[slot]:policy}}});
|
|
const query={sendQuery:true,specifyQuery:'keypair',queryParameters:{parameters:[{name:'incltrip',value:'true'}]}};
|
|
const graph={nodes:[read('monitoring','https://api.geliospro.com/api/v1/users/me/monitoring-config'),read('units','https://api.geliospro.com/api/v1/units',query),{id:'publish',data:{n8n:{id:'publish',name:'publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.positions.current.v3'},credentials:{}}}}],edges:[{source:'monitoring',target:'publish'},{source:'units',target:'publish'}]};
|
|
const resolved=module.resolveProviderConnectionFromGraph({graph,targetNodeId:'publish',catalog});
|
|
if(!validation.ok||catalog.packages?.[0]?.id!=='gelios.provider.v4'||catalog.packages?.[0]?.providerCredential?.credentialType!==slot||!resolved.ok||resolved.descriptor?.providerCredentialRef!=='provider-ref'||resolved.descriptor?.capabilityIds?.join(',')!=='gelios.monitoring_config.current.read,gelios.units.current.read'||resolved.descriptor?.providerRequestNodeIds?.join(',')!=='monitoring,units')process.exit(2);
|
|
process.stdout.write('engine-composite-provider:gelios.provider.v4:ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units');
|
|
""".strip(),
|
|
),
|
|
"Engine composite provider authority",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-composite-provider:gelios.provider.v4:"
|
|
"ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units"
|
|
)
|
|
if live != expected:
|
|
die("Engine composite provider live acceptance mismatch")
|
|
return {
|
|
"catalog_sha256": ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256,
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_provider_rotating_slot_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_provider_rotating_slot_slice(
|
|
root,
|
|
ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js');
|
|
const catalog=await module.loadProviderSecurityCatalog();
|
|
const validation=module.validateProviderSecurityCatalog(catalog);
|
|
const credential={id:'native-provider',name:'provider',nodeDcCredentialId:'provider-ref'};
|
|
const policy={kind:'httpRequest',allowedHosts:['api.geliospro.com'],requireHttps:true,disableRedirects:true};
|
|
const slot='ndcProviderRotatingAccessApi';
|
|
const read=(id,url,parameters={})=>({id,data:{n8n:{id,name:id,type:'n8n-nodes-base.httpRequest',parameters:{method:'GET',url,...parameters},credentials:{[slot]:credential}},nodedcAgentCredentialPolicies:{[slot]:policy}}});
|
|
const query={sendQuery:true,specifyQuery:'keypair',queryParameters:{parameters:[{name:'incltrip',value:'true'}]}};
|
|
const graph={nodes:[read('monitoring','https://api.geliospro.com/api/v1/users/me/monitoring-config'),read('units','https://api.geliospro.com/api/v1/units',query),{id:'publish',data:{n8n:{id:'publish',name:'publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.positions.current.v3'},credentials:{}}}}],edges:[{source:'monitoring',target:'publish'},{source:'units',target:'publish'}]};
|
|
const resolved=module.resolveProviderConnectionFromGraph({graph,targetNodeId:'publish',catalog});
|
|
if(!validation.ok||catalog.packages?.[0]?.id!=='gelios.provider.v4'||catalog.packages?.[0]?.providerCredential?.credentialType!==slot||!resolved.ok||resolved.descriptor?.providerCredentialRef!=='provider-ref'||resolved.descriptor?.capabilityIds?.join(',')!=='gelios.monitoring_config.current.read,gelios.units.current.read'||resolved.descriptor?.providerRequestNodeIds?.join(',')!=='monitoring,units')process.exit(2);
|
|
process.stdout.write('engine-provider-rotating-slot:gelios.provider.v4:ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units');
|
|
""".strip(),
|
|
),
|
|
"Engine provider rotating slot authority",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-provider-rotating-slot:gelios.provider.v4:"
|
|
"ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units"
|
|
)
|
|
if live != expected:
|
|
die("Engine provider rotating slot live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_provider_authority_diagnostics_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_provider_authority_diagnostics_slice(
|
|
root,
|
|
ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const module=await import('file:///app/server/nodeIntelligence/upstreamProjection.js');
|
|
const privateValidation={nodeId:'private-1',nodeName:'NDC Data Product Publish',valid:true,errors:[],warnings:[]};
|
|
const privateOnly=module.reconcilePrivateNodeValidation({valid:false,errorCount:1,warningCount:0,summary:{errorCount:1,warningCount:0},errors:[{node:'NDC Data Product Publish',message:'Unknown node type: \"ndcDataProductPublish\".'}],warnings:[]},['private-1'],[privateValidation]);
|
|
const mixed=module.reconcilePrivateNodeValidation({valid:false,errorCount:2,warningCount:0,summary:{errorCount:2,warningCount:0},errors:[{node:'NDC Data Product Publish',message:'Unknown node type: \"ndcDataProductPublish\".'},{nodeId:'built-in-1',nodeName:'HTTP',message:'URL is required'}],warnings:[]},['private-1'],[privateValidation]);
|
|
if(!privateOnly.valid||privateOnly.errors?.length!==0||privateOnly.summary?.errorCount!==0||privateOnly.summary?.warningCount!==1||mixed.valid||mixed.errors?.length!==1||mixed.errors?.[0]?.nodeId!=='built-in-1')process.exit(2);
|
|
process.stdout.write('engine-private-node-reconciliation:exact-id-or-name:v1');
|
|
""".strip(),
|
|
),
|
|
"Engine private node validation reconciliation",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = "engine-private-node-reconciliation:exact-id-or-name:v1"
|
|
if live != expected:
|
|
die("Engine provider authority diagnostics live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_depttrans_zone_authority_v1_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_depttrans_zone_authority_v1_slice(
|
|
root,
|
|
ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js');
|
|
const catalog=await module.loadProviderSecurityCatalog();
|
|
const parameters={method:'GET',url:'http://map-gateway:18103/internal/zone-sources/v1/profiles/moscow-pmd-slow-zones/current',authentication:'none',sendQuery:false,sendHeaders:true,specifyHeaders:'keypair',headerParameters:{parameters:[{name:'Accept',value:'application/json'}]},sendBody:false,options:{response:{response:{fullResponse:false,responseFormat:'json'}},allowUnauthorizedCerts:false,redirect:{redirect:{followRedirects:false}},timeout:15000}};
|
|
const graph={nodes:[{id:'source',data:{n8n:{id:'source',name:'source',type:'n8n-nodes-base.httpRequest',parameters}}},{id:'publish',data:{n8n:{id:'publish',name:'publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'map.zones.current.v2',publishMode:'replace'},credentials:{}}}}],edges:[{source:'source',target:'publish'}]};
|
|
const resolved=module.resolveProviderConnectionFromGraph({graph,targetNodeId:'publish',catalog});
|
|
const drift=structuredClone(graph);drift.nodes[0].data.n8n.parameters.options.redirect.redirect.followRedirects=true;
|
|
const rejected=module.resolveProviderConnectionFromGraph({graph:drift,targetNodeId:'publish',catalog});
|
|
if(!module.validateProviderSecurityCatalog(catalog).ok||!resolved.ok||resolved.descriptor?.providerId!=='moscow-department-of-transport'||resolved.descriptor?.authorityBoundary!=='platform-service'||resolved.descriptor?.providerCredentialRef!==null||resolved.descriptor?.platformServiceIds?.join(',')!=='nodedc-map-gateway'||rejected.blockers?.join(',')!=='publish_grant_provider_request_not_exact')process.exit(2);
|
|
process.stdout.write('engine-depttrans-zone-authority:platform-service:map.zones.current.v2:nodedc-map-gateway');
|
|
""".strip(),
|
|
),
|
|
"Engine Depttrans zone authority v1",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-depttrans-zone-authority:platform-service:"
|
|
"map.zones.current.v2:nodedc-map-gateway"
|
|
)
|
|
if live != expected:
|
|
die("Engine Depttrans zone authority v1 live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_provider_target_host_policy_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_provider_target_host_policy_slice(
|
|
root,
|
|
ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const module=await import('file:///app/server/routes/n8n.js');
|
|
const slot='ndcProviderRotatingAccessApi';
|
|
const entry={id:'provider',name:'provider',type:slot,nodeDcCredentialId:'provider-ref',data:{engineAgentAllowedHttpHosts:['*.geliospro.com','telemetry.example']}};
|
|
const target={type:'n8n-nodes-base.httpRequest',parameters:{url:'https://api.geliospro.com/api/v1/units'}};
|
|
const deniedTarget={type:'n8n-nodes-base.httpRequest',parameters:{url:'https://untrusted.example/collect'}};
|
|
const allowed=module.buildEngineAgentCredentialTransportPolicy(entry,{nodes:[]},slot,target);
|
|
const denied=module.buildEngineAgentCredentialTransportPolicy(entry,{nodes:[]},slot,deniedTarget);
|
|
if(!allowed.bindable||allowed.policy?.allowedHosts?.join(',')!=='api.geliospro.com'||allowed.policy?.requireHttps!==true||allowed.policy?.disableRedirects!==true||denied.bindable||denied.reason!=='credential_host_not_allowed')process.exit(2);
|
|
process.stdout.write('engine-provider-target-host-policy:exact-literal-host:v1');
|
|
""".strip(),
|
|
),
|
|
"Engine provider target host policy",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = "engine-provider-target-host-policy:exact-literal-host:v1"
|
|
if live != expected:
|
|
die("Engine provider target host policy live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_execution_profile_decoder_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_execution_profile_decoder_slice(
|
|
root,
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const module=await import('file:///app/server/routes/n8n.js');
|
|
const execution={id:'profile-decoder-acceptance',data:JSON.stringify([{resultData:'1'},{runData:'2'},{Probe:'3'},['4'],{data:'5'},{main:'6'},[['7']],{json:'8'},{params:'9'},{VS_43:'10',mcc:'11',sats:9},'16','250'])};
|
|
const profile=module.toSafeNodeOutputProfile(execution,'Probe');
|
|
const paths=new Map(profile.paths.map((item)=>[item.path,item]));
|
|
if(!profile.found||profile.valuesIncluded!==false||paths.get('$.params.VS_43')?.types?.join(',')!=='string'||paths.get('$.params.VS_43')?.minLength!==2||paths.get('$.params.mcc')?.types?.join(',')!=='string'||paths.get('$.params.mcc')?.minLength!==3||paths.get('$.params.sats')?.types?.join(',')!=='number'||profile.paths.some((item)=>item.path.includes('.json.fact')))process.exit(2);
|
|
process.stdout.write('engine-mcp-execution-profile-decoder:flatted-numeric-strings:safe-profile:v1');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP execution profile decoder",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-execution-profile-decoder:"
|
|
"flatted-numeric-strings:safe-profile:v1"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP execution profile decoder live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(ENGINE_MCP_EXECUTION_PROFILE_DECODER_TARGET_SHA256),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_telemetry_catalog_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_telemetry_catalog_slice(
|
|
root,
|
|
ENGINE_MCP_TELEMETRY_CATALOG_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const n8n=await import('file:///app/server/routes/n8n.js');
|
|
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const execution={id:'telemetry-catalog-acceptance',data:{resultData:{runData:{Mapper:[{data:{main:[[{json:{fact:{attributes:{sensor_readings:[{id:'engine.rpm',label:'Engine RPM',unit:'rpm',value:1350},{id:'tracker.status',label:'Bearer provider-token-must-not-escape',value:'provider-token-must-not-escape'}]}},lastMsg:{params:{sensor_readings:[{id:'raw.must-not-enter',label:'Raw',value:'raw-provider-value'}]}}}}]]}}]}}}};
|
|
const catalog=n8n.toSafeTelemetryReadingCatalog(execution,'Mapper');
|
|
const rpm=catalog.readings.find((item)=>item.id==='engine.rpm');
|
|
const status=catalog.readings.find((item)=>item.id==='tracker.status');
|
|
const serialized=JSON.stringify(catalog);
|
|
const tool=gateway.engineAgentTools.find((item)=>item.name==='engine_get_telemetry_reading_catalog');
|
|
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.8.0'||!tool||!catalog.found||catalog.valuesIncluded!==false||catalog.rawExecutionDataIncluded!==false||rpm?.label!=='Engine RPM'||rpm?.unit!=='rpm'||rpm?.valueTypes?.join(',')!=='number'||status?.label!==undefined||catalog.readings.some((item)=>item.id==='raw.must-not-enter')||serialized.includes('1350')||serialized.includes('provider-token-must-not-escape')||serialized.includes('raw-provider-value'))process.exit(2);
|
|
process.stdout.write('engine-mcp-telemetry-catalog:0.8.0:normalized-metadata-only:v1');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP telemetry reading catalog",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = "engine-mcp-telemetry-catalog:0.8.0:normalized-metadata-only:v1"
|
|
if live != expected:
|
|
die("Engine MCP telemetry reading catalog live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_execution_plan_materialization_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_execution_plan_materialization_slice(
|
|
root,
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const fs=await import('node:fs/promises');
|
|
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const catalogModule=await import('file:///app/server/l2ExecutionPlan/catalog.js');
|
|
const materializer=await import('file:///app/server/l2ExecutionPlan/materializer.js');
|
|
const catalog=await catalogModule.loadExecutionPlanCatalog();
|
|
const plan=gateway.engineAgentTools.find((item)=>item.name==='engine_plan_l2_execution_plan_materialization');
|
|
const apply=gateway.engineAgentTools.find((item)=>item.name==='engine_apply_l2_execution_plan_materialization');
|
|
const planRequired=plan?.inputSchema?.required||[];
|
|
const applyRequired=apply?.inputSchema?.required||[];
|
|
const genericSource=(await Promise.all(['/app/server/l2ExecutionPlan/compiler.js','/app/server/l2ExecutionPlan/materializer.js'].map((path)=>fs.readFile(path,'utf8')))).join('\\n');
|
|
const unmanaged=materializer.materializationState({source:{},nodes:[{id:'existing'}],edges:[]});
|
|
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.9.0'||!plan||!apply||plan?.inputSchema?.additionalProperties!==false||apply?.inputSchema?.additionalProperties!==false||!planRequired.includes('executionPlan')||!applyRequired.includes('planRef')||!applyRequired.includes('providerAuthRef')||catalog.schemaVersion!=='nodedc.engine.execution-plan-catalog/v1'||catalog.runtime?.graphBlueprintSchemaVersion!=='nodedc.l2-graph-blueprint/v1'||catalog.runtime?.compilerVersions?.join(',')!=='1.1.0'||unmanaged!=='unmanaged_existing'||/gelios|robot2b/i.test(genericSource))process.exit(2);
|
|
process.stdout.write('engine-mcp-execution-plan-materialization:0.9.0:provider-package-authority:two-phase:v1');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP execution plan materialization",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-execution-plan-materialization:"
|
|
"0.9.0:provider-package-authority:two-phase:v1"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP execution plan materialization live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_execution_plan_telemetry_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
root,
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const fs=await import('node:fs/promises');
|
|
const catalogModule=await import('file:///app/server/l2ExecutionPlan/catalog.js');
|
|
const catalog=await catalogModule.loadExecutionPlanCatalog();
|
|
const compiler=await fs.readFile('/app/server/l2ExecutionPlan/compiler.js','utf8');
|
|
const versions=catalog.runtime?.compilerVersions||[];
|
|
if(versions.join(',')!=='1.1.0,1.2.0'||!compiler.includes("if (compilerVersion === '1.1.0')")||!compiler.includes("if (compilerVersion !== '1.2.0')")||!compiler.includes('"msgParam", "msg_param"')||!compiler.includes('descriptor.telemetryProjection')||!compiler.includes('"message-param"')||!compiler.includes('convertedSensorValue')||!compiler.includes('visibleSensorDefinition')||/gelios|robot2b/i.test(compiler))process.exit(2);
|
|
process.stdout.write('engine-mcp-execution-plan-telemetry-runtime:0.9.0:compiler-1.2.0:declared-projected:v2');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP execution plan telemetry runtime",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-execution-plan-telemetry-runtime:"
|
|
"0.9.0:compiler-1.2.0:declared-projected:v2"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP execution plan telemetry runtime live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_execution_plan_module_ownership_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_execution_plan_module_ownership_slice(
|
|
root,
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const fs=await import('node:fs/promises');
|
|
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const catalogModule=await import('file:///app/server/l2ExecutionPlan/catalog.js');
|
|
const materializer=await import('file:///app/server/l2ExecutionPlan/materializer.js');
|
|
const catalog=await catalogModule.loadExecutionPlanCatalog();
|
|
const source=await fs.readFile('/app/server/l2ExecutionPlan/materializer.js','utf8');
|
|
const plan=gateway.engineAgentTools.find((item)=>item.name==='engine_plan_l2_execution_plan_materialization');
|
|
const strategies=plan?.inputSchema?.properties?.strategy?.enum||[];
|
|
const adoption=plan?.inputSchema?.properties?.moduleAdoption?.properties||{};
|
|
const geliosV9=catalog.packages?.find((item)=>item.id==='gelios.provider.v9');
|
|
const legacy=catalog.packages?.some((item)=>item.id==='gelios.provider.v8');
|
|
const unmanaged=materializer.materializationState({source:{},nodes:[{id:'existing'}],edges:[]});
|
|
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.10.0'||strategies.join(',')!=='create_or_reconcile_owned,adopt_existing,adopt_existing_module'||!adoption.preserveBoundNodeIds||!source.includes('execution_plan_module_retire_boundary_not_closed')||!source.includes('manual_webhook_same_method')||/gelios|robot2b/i.test(source)||geliosV9?.contractDigest!=='sha256:7dd4ce4a45ce76e884ffa1e304bfaa521f552e9a45e535930f2d17b882ae23ed'||!legacy||unmanaged!=='unmanaged_existing')process.exit(2);
|
|
process.stdout.write('engine-mcp-execution-plan-module-ownership:0.10.0:module-scoped:shared-boundary:v3');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP execution plan module ownership",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-execution-plan-module-ownership:"
|
|
"0.10.0:module-scoped:shared-boundary:v3"
|
|
)
|
|
if live != expected:
|
|
die(
|
|
"Engine MCP execution plan module ownership live acceptance "
|
|
"mismatch"
|
|
)
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_normalized_identity_search_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_normalized_identity_search_slice(
|
|
root,
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const fs=await import('node:fs/promises');
|
|
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const executionCatalog=JSON.parse(await fs.readFile('/app/server/assets/execution-plans/v1/catalog.json','utf8'));
|
|
const securityCatalog=JSON.parse(await fs.readFile('/app/server/assets/provider-packages/v1/catalog.json','utf8'));
|
|
const route=await fs.readFile('/app/server/routes/n8n.js','utf8');
|
|
const compiler=await fs.readFile('/app/server/l2ExecutionPlan/compiler.js','utf8');
|
|
const tool=gateway.engineAgentTools.find((item)=>item.name==='engine_find_normalized_subjects');
|
|
const packageV11=executionCatalog.packages?.find((item)=>item.id==='gelios.provider.v11');
|
|
const securityV11=securityCatalog.packages?.find((item)=>item.id==='gelios.provider.v11');
|
|
const identityProfile=packageV11?.profiles?.find((item)=>item.dataProductId==='fleet.units.identity.current.v1');
|
|
const identityCapability=securityV11?.capabilities?.find((item)=>item.id==='gelios.units.identity.read');
|
|
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.11.0'||!tool?.inputSchema?.properties?.boundingBox||!tool?.inputSchema?.properties?.fields||!route.includes('normalizedFactsOnly: true')||!route.includes('rawExecutionDataIncluded: false')||!route.includes('commandSurfaceIncluded: false')||!compiler.includes('boundedStringList')||!compiler.includes('boundedNamedValues')||/gelios|robot2b/i.test(compiler)||executionCatalog.runtime?.compilerVersions?.join(',')!=='1.1.0,1.2.0,1.3.0,1.4.0'||packageV11?.contractDigest!=='sha256:4bf211d9ed6cfd227df444d49d8c5ce77a24389ed2ba3d159d53d83e0d4738d9'||!identityProfile||identityCapability?.classification!=='read'||identityCapability?.dataProductIds?.join(',')!=='fleet.units.identity.current.v1')process.exit(2);
|
|
process.stdout.write('engine-mcp-normalized-identity-search:0.11.0:canonical-facts:full-admin-identifiers:read-only:v1');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP normalized identity search",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-normalized-identity-search:"
|
|
"0.11.0:canonical-facts:full-admin-identifiers:read-only:v1"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP normalized identity search live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_execution_plan_sandbox_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
root,
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const fs=await import('node:fs/promises');
|
|
const compiler=await fs.readFile('/app/server/l2ExecutionPlan/compiler.js','utf8');
|
|
const start=compiler.indexOf('const encodedBytes = (value) => {');
|
|
const end=compiler.indexOf('for (const raw of input) {',start);
|
|
const byteCounter=start>=0&&end>start?compiler.slice(start,end):'';
|
|
if(!byteCounter.includes('encoded.charCodeAt(index)')||!byteCounter.includes('length += 4')||byteCounter.includes('TextEncoder')||!compiler.includes('return extracted.map((source) => ({ json: { source, collectionReceivedAt: receivedAt } }))')||/gelios|robot2b/i.test(compiler))process.exit(2);
|
|
process.stdout.write('engine-mcp-execution-plan-sandbox-runtime:0.11.0:n8n-2.3.2:pure-js-utf8:v4');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP execution plan sandbox runtime",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-execution-plan-sandbox-runtime:"
|
|
"0.11.0:n8n-2.3.2:pure-js-utf8:v4"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP execution plan sandbox runtime live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_gelios_items_envelope_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_gelios_items_envelope_slice(
|
|
root,
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const fs=await import('node:fs/promises');
|
|
const execution=JSON.parse(await fs.readFile('/app/server/assets/execution-plans/v1/catalog.json','utf8'));
|
|
const security=JSON.parse(await fs.readFile('/app/server/assets/provider-packages/v1/catalog.json','utf8'));
|
|
const compiler=await fs.readFile('/app/server/l2ExecutionPlan/compiler.js','utf8');
|
|
const ids=execution.packages.map(({id})=>id);
|
|
const v12=execution.packages.find(({id})=>id==='gelios.provider.v12');
|
|
const identity=v12?.profiles?.filter(({id,dataProductId})=>id==='gelios.units.identity.warm.v1'&&dataProductId==='fleet.units.identity.current.v1')||[];
|
|
const authorities=security.packages.filter(({capabilities=[]})=>capabilities.some(({dataProductIds=[]})=>dataProductIds.includes('fleet.units.identity.current.v1')));
|
|
if(!ids.includes('gelios.provider.v11')||v12?.version!=='12.0.0'||identity.length!==1||authorities.length!==1||authorities[0].id!=='gelios.provider.v12'||/gelios|robot2b/i.test(compiler)||compiler.includes('TextEncoder'))process.exit(2);
|
|
process.stdout.write('engine-mcp-gelios-items-envelope:0.11.0:v11-history:v12-authority:items');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP Gelios items envelope",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-gelios-items-envelope:"
|
|
"0.11.0:v11-history:v12-authority:items"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP Gelios items envelope live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_registered_execution_profiles_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_registered_execution_profiles_slice(
|
|
root,
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const catalogModule=await import('file:///app/server/l2ExecutionPlan/catalog.js');
|
|
const registeredModule=await import('file:///app/server/l2ExecutionPlan/registeredProfiles.js');
|
|
const catalog=await catalogModule.loadExecutionPlanCatalog();
|
|
let captured=null;
|
|
const service=registeredModule.createRegisteredExecutionProfileService({
|
|
loadCatalog:async()=>catalog,
|
|
planMaterialization:async(input)=>{captured=input;return {ok:true,status:'planned',planRef:'emplan_probe',providerAuthRefs:[]};},
|
|
});
|
|
const target={workflowId:'workflow-probe',nodeId:'node-probe'};
|
|
const listed=await service.list({actorKey:'actor-probe',target});
|
|
const profile=listed.profiles.find(({profile})=>profile.id==='gelios.units.profile.cold.v1');
|
|
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.12.0'||!gateway.engineAgentTools.some(({name})=>name==='engine_list_l2_execution_profiles')||!gateway.engineAgentTools.some(({name})=>name==='engine_plan_registered_l2_execution')||listed.count!==6||!profile||JSON.stringify(listed).includes('executionPlanTemplate'))process.exit(2);
|
|
await service.plan({actorKey:'actor-probe',target,profileRef:profile.profileRef,expectedRevision:'revision-probe',strategy:'create_or_reconcile_owned',expectedScope:{tenantId:'tenant-probe',connectionId:'connection-probe'}});
|
|
if(captured?.executionPlan?.connection?.tenantId!=='tenant-probe'||captured?.executionPlan?.connection?.connectionId!=='connection-probe'||!String(captured?.executionPlan?.bindings?.provider?.reference||'').startsWith('ndc-credref:engine-target-'))process.exit(3);
|
|
process.stdout.write('engine-mcp-registered-execution-profiles:0.12.0:profiles-6:target-scope:existing-materializer');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP registered execution profiles",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-registered-execution-profiles:"
|
|
"0.12.0:profiles-6:target-scope:existing-materializer"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP registered execution profiles live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_gelios_units_items_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_gelios_units_items_slice(
|
|
root,
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
|
|
const executionModule=await import('file:///app/server/l2ExecutionPlan/catalog.js');
|
|
const securityModule=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js');
|
|
const execution=await executionModule.loadExecutionPlanCatalog();
|
|
const security=await securityModule.loadProviderSecurityCatalog();
|
|
const provider=execution.packages.find((item)=>item.id==='gelios.provider.v12');
|
|
const profile=provider?.profiles.find((item)=>item.id==='gelios.units.profile.cold.v1');
|
|
const plan=profile?.executionPlanTemplate;
|
|
const extract=plan?.steps.find((step)=>step.kind==='extract_items'&&step.config?.capabilityId==='gelios.units.current.read');
|
|
const secureProvider=security.packages.find((item)=>item.id==='gelios.provider.v12');
|
|
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.12.0'||!gateway.engineAgentTools.some(({name})=>name==='engine_list_l2_execution_profiles')||!gateway.engineAgentTools.some(({name})=>name==='engine_plan_registered_l2_execution')||provider?.version!=='12.0.1'||secureProvider?.version!=='12.0.1'||plan?.compilerVersion!=='1.4.0'||extract?.config?.response?.collectionPaths?.[0]!=='items')process.exit(2);
|
|
process.stdout.write('engine-mcp-gelios-units-items:0.12.0:gelios.provider.v12@12.0.1:technical-profile:items');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP Gelios units items envelope",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-gelios-units-items:0.12.0:"
|
|
"gelios.provider.v12@12.0.1:technical-profile:items"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP Gelios units items live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_l1_credential_reuse_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_l1_credential_reuse_slice(
|
|
root,
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const route=await import('file:///app/server/routes/n8n.js');
|
|
const slot='httpBearerAuth';
|
|
const entry={nodeDcCredentialId:'credential-provider',n8nCredentialId:'runtime-provider',type:slot,name:'Provider read access',source:'manual',status:'ok',data:{token:'synthetic'}};
|
|
const sameL1=new Set([entry.nodeDcCredentialId]);
|
|
const otherL1=new Set(['credential-other']);
|
|
const graph={nodes:[{id:'source-reader',data:{n8n:{type:'n8n-nodes-base.httpRequest',parameters:{url:'https://api.provider.example/v1/items'},credentials:{[slot]:{id:entry.n8nCredentialId,name:entry.name,nodeDcCredentialId:entry.nodeDcCredentialId}}}}}]};
|
|
const target={type:'n8n-nodes-base.httpRequest',parameters:{url:'https://api.provider.example/v1/identity'}};
|
|
const deniedTarget={type:'n8n-nodes-base.httpRequest',parameters:{url:'https://untrusted.example/collect'}};
|
|
const allowed=route.buildEngineAgentCredentialTransportPolicy({...entry,data:{allowedHttpRequestDomains:'all'}},graph,slot,target);
|
|
const denied=route.buildEngineAgentCredentialTransportPolicy({...entry,data:{allowedHttpRequestDomains:'all'}},graph,slot,deniedTarget);
|
|
if(route.engineAgentCandidateScope(entry,{localEntries:[]},sameL1)!=='l1'||route.engineAgentCandidateScope(entry,{localEntries:[]},otherL1)!==''||route.engineAgentCredentialMayReuseWithinL1({...entry,source:'workflow-inline'})||!allowed.bindable||allowed.policy?.allowedHosts?.join(',')!=='api.provider.example'||denied.bindable)process.exit(2);
|
|
process.stdout.write('engine-mcp-l1-credential-reuse:0.11.0:same-l1:opaque-ref:v1');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP L1 credential reuse",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-l1-credential-reuse:"
|
|
"0.11.0:same-l1:opaque-ref:v1"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP L1 credential reuse live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(ENGINE_MCP_L1_CREDENTIAL_REUSE_TARGET_SHA256),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def accept_engine_mcp_l1_credential_provenance_runtime():
|
|
root = component_root("engine")
|
|
validate_engine_mcp_l1_credential_provenance_slice(
|
|
root,
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_ARTIFACT_ENTRIES,
|
|
)
|
|
live = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
"""
|
|
const route=await import('file:///app/server/routes/n8n.js');
|
|
const slot='httpBearerAuth';
|
|
const common={nodeDcCredentialId:'credential-provider',n8nCredentialId:'runtime-provider',type:slot,name:'Provider read access',source:'manual',status:'ok',data:{token:'synthetic'}};
|
|
const workflowRef={...common,source:'workflow-ref'};
|
|
const workflowRefWithoutSync={...workflowRef,data:{}};
|
|
const sameL1=new Set([common.nodeDcCredentialId]);
|
|
const graph={nodes:[{id:'source-reader',data:{n8n:{type:'n8n-nodes-base.httpRequest',parameters:{url:'https://api.provider.example/v1/items'},credentials:{[slot]:{id:common.n8nCredentialId,name:common.name,nodeDcCredentialId:common.nodeDcCredentialId}}}}}]};
|
|
const target={type:'n8n-nodes-base.httpRequest',parameters:{url:'https://api.provider.example/v1/identity'}};
|
|
const allowed=route.buildEngineAgentCredentialTransportPolicy({...common,data:{allowedHttpRequestDomains:'all'}},graph,slot,target);
|
|
if(!route.engineAgentCredentialMayProveL1Provenance(workflowRef)||route.engineAgentCredentialMayProveL1Provenance(workflowRefWithoutSync)||route.engineAgentCredentialMayReuseWithinL1(workflowRef)||route.engineAgentCandidateScope(common,{localEntries:[]},sameL1)!=='l1'||!allowed.bindable||allowed.policy?.allowedHosts?.join(',')!=='api.provider.example')process.exit(2);
|
|
process.stdout.write('engine-mcp-l1-credential-provenance:0.11.0:workflow-ref-sync-proof:v2');
|
|
""".strip(),
|
|
),
|
|
"Engine MCP L1 credential provenance",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = (
|
|
"engine-mcp-l1-credential-provenance:"
|
|
"0.11.0:workflow-ref-sync-proof:v2"
|
|
)
|
|
if live != expected:
|
|
die("Engine MCP L1 credential provenance live acceptance mismatch")
|
|
return {
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_TARGET_SHA256
|
|
),
|
|
"live": live,
|
|
}
|
|
|
|
|
|
def validate_no_lifecycle_scripts(payload_dir, label):
|
|
forbidden = ("preinstall", "install", "postinstall", "prepare", "prepack", "postpack")
|
|
for package_path in payload_dir.rglob("package.json"):
|
|
package = read_strict_json(package_path, f"{label} package.json", max_bytes=1024 * 1024)
|
|
scripts = package.get("scripts") or {}
|
|
if not isinstance(scripts, dict):
|
|
die(f"{label} package scripts must be an object")
|
|
for name in forbidden:
|
|
if name in scripts:
|
|
die(f"{label} lifecycle script is forbidden: {package_path.relative_to(payload_dir)}:{name}")
|
|
|
|
|
|
def expected_engine_credential_backend_override():
|
|
return "\n".join((
|
|
"services:",
|
|
" nodedc-backend:",
|
|
f" image: {ENGINE_CREDENTIAL_BACKEND_IMAGE}",
|
|
" pull_policy: never",
|
|
" environment:",
|
|
" HOME: /tmp",
|
|
" command:",
|
|
" - /bin/sh",
|
|
" - -lc",
|
|
" - |-|".replace("|-|", "|-"),
|
|
" set -eu",
|
|
" test \"$$(command -v node)\" = /usr/local/bin/node",
|
|
" command -v sqlite3 >/dev/null",
|
|
" command -v docker >/dev/null",
|
|
" docker compose version >/dev/null",
|
|
f" test \"$$(sha256sum /app/package-lock.json | cut -d ' ' -f 1)\" = {ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256}",
|
|
" test -d /app/node_modules",
|
|
" mkdir -p /app/server/data/api",
|
|
" if [ -d /seed-api ]; then",
|
|
" for d in cesium gelios overpass wind yandex; do",
|
|
" src=/seed-api/$$d",
|
|
" [ -d \"$$src\" ] || continue",
|
|
" dst=/app/server/data/api/$$d",
|
|
" mkdir -p \"$$dst\"",
|
|
" if [ \"$$NODEDC_API_SEED_MODE\" = force ]; then",
|
|
" cp -R \"$$src/.\" \"$$dst/\"",
|
|
" elif [ -z \"$$(ls -A \"$$dst\" 2>/dev/null)\" ]; then",
|
|
" cp -R \"$$src/.\" \"$$dst/\"",
|
|
" fi",
|
|
" done",
|
|
" fi",
|
|
" if [ \"$$NODEDC_DATA_SEED_MODE\" != off ] && [ \"$$NODEDC_DATA_SEED_MODE\" != none ]; then",
|
|
" for d in workflows n8n; do",
|
|
" src=/seed-data/$$d",
|
|
" dst=/app/server/data/$$d",
|
|
" [ -d \"$$src\" ] || continue",
|
|
" mkdir -p \"$$dst\"",
|
|
" if [ \"$$NODEDC_DATA_SEED_MODE\" = merge ] || [ \"$$NODEDC_DATA_SEED_MODE\" = force ]; then",
|
|
" cp -R \"$$src/.\" \"$$dst/\"",
|
|
" elif [ -z \"$$(ls -A \"$$dst\" 2>/dev/null)\" ]; then",
|
|
" cp -R \"$$src/.\" \"$$dst/\"",
|
|
" fi",
|
|
" done",
|
|
" fi",
|
|
" exec node server/index.js",
|
|
" volumes:",
|
|
" - ./nodedc-backend-node_modules:/app/node_modules:ro",
|
|
" tmpfs:",
|
|
" - /tmp:mode=1777",
|
|
" - /run:mode=0755",
|
|
" - /var/cache:mode=0755",
|
|
" - /var/log:mode=0755",
|
|
"",
|
|
))
|
|
|
|
|
|
def expected_engine_data_product_publish_grant_override():
|
|
return "\n".join((
|
|
"services:",
|
|
" nodedc-backend:",
|
|
' user: "0:0"',
|
|
" environment:",
|
|
f" ENGINE_DATA_PLANE_BASE_URL: {EXTERNAL_DATA_PLANE_INTERNAL_URL}",
|
|
f" ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE: {ENGINE_EDP_PRIVATE_KEY_CONTAINER_PATH}",
|
|
f" ENGINE_CONTROL_PLANE_PUBLISH_GRANT_ROOT: {ENGINE_PUBLISH_GRANT_CONTAINER_PATH}",
|
|
" volumes:",
|
|
" - type: bind",
|
|
f" source: {ENGINE_PUBLISH_GRANT_STATE_PATH}",
|
|
f" target: {ENGINE_PUBLISH_GRANT_CONTAINER_PATH}",
|
|
" bind:",
|
|
" create_host_path: false",
|
|
" - type: bind",
|
|
f" source: {ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}",
|
|
f" target: {ENGINE_EDP_PRIVATE_KEY_CONTAINER_PATH}",
|
|
" read_only: true",
|
|
" bind:",
|
|
" create_host_path: false",
|
|
"",
|
|
))
|
|
|
|
|
|
def expected_engine_data_product_read_grant_override():
|
|
return "\n".join((
|
|
"services:",
|
|
" nodedc-backend:",
|
|
" environment:",
|
|
f" ENGINE_CONTROL_PLANE_READ_GRANT_ROOT: {ENGINE_READ_GRANT_CONTAINER_PATH}",
|
|
" volumes:",
|
|
" - type: bind",
|
|
f" source: {ENGINE_READ_GRANT_STATE_PATH}",
|
|
f" target: {ENGINE_READ_GRANT_CONTAINER_PATH}",
|
|
" bind:",
|
|
" create_host_path: false",
|
|
"",
|
|
))
|
|
|
|
|
|
def validate_engine_credential_sink_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_CREDENTIAL_SINK_ARTIFACT_ENTRIES:
|
|
die("Engine credential sink files.txt exact set/order mismatch")
|
|
index_text = (payload_dir / "nodedc-source/server/index.js").read_text(encoding="utf-8")
|
|
if "engineCredentialSink" not in index_text:
|
|
die("Engine credential sink router mount is missing")
|
|
vendor_contract = payload_dir / "nodedc-source/server/credentialSink/vendor/engine-credential-sink.mjs"
|
|
if sha256_file(vendor_contract) != ENGINE_CREDENTIAL_SINK_CONTRACT_SHA256:
|
|
die("Engine credential sink vendored contract sha256 mismatch")
|
|
override = payload_dir / ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL
|
|
try:
|
|
override_text = override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine credential immutable runtime override is unreadable")
|
|
if override_text != expected_engine_credential_backend_override():
|
|
die("Engine credential immutable runtime override mismatch")
|
|
if re.search(r"\b(?:apk|npm|yarn|pnpm)\b", override_text):
|
|
die("Engine credential mutable runtime setup is forbidden")
|
|
generic_roots = (
|
|
payload_dir / "nodedc-source/server/credentialSink",
|
|
payload_dir / "nodedc-source/server/credentialPolicies/ndcPrivateNode.js",
|
|
payload_dir / "nodedc-source/server/routes/engineCredentialSink.js",
|
|
)
|
|
for root in generic_roots:
|
|
candidates = [root] if root.is_file() else list(root.rglob("*"))
|
|
for path in candidates:
|
|
if not path.is_file() or path.suffix.lower() not in (".js", ".mjs", ".json"):
|
|
continue
|
|
try:
|
|
text_value = path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die(f"Engine credential sink source is unreadable: {path.relative_to(payload_dir)}")
|
|
if re.search(r"gelios|robot2b", text_value, re.IGNORECASE):
|
|
die(f"Engine credential sink provider logic is forbidden: {path.relative_to(payload_dir)}")
|
|
validate_no_lifecycle_scripts(payload_dir, "Engine credential sink")
|
|
|
|
|
|
def validate_engine_data_product_publish_grant_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES:
|
|
die("Engine data product publish grant files.txt exact set/order mismatch")
|
|
|
|
override = payload_dir / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL
|
|
try:
|
|
override_text = override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine data product publish grant runtime override is unreadable")
|
|
if override_text != expected_engine_data_product_publish_grant_override():
|
|
die("Engine data product publish grant runtime override mismatch")
|
|
|
|
n8n_route = (payload_dir / "nodedc-source/server/routes/n8n.js").read_text(encoding="utf-8")
|
|
if "from '../credentialSink/" in n8n_route:
|
|
die("Engine data product publish grant must not depend on the legacy sink")
|
|
for adapter in (
|
|
"engineCredentialSinkN8nAdapter",
|
|
"engineDataProductPublishGrantN8nAdapter",
|
|
):
|
|
if adapter not in n8n_route:
|
|
die(f"Engine data product publish grant adapter is missing: {adapter}")
|
|
|
|
gateway = (payload_dir / "nodedc-source/server/routes/engineAgentGateway.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
for tool in (
|
|
"engine_plan_data_product_publish_grant",
|
|
"engine_apply_data_product_publish_grant",
|
|
"engine_accept_data_product_publish_grant",
|
|
"engine_rollback_data_product_publish_grant",
|
|
):
|
|
if tool not in gateway:
|
|
die(f"Engine data product publish grant tool is missing: {tool}")
|
|
|
|
catalog_path = payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL
|
|
if sha256_file(catalog_path) == ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256:
|
|
catalog = read_strict_json(catalog_path, "Engine composite provider security catalog")
|
|
packages = catalog.get("packages") if isinstance(catalog, dict) else None
|
|
provider = packages[0] if isinstance(packages, list) and len(packages) == 1 else None
|
|
capabilities = provider.get("capabilities") if isinstance(provider, dict) else None
|
|
if (
|
|
catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1"
|
|
or not isinstance(provider, dict)
|
|
or provider.get("id") != "gelios.provider.v4"
|
|
or provider.get("version") != "4.0.0"
|
|
or provider.get("providerId") != "gelios"
|
|
or provider.get("providerCredential") != {
|
|
"authModeId": "gelios.rest-rotating-bearer.v3",
|
|
"credentialType": "httpBearerAuth",
|
|
}
|
|
or capabilities != [{
|
|
"id": "gelios.monitoring_config.current.read",
|
|
"classification": "read",
|
|
"status": "implemented",
|
|
"request": {
|
|
"method": "GET",
|
|
"url": "https://api.geliospro.com/api/v1/users/me/monitoring-config",
|
|
},
|
|
"dataProductIds": ["fleet.positions.current.v3"],
|
|
}, {
|
|
"id": "gelios.units.current.read",
|
|
"classification": "read",
|
|
"status": "implemented",
|
|
"request": {
|
|
"method": "GET",
|
|
"url": "https://api.geliospro.com/api/v1/units?incltrip=true",
|
|
},
|
|
"dataProductIds": ["fleet.positions.current.v3"],
|
|
}]
|
|
or provider.get("publisher") != {
|
|
"nodeType": "n8n-nodes-ndc.ndcDataProductPublish",
|
|
"credentialType": "ndcDataProductWriterApi",
|
|
}
|
|
):
|
|
die("Engine composite provider security catalog projection mismatch")
|
|
provider_catalog = (
|
|
payload_dir / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js"
|
|
).read_text(encoding="utf-8")
|
|
publish_service = (
|
|
payload_dir / "nodedc-source/server/dataProductPublishGrant/service.js"
|
|
).read_text(encoding="utf-8")
|
|
publish_store = (
|
|
payload_dir / "nodedc-source/server/dataProductPublishGrant/store.js"
|
|
).read_text(encoding="utf-8")
|
|
if any(
|
|
marker not in provider_catalog
|
|
for marker in (
|
|
"function capabilityRequests(capability)",
|
|
"function exactHttpRequestUrl(n8n)",
|
|
"capabilityIds",
|
|
"providerRequestNodeIds",
|
|
"providerCredentialRefs.size !== 1",
|
|
)
|
|
):
|
|
die("Engine composite provider resolver contract mismatch")
|
|
if any(
|
|
marker not in publish_service
|
|
for marker in (
|
|
"capabilities: descriptor.capabilityIds",
|
|
"providerRequestNodeIds: descriptor.providerRequestNodeIds",
|
|
)
|
|
):
|
|
die("Engine composite provider plan projection mismatch")
|
|
if any(
|
|
marker not in publish_store
|
|
for marker in (
|
|
"Array.isArray(raw.capabilityIds)",
|
|
"Array.isArray(raw.providerRequestNodeIds)",
|
|
"new Set(descriptor.providerRequestNodeIds).size",
|
|
)
|
|
):
|
|
die("Engine composite provider durable descriptor mismatch")
|
|
validate_no_lifecycle_scripts(payload_dir, "Engine data product publish grant")
|
|
|
|
|
|
def validate_engine_composite_provider_v4_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES:
|
|
die("Engine composite provider v4 files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine composite provider v4 target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine composite provider v4 target sha256 mismatch: {rel}")
|
|
|
|
catalog = read_strict_json(
|
|
payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL,
|
|
"Engine composite provider v4 catalog",
|
|
)
|
|
packages = catalog.get("packages") if isinstance(catalog, dict) else None
|
|
provider = packages[0] if isinstance(packages, list) and len(packages) == 1 else None
|
|
capabilities = provider.get("capabilities") if isinstance(provider, dict) else None
|
|
if (
|
|
catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1"
|
|
or not isinstance(provider, dict)
|
|
or provider.get("id") != "gelios.provider.v4"
|
|
or provider.get("version") != "4.0.0"
|
|
or provider.get("providerCredential") != {
|
|
"authModeId": "gelios.rest-rotating-bearer.v3",
|
|
"credentialType": "httpBearerAuth",
|
|
}
|
|
or not isinstance(capabilities, list)
|
|
or [item.get("id") for item in capabilities if isinstance(item, dict)] != [
|
|
"gelios.monitoring_config.current.read",
|
|
"gelios.units.current.read",
|
|
]
|
|
or [item.get("dataProductIds") for item in capabilities if isinstance(item, dict)] != [
|
|
["fleet.positions.current.v3"],
|
|
["fleet.positions.current.v3"],
|
|
]
|
|
or [
|
|
(item.get("request") or {}).get("url")
|
|
for item in capabilities
|
|
if isinstance(item, dict)
|
|
] != [
|
|
"https://api.geliospro.com/api/v1/users/me/monitoring-config",
|
|
"https://api.geliospro.com/api/v1/units?incltrip=true",
|
|
]
|
|
):
|
|
die("Engine composite provider v4 projection mismatch")
|
|
|
|
source_markers = {
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": (
|
|
"function exactHttpRequestUrl(n8n)",
|
|
"const capabilityIds = productCapabilities.map",
|
|
"providerCredentialRefs.size !== 1",
|
|
),
|
|
"nodedc-source/server/dataProductPublishGrant/service.js": (
|
|
"capabilities: descriptor.capabilityIds",
|
|
"providerRequestNodeIds: descriptor.providerRequestNodeIds",
|
|
),
|
|
"nodedc-source/server/dataProductPublishGrant/store.js": (
|
|
"Array.isArray(raw.capabilityIds)",
|
|
"Array.isArray(raw.providerRequestNodeIds)",
|
|
),
|
|
}
|
|
for rel, markers in source_markers.items():
|
|
source = (payload_dir / rel).read_text(encoding="utf-8")
|
|
if any(marker not in source for marker in markers):
|
|
die(f"Engine composite provider v4 source contract mismatch: {rel}")
|
|
|
|
|
|
def validate_engine_provider_rotating_slot_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES:
|
|
die("Engine provider rotating slot files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine provider rotating slot target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine provider rotating slot target sha256 mismatch: {rel}")
|
|
|
|
catalog = read_strict_json(
|
|
payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL,
|
|
"Engine provider rotating slot catalog",
|
|
)
|
|
packages = catalog.get("packages") if isinstance(catalog, dict) else None
|
|
provider = packages[0] if isinstance(packages, list) and len(packages) == 1 else None
|
|
if (
|
|
not isinstance(provider, dict)
|
|
or provider.get("id") != "gelios.provider.v4"
|
|
or provider.get("providerCredential") != {
|
|
"authModeId": "gelios.rest-rotating-bearer.v3",
|
|
"credentialType": "ndcProviderRotatingAccessApi",
|
|
}
|
|
):
|
|
die("Engine provider rotating slot catalog projection mismatch")
|
|
|
|
resolver = (
|
|
payload_dir / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js"
|
|
).read_text(encoding="utf-8")
|
|
if (
|
|
"'ndcProviderRotatingAccessApi'" not in resolver
|
|
or "n8n.credentials?.[providerPackage.providerCredential.credentialType]" not in resolver
|
|
or "nodedcAgentCredentialPolicies?.[providerPackage.providerCredential.credentialType]" not in resolver
|
|
):
|
|
die("Engine provider rotating slot resolver contract mismatch")
|
|
|
|
|
|
def validate_engine_provider_authority_diagnostics_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES:
|
|
die("Engine provider authority diagnostics files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine provider authority diagnostics target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine provider authority diagnostics target sha256 mismatch: {rel}")
|
|
source = (
|
|
payload_dir / "nodedc-source/server/nodeIntelligence/upstreamProjection.js"
|
|
).read_text(encoding="utf-8")
|
|
required_markers = (
|
|
"isUnknownPrivateNodeIssue",
|
|
"issue?.nodeName || issue?.node",
|
|
"privateResults",
|
|
"NDC_PRIVATE_NODE_VALIDATED_BY_ENGINE_CATALOG",
|
|
)
|
|
if any(marker not in source for marker in required_markers):
|
|
die("Engine private node validation reconciliation contract mismatch")
|
|
descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
"Engine private node validation reconciliation descriptor",
|
|
)
|
|
if (
|
|
descriptor.get("action") != "activate"
|
|
or descriptor.get("releaseId") != "2.33.2-974a9fb3492f"
|
|
or descriptor.get("source", {}).get("gatewaySha256")
|
|
!= "5331f6dc8dc306f641370a2968eb025ee3e278e27ae9a217e4cfc2901fec369c"
|
|
or descriptor.get("source", {}).get("upstreamProjectionSha256")
|
|
!= ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256[
|
|
"nodedc-source/server/nodeIntelligence/upstreamProjection.js"
|
|
]
|
|
):
|
|
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):
|
|
if tuple(entries) != ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES:
|
|
die("Engine Depttrans zone authority v1 files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine Depttrans zone authority v1 target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine Depttrans zone authority v1 target sha256 mismatch: {rel}")
|
|
|
|
catalog = read_strict_json(
|
|
payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL,
|
|
"Engine Depttrans zone authority v1 catalog",
|
|
)
|
|
packages = catalog.get("packages") if isinstance(catalog, dict) else None
|
|
package_ids = [item.get("id") for item in packages if isinstance(item, dict)] \
|
|
if isinstance(packages, list) else None
|
|
provider = next(
|
|
(
|
|
item for item in packages
|
|
if isinstance(item, dict)
|
|
and item.get("id") == "moscow-department-of-transport.pmd-slow-zones.v1"
|
|
),
|
|
None,
|
|
) if isinstance(packages, list) else None
|
|
capability = provider.get("capabilities", [None])[0] if isinstance(provider, dict) else None
|
|
request = capability.get("request") if isinstance(capability, dict) else None
|
|
marker = read_strict_json(
|
|
payload_dir / "nodedc-source/server/assets/provider-packages/v1/depttrans-zone-authority-v1.json",
|
|
"Engine Depttrans zone authority v1 marker",
|
|
)
|
|
if (
|
|
catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1"
|
|
or package_ids != [
|
|
"gelios.provider.v1",
|
|
"gelios.provider.v4",
|
|
"gelios.provider.v5",
|
|
"moscow-department-of-transport.pmd-slow-zones.v1",
|
|
]
|
|
or provider.get("providerId") != "moscow-department-of-transport"
|
|
or provider.get("version") != "1.0.0"
|
|
or "providerCredential" in provider
|
|
or capability.get("id") != "depttrans.pmd-slow-zones.current.read"
|
|
or capability.get("dataProductIds") != ["map.zones.current.v2"]
|
|
or request != {
|
|
"method": "GET",
|
|
"url": "http://map-gateway:18103/internal/zone-sources/v1/profiles/moscow-pmd-slow-zones/current",
|
|
"authorityBoundary": "platform-service",
|
|
"serviceId": "nodedc-map-gateway",
|
|
"network": "engine",
|
|
}
|
|
or marker != {
|
|
"schemaVersion": "nodedc.engine.platform-service-authority/v1",
|
|
"id": "moscow-department-of-transport.pmd-slow-zones.v1",
|
|
"providerId": "moscow-department-of-transport",
|
|
"authorityBoundary": "platform-service",
|
|
"serviceId": "nodedc-map-gateway",
|
|
"network": "engine",
|
|
"dataProductId": "map.zones.current.v2",
|
|
}
|
|
):
|
|
die("Engine Depttrans zone authority v1 catalog projection mismatch")
|
|
|
|
required_markers = {
|
|
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": (
|
|
"function platformServiceRequestIsExact",
|
|
"authorityBoundary === 'platform-service'",
|
|
"platformServiceIds",
|
|
),
|
|
"nodedc-source/server/dataProductPublishGrant/service.js": (
|
|
"pinned_platform_service_no_graph_credential",
|
|
"platformServiceIds: descriptor.platformServiceIds",
|
|
),
|
|
"nodedc-source/server/dataProductPublishGrant/store.js": (
|
|
"new Set(['provider-credential', 'platform-service'])",
|
|
"descriptor.authorityBoundary === 'platform-service'",
|
|
),
|
|
}
|
|
for rel, markers in required_markers.items():
|
|
source = (payload_dir / rel).read_text(encoding="utf-8")
|
|
if any(marker not in source for marker in markers):
|
|
die(f"Engine Depttrans zone authority v1 source contract mismatch: {rel}")
|
|
|
|
|
|
def validate_engine_provider_target_host_policy_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES:
|
|
die("Engine provider target host policy files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine provider target host policy target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine provider target host policy target sha256 mismatch: {rel}")
|
|
source = (payload_dir / "nodedc-source/server/routes/n8n.js").read_text(encoding="utf-8")
|
|
required_markers = (
|
|
"const eligibleHosts = explicitHosts.length",
|
|
"allowedHosts: [targetHost]",
|
|
"credential_host_not_allowed",
|
|
)
|
|
if any(marker not in source for marker in required_markers):
|
|
die("Engine provider target host policy contract mismatch")
|
|
|
|
|
|
def validate_engine_mcp_execution_profile_decoder_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_EXECUTION_PROFILE_DECODER_ARTIFACT_ENTRIES:
|
|
die("Engine MCP execution profile decoder files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_MCP_EXECUTION_PROFILE_DECODER_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP execution profile decoder target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine MCP execution profile decoder target sha256 mismatch: {rel}")
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL,
|
|
"Engine MCP execution profile decoder descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-execution-profile-decoder-v1",
|
|
"component": "engine",
|
|
"scope": "external-mcp-observability",
|
|
"sourcePath": "nodedc-source/server/routes/n8n.js",
|
|
"behavior": "preserve-top-level-numeric-string-primitives-in-flatted-execution-data",
|
|
"acceptance": {
|
|
"tool": "engine_get_node_output_profile",
|
|
"valuesIncluded": False,
|
|
"rawExecutionDataIncluded": False,
|
|
},
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP execution profile decoder descriptor contract mismatch")
|
|
source = (payload_dir / "nodedc-source/server/routes/n8n.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
required_markers = (
|
|
"const compactReference = Symbol('n8nCompactReference')",
|
|
"Top-level string entries are primitive values.",
|
|
"valuesIncluded: false,",
|
|
)
|
|
if any(marker not in source for marker in required_markers):
|
|
die("Engine MCP execution profile decoder source contract mismatch")
|
|
|
|
|
|
def validate_engine_mcp_telemetry_catalog_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_TELEMETRY_CATALOG_ARTIFACT_ENTRIES:
|
|
die("Engine MCP telemetry catalog files.txt exact set/order mismatch")
|
|
for rel, expected_sha256 in ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256.items():
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP telemetry catalog target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine MCP telemetry catalog target sha256 mismatch: {rel}")
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL,
|
|
"Engine MCP telemetry catalog descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-telemetry-reading-catalog-v1",
|
|
"component": "engine",
|
|
"scope": "external-mcp-observability",
|
|
"mcpVersion": "0.8.0",
|
|
"sourcePaths": [
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
],
|
|
"readsOnly": "normalized-attributes.sensor_readings",
|
|
"returns": [
|
|
"reading-id",
|
|
"safe-label",
|
|
"safe-unit",
|
|
"value-types",
|
|
"coverage-counts",
|
|
],
|
|
"neverReturns": [
|
|
"reading-value",
|
|
"raw-execution-data",
|
|
"raw-provider-payload",
|
|
"credential-shaped-data",
|
|
],
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP telemetry catalog descriptor contract mismatch")
|
|
n8n_source = (payload_dir / "nodedc-source/server/routes/n8n.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
gateway_source = (
|
|
payload_dir / "nodedc-source/server/routes/engineAgentGateway.js"
|
|
).read_text(encoding="utf-8")
|
|
if any(
|
|
marker not in n8n_source
|
|
for marker in (
|
|
"function toSafeTelemetryReadingCatalog",
|
|
"key === 'sensor_readings' && pathName.endsWith('.attributes')",
|
|
"valuesIncluded: false,",
|
|
"rawExecutionDataIncluded: false,",
|
|
)
|
|
):
|
|
die("Engine MCP telemetry catalog backend contract mismatch")
|
|
if any(
|
|
marker not in gateway_source
|
|
for marker in (
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.8.0'",
|
|
"name: 'engine_get_telemetry_reading_catalog'",
|
|
"/telemetry-reading-catalog?",
|
|
)
|
|
):
|
|
die("Engine MCP telemetry catalog gateway contract mismatch")
|
|
node_intelligence_descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
"Engine MCP telemetry catalog node-intelligence descriptor",
|
|
)
|
|
if (
|
|
node_intelligence_descriptor.get("action") != "activate"
|
|
or node_intelligence_descriptor.get("releaseId")
|
|
!= ENGINE_NODE_INTELLIGENCE_RELEASE_ID
|
|
or node_intelligence_descriptor.get("source", {}).get("gatewaySha256")
|
|
!= ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256[
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
|
]
|
|
):
|
|
die(
|
|
"Engine MCP telemetry catalog node-intelligence attestation "
|
|
"contract mismatch"
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_execution_plan_materialization_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_ARTIFACT_ENTRIES:
|
|
die(
|
|
"Engine MCP execution plan materialization files.txt exact "
|
|
"set/order mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP execution plan materialization target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP execution plan materialization target sha256 "
|
|
f"mismatch: {rel}"
|
|
)
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_DESCRIPTOR_REL,
|
|
"Engine MCP execution plan materialization descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-l2-execution-plan-materialization-v1",
|
|
"component": "engine",
|
|
"scope": "external-mcp-l2-authoring",
|
|
"mcpVersion": "0.9.0",
|
|
"sourcePaths": [
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/l2ExecutionPlan/catalog.js",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
],
|
|
"plan": {
|
|
"tool": "engine_plan_l2_execution_plan_materialization",
|
|
"readOnly": True,
|
|
"returnsPlanRef": True,
|
|
},
|
|
"apply": {
|
|
"tool": "engine_apply_l2_execution_plan_materialization",
|
|
"accepts": [
|
|
"planRef",
|
|
"providerAuthRef",
|
|
"confirmation",
|
|
"changeRef",
|
|
],
|
|
"rejectsCallerGraphOperations": True,
|
|
"revalidatesTargetRevisionAndScope": True,
|
|
},
|
|
"providerLogicAuthority": "trusted-provider-package",
|
|
"unmanagedGraphPolicy": "explicit-adoption-required",
|
|
"embeddedCodexChanged": False,
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP execution plan materialization descriptor contract mismatch")
|
|
gateway_source = (
|
|
payload_dir / "nodedc-source/server/routes/engineAgentGateway.js"
|
|
).read_text(encoding="utf-8")
|
|
if any(
|
|
marker not in gateway_source
|
|
for marker in (
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.9.0'",
|
|
"name: 'engine_plan_l2_execution_plan_materialization'",
|
|
"name: 'engine_apply_l2_execution_plan_materialization'",
|
|
"'execution-plan.apply'",
|
|
"assertExactToolArguments(args, [",
|
|
)
|
|
):
|
|
die("Engine MCP execution plan materialization gateway contract mismatch")
|
|
materializer_source = (
|
|
payload_dir / "nodedc-source/server/l2ExecutionPlan/materializer.js"
|
|
).read_text(encoding="utf-8")
|
|
compiler_source = (
|
|
payload_dir / "nodedc-source/server/l2ExecutionPlan/compiler.js"
|
|
).read_text(encoding="utf-8")
|
|
if any(
|
|
marker not in materializer_source
|
|
for marker in (
|
|
"status: 'adoption_required'",
|
|
"type: 'replaceGraph'",
|
|
"assignCredentialBindingInternal",
|
|
"execution_plan_materialization_equality_failed",
|
|
)
|
|
):
|
|
die("Engine MCP execution plan materializer contract mismatch")
|
|
if any(
|
|
marker not in compiler_source
|
|
for marker in (
|
|
"compileExecutionPlanGraph",
|
|
"materializedGraphEqualityDigest",
|
|
"ndc.semantic-mapping",
|
|
"providerPackage.publisher.nodeType",
|
|
)
|
|
):
|
|
die("Engine MCP execution plan compiler contract mismatch")
|
|
if re.search(r"gelios|robot2b", materializer_source, re.IGNORECASE) or re.search(
|
|
r"gelios|robot2b",
|
|
compiler_source,
|
|
re.IGNORECASE,
|
|
):
|
|
die("Engine MCP execution plan runtime contains provider hardcode")
|
|
node_intelligence_descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
"Engine MCP execution plan materialization node-intelligence descriptor",
|
|
)
|
|
if (
|
|
node_intelligence_descriptor.get("action") != "activate"
|
|
or node_intelligence_descriptor.get("releaseId")
|
|
!= ENGINE_NODE_INTELLIGENCE_RELEASE_ID
|
|
or node_intelligence_descriptor.get("source", {}).get("gatewaySha256")
|
|
!= ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_TARGET_SHA256[
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
|
]
|
|
):
|
|
die(
|
|
"Engine MCP execution plan materialization node-intelligence "
|
|
"attestation contract mismatch"
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
payload_dir,
|
|
entries,
|
|
):
|
|
if (
|
|
tuple(entries)
|
|
!= ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_ARTIFACT_ENTRIES
|
|
):
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime files.txt exact "
|
|
"set/order mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime target is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime target sha256 "
|
|
f"mismatch: {rel}"
|
|
)
|
|
descriptor = read_strict_json(
|
|
payload_dir
|
|
/ ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_DESCRIPTOR_REL,
|
|
"Engine MCP execution plan telemetry runtime descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-l2-execution-plan-telemetry-runtime-v2",
|
|
"component": "engine",
|
|
"scope": "external-mcp-l2-authoring-runtime",
|
|
"mcpVersion": "0.9.0",
|
|
"sourcePaths": [
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
],
|
|
"compilerTransition": {
|
|
"predecessor": "1.1.0",
|
|
"supported": ["1.1.0", "1.2.0"],
|
|
"legacyRuntimePreserved": True,
|
|
},
|
|
"telemetryInputBoundary": {
|
|
"sources": [
|
|
"declared-sensor-values",
|
|
"declared-message-parameters",
|
|
],
|
|
"authorization": [
|
|
"trusted-telemetry-projection",
|
|
"visible-sensor-definition",
|
|
],
|
|
"unprojectedParameters": "discarded",
|
|
"rawProviderPayloadAtPublish": "forbidden",
|
|
"conversion": "bounded-declared-conversion-table",
|
|
},
|
|
"providerLogicAuthority": "trusted-provider-package",
|
|
"engineProviderHardcode": False,
|
|
"embeddedCodexChanged": False,
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime descriptor "
|
|
"contract mismatch"
|
|
)
|
|
compiler_source = (
|
|
payload_dir / "nodedc-source/server/l2ExecutionPlan/compiler.js"
|
|
).read_text(encoding="utf-8")
|
|
catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"Engine execution plan catalog",
|
|
max_bytes=512 * 1024,
|
|
)
|
|
required_markers = (
|
|
"if (compilerVersion === '1.1.0')",
|
|
"if (compilerVersion !== '1.2.0')",
|
|
'"msgParam", "msg_param"',
|
|
"descriptor.telemetryProjection",
|
|
'"message-param"',
|
|
"convertedSensorValue",
|
|
"visibleSensorDefinition",
|
|
)
|
|
if any(marker not in compiler_source for marker in required_markers):
|
|
die("Engine MCP execution plan telemetry runtime contract mismatch")
|
|
if catalog.get("runtime", {}).get("compilerVersions") != ["1.1.0", "1.2.0"]:
|
|
die("Engine MCP execution plan telemetry runtime catalog mismatch")
|
|
if re.search(r"gelios|robot2b", compiler_source, re.IGNORECASE):
|
|
die("Engine MCP execution plan telemetry runtime contains provider hardcode")
|
|
|
|
|
|
def validate_engine_mcp_execution_plan_module_ownership_slice(
|
|
payload_dir,
|
|
entries,
|
|
):
|
|
if (
|
|
tuple(entries)
|
|
!= ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_ARTIFACT_ENTRIES
|
|
):
|
|
die(
|
|
"Engine MCP execution plan module ownership files.txt exact "
|
|
"set/order mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan module ownership target is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP execution plan module ownership target sha256 "
|
|
f"mismatch: {rel}"
|
|
)
|
|
descriptor = read_strict_json(
|
|
payload_dir
|
|
/ ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_DESCRIPTOR_REL,
|
|
"Engine MCP execution plan module ownership descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-l2-execution-plan-module-ownership-v3",
|
|
"component": "engine",
|
|
"scope": "external-mcp-l2-authoring-runtime",
|
|
"mcpVersion": "0.10.0",
|
|
"sourcePaths": [
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
],
|
|
"moduleOwnership": {
|
|
"schemaVersion":
|
|
"nodedc.engine.materialized-execution-plan-module/v1",
|
|
"adoptionStrategy": "adopt_existing_module",
|
|
"reconciliationStrategy": "create_or_reconcile_owned",
|
|
"nodeBindings": "exact-one-to-one",
|
|
"retirementScope": "closed-module-boundary",
|
|
"unrelatedNodes": "preserved",
|
|
"unrelatedEdges": "preserved",
|
|
"internalEdges": "server-derived-from-trusted-plan",
|
|
"wholeGraphReplacement": "not-required",
|
|
},
|
|
"sharedBoundary": {
|
|
"runtimeKinds": ["ndc.manual-trigger"],
|
|
"nodeTypes": ["n8n-nodes-base.webhook"],
|
|
"compatibility": "same-node-type-and-http-method",
|
|
"configurationAuthority": "existing-graph",
|
|
"providerCredentialNodes": "must-be-engine-managed",
|
|
"publisherNodes": "must-be-engine-managed",
|
|
},
|
|
"providerPackageTrust": {
|
|
"addedPackageId": "gelios.provider.v9",
|
|
"addedPackageDigest":
|
|
"sha256:7dd4ce4a45ce76e884ffa1e304bfaa521f552e9a45e535930f2d17b882ae23ed",
|
|
"legacyPackageId": "gelios.provider.v8",
|
|
"legacyPackagePreserved": True,
|
|
"telemetryRegistryDigest":
|
|
"sha256:8338dd1f3e757dae5a0fc558e41b3a5cc6395c33c94841ce4d2af976e1911381",
|
|
"telemetryProjectionDigest":
|
|
"sha256:ecdafb13173338f10043a08603ddd5cd1f6091f1c54d7fbb7e8a9606fbbdef65",
|
|
"providerLogicAuthority": "trusted-provider-package",
|
|
},
|
|
"engineProviderHardcode": False,
|
|
"embeddedCodexChanged": False,
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die(
|
|
"Engine MCP execution plan module ownership descriptor "
|
|
"contract mismatch"
|
|
)
|
|
materializer_source = (
|
|
payload_dir / "nodedc-source/server/l2ExecutionPlan/materializer.js"
|
|
).read_text(encoding="utf-8")
|
|
gateway_source = (
|
|
payload_dir / "nodedc-source/server/routes/engineAgentGateway.js"
|
|
).read_text(encoding="utf-8")
|
|
for marker in (
|
|
"nodedc.engine.materialized-execution-plan-module/v1",
|
|
"adopt_existing_module",
|
|
"preserveBoundNodeIds",
|
|
"execution_plan_module_retire_boundary_not_closed",
|
|
"manual_webhook_same_method",
|
|
):
|
|
if marker not in materializer_source:
|
|
die(
|
|
"Engine MCP execution plan module ownership contract "
|
|
f"marker missing: {marker}"
|
|
)
|
|
if (
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.10.0'" not in gateway_source
|
|
or "preserveBoundNodeIds" not in gateway_source
|
|
or "adopt_existing_module" not in gateway_source
|
|
):
|
|
die("Engine MCP execution plan module ownership gateway mismatch")
|
|
if re.search(r"gelios|robot2b", materializer_source, re.IGNORECASE):
|
|
die("Engine MCP execution plan module ownership contains provider hardcode")
|
|
catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"Engine execution plan catalog",
|
|
max_bytes=512 * 1024,
|
|
)
|
|
package_by_id = {
|
|
package.get("id"): package
|
|
for package in catalog.get("packages", [])
|
|
if isinstance(package, dict)
|
|
}
|
|
if (
|
|
package_by_id.get("gelios.provider.v9", {}).get("contractDigest")
|
|
!= "sha256:7dd4ce4a45ce76e884ffa1e304bfaa521f552e9a45e535930f2d17b882ae23ed"
|
|
or "gelios.provider.v8" not in package_by_id
|
|
):
|
|
die("Engine MCP execution plan module ownership catalog mismatch")
|
|
node_intelligence_descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
"Engine MCP execution plan module ownership node-intelligence descriptor",
|
|
)
|
|
if (
|
|
node_intelligence_descriptor.get("action") != "activate"
|
|
or node_intelligence_descriptor.get("releaseId")
|
|
!= ENGINE_NODE_INTELLIGENCE_RELEASE_ID
|
|
or node_intelligence_descriptor.get("source", {}).get("gatewaySha256")
|
|
!= ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_TARGET_SHA256[
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
|
]
|
|
):
|
|
die(
|
|
"Engine MCP execution plan module ownership node-intelligence "
|
|
"attestation contract mismatch"
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_normalized_identity_search_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_ARTIFACT_ENTRIES:
|
|
die(
|
|
"Engine MCP normalized identity search files.txt exact set/order "
|
|
"mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP normalized identity search target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP normalized identity search target sha256 mismatch: "
|
|
f"{rel}"
|
|
)
|
|
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_DESCRIPTOR_REL,
|
|
"Engine MCP normalized identity search descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-normalized-identity-search-v1",
|
|
"component": "engine",
|
|
"scope": "external-mcp-readonly-identity-discovery",
|
|
"mcpVersion": "0.11.0",
|
|
"sourcePaths": [
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
],
|
|
"normalizedFactSearch": {
|
|
"tool": "engine_find_normalized_subjects",
|
|
"authority": "granted-execution-normalized-facts",
|
|
"matchModes": ["exact", "prefix", "contains"],
|
|
"queryScopes": ["canonical-fields", "point-bounding-box"],
|
|
"fullAdministrativeIdentifiers": True,
|
|
"rawExecutionData": False,
|
|
"rawProviderPayload": False,
|
|
"commandSurface": False,
|
|
},
|
|
"executionPlanRuntime": {
|
|
"compilerVersionAdded": "1.4.0",
|
|
"legacyCompilerVersionsPreserved": ["1.1.0", "1.2.0", "1.3.0"],
|
|
"derivationKindsAdded": [
|
|
"bounded_string_list",
|
|
"bounded_named_values",
|
|
],
|
|
"providerSpecificLogicInEngine": False,
|
|
},
|
|
"providerPackageTrust": {
|
|
"addedPackageId": "gelios.provider.v11",
|
|
"addedPackageDigest":
|
|
"sha256:4bf211d9ed6cfd227df444d49d8c5ce77a24389ed2ba3d159d53d83e0d4738d9",
|
|
"identityCapabilityId": "gelios.units.identity.read",
|
|
"identityDataProductId": "fleet.units.identity.current.v1",
|
|
"legacyPackagesPreserved": True,
|
|
},
|
|
"reverseGeocodingChanged": False,
|
|
"embeddedCodexChanged": False,
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP normalized identity search descriptor contract mismatch")
|
|
|
|
gateway_source = (
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
|
).read_text(encoding="utf-8")
|
|
route_source = (
|
|
payload_dir / "nodedc-source/server/routes/n8n.js"
|
|
).read_text(encoding="utf-8")
|
|
compiler_source = (
|
|
payload_dir / "nodedc-source/server/l2ExecutionPlan/compiler.js"
|
|
).read_text(encoding="utf-8")
|
|
for marker in (
|
|
"engine_find_normalized_subjects",
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.11.0'",
|
|
):
|
|
if marker not in gateway_source:
|
|
die(
|
|
"Engine MCP normalized identity search gateway marker missing: "
|
|
f"{marker}"
|
|
)
|
|
for marker in (
|
|
"normalized-fact-search",
|
|
"normalizedFactsOnly: true",
|
|
"rawExecutionDataIncluded: false",
|
|
"commandSurfaceIncluded: false",
|
|
):
|
|
if marker not in route_source:
|
|
die(
|
|
"Engine MCP normalized identity search route marker missing: "
|
|
f"{marker}"
|
|
)
|
|
for marker in ("boundedStringList", "boundedNamedValues", "'1.4.0'"):
|
|
if marker not in compiler_source:
|
|
die(
|
|
"Engine MCP normalized identity search compiler marker missing: "
|
|
f"{marker}"
|
|
)
|
|
if re.search(r"gelios|robot2b", compiler_source, re.IGNORECASE):
|
|
die("Engine MCP normalized identity search contains provider hardcode")
|
|
|
|
execution_catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"Engine execution plan catalog",
|
|
max_bytes=1024 * 1024,
|
|
)
|
|
package_by_id = {
|
|
package.get("id"): package
|
|
for package in execution_catalog.get("packages", [])
|
|
if isinstance(package, dict)
|
|
}
|
|
gelios_v11 = package_by_id.get("gelios.provider.v11", {})
|
|
if (
|
|
execution_catalog.get("runtime", {}).get("compilerVersions")
|
|
!= ["1.1.0", "1.2.0", "1.3.0", "1.4.0"]
|
|
or gelios_v11.get("contractDigest")
|
|
!= "sha256:4bf211d9ed6cfd227df444d49d8c5ce77a24389ed2ba3d159d53d83e0d4738d9"
|
|
or not any(
|
|
profile.get("dataProductId") == "fleet.units.identity.current.v1"
|
|
for profile in gelios_v11.get("profiles", [])
|
|
if isinstance(profile, dict)
|
|
)
|
|
):
|
|
die("Engine MCP normalized identity search execution catalog mismatch")
|
|
|
|
security_catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"Engine provider security catalog",
|
|
max_bytes=512 * 1024,
|
|
)
|
|
security_package = next(
|
|
(
|
|
package
|
|
for package in security_catalog.get("packages", [])
|
|
if isinstance(package, dict)
|
|
and package.get("id") == "gelios.provider.v11"
|
|
),
|
|
{},
|
|
)
|
|
capabilities = security_package.get("capabilities", [])
|
|
if (
|
|
len(capabilities) != 1
|
|
or capabilities[0].get("id") != "gelios.units.identity.read"
|
|
or capabilities[0].get("classification") != "read"
|
|
or capabilities[0].get("dataProductIds")
|
|
!= ["fleet.units.identity.current.v1"]
|
|
):
|
|
die("Engine MCP normalized identity search security catalog mismatch")
|
|
|
|
node_intelligence_descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
"Engine MCP normalized identity search node-intelligence descriptor",
|
|
)
|
|
if (
|
|
node_intelligence_descriptor.get("action") != "activate"
|
|
or node_intelligence_descriptor.get("releaseId")
|
|
!= ENGINE_NODE_INTELLIGENCE_RELEASE_ID
|
|
or node_intelligence_descriptor.get("source", {}).get("gatewaySha256")
|
|
!= ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_TARGET_SHA256[
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
|
]
|
|
):
|
|
die(
|
|
"Engine MCP normalized identity search node-intelligence "
|
|
"attestation contract mismatch"
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_l1_credential_reuse_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_L1_CREDENTIAL_REUSE_ARTIFACT_ENTRIES:
|
|
die(
|
|
"Engine MCP L1 credential reuse files.txt exact set/order "
|
|
"mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP L1 credential reuse target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP L1 credential reuse target sha256 mismatch: "
|
|
f"{rel}"
|
|
)
|
|
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_L1_CREDENTIAL_REUSE_DESCRIPTOR_REL,
|
|
"Engine MCP L1 credential reuse descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-l1-credential-reuse-v1",
|
|
"component": "engine",
|
|
"scope": "external-mcp-l2-authoring",
|
|
"mcpVersion": "0.11.0",
|
|
"sourcePath": "nodedc-source/server/routes/n8n.js",
|
|
"visibilityProof": {
|
|
"scope": "same-l1-workflow",
|
|
"source": "local-registry-provenance",
|
|
"instanceScope": "preserved",
|
|
},
|
|
"binding": {
|
|
"listTool": "engine_list_l2_credential_refs",
|
|
"applyOperation": "assignCredentialRef",
|
|
"returns": ["label", "status", "scope", "opaque-ref"],
|
|
"neverReturns": [
|
|
"native-credential-id",
|
|
"logical-credential-id",
|
|
"credential-data",
|
|
"credential-value",
|
|
],
|
|
},
|
|
"transportPolicy": {
|
|
"httpsRequired": True,
|
|
"redirectsDisabled": True,
|
|
"hostAuthority": [
|
|
"credential-allowlist",
|
|
"same-l1-observed-host",
|
|
],
|
|
},
|
|
"managedGrants": "target-local",
|
|
"crossL1Sharing": False,
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP L1 credential reuse descriptor contract mismatch")
|
|
|
|
route_source = (
|
|
payload_dir / "nodedc-source/server/routes/n8n.js"
|
|
).read_text(encoding="utf-8")
|
|
for marker in (
|
|
"function engineAgentCredentialMayReuseWithinL1",
|
|
"function buildEngineAgentL1CredentialContext",
|
|
"return 'l1'",
|
|
"candidateScope === 'l1' ? l1Context.graph : graph",
|
|
"managed writer/reader grants",
|
|
):
|
|
if marker not in route_source:
|
|
die(
|
|
"Engine MCP L1 credential reuse route marker missing: "
|
|
f"{marker}"
|
|
)
|
|
if (
|
|
"LEGACY_CREDENTIAL_SINK_OWNER" not in route_source
|
|
or "DATA_PRODUCT_PUBLISH_GRANT_OWNER" not in route_source
|
|
or "DATA_PRODUCT_READ_GRANT_OWNER" not in route_source
|
|
):
|
|
die("Engine MCP L1 credential reuse managed-grant boundary missing")
|
|
|
|
|
|
def validate_engine_mcp_l1_credential_provenance_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_ARTIFACT_ENTRIES:
|
|
die(
|
|
"Engine MCP L1 credential provenance files.txt exact set/order "
|
|
"mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP L1 credential provenance target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP L1 credential provenance target sha256 mismatch: "
|
|
f"{rel}"
|
|
)
|
|
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_DESCRIPTOR_REL,
|
|
"Engine MCP L1 credential provenance descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-l1-credential-provenance-v2",
|
|
"component": "engine",
|
|
"scope": "external-mcp-l2-authoring",
|
|
"mcpVersion": "0.11.0",
|
|
"sourcePath": "nodedc-source/server/routes/n8n.js",
|
|
"predecessor": "engine-mcp-l1-credential-reuse-v1",
|
|
"visibilityProof": {
|
|
"scope": "same-l1-workflow",
|
|
"localSources": ["manual", "workflow-ref", "credentials-file"],
|
|
"referencedSourceRequiresSyncPayload": True,
|
|
"logicalKeyEqualityRequired": True,
|
|
},
|
|
"candidateBoundary": {
|
|
"commonEntryMustBeReusable": True,
|
|
"managedEntriesAllowed": False,
|
|
"managedGrants": "target-local",
|
|
},
|
|
"binding": {
|
|
"listTool": "engine_list_l2_credential_refs",
|
|
"applyOperation": "assignCredentialRef",
|
|
"opaqueRefOnly": True,
|
|
"credentialIdsReturned": False,
|
|
"credentialValuesReturned": False,
|
|
},
|
|
"transportPolicy": {
|
|
"httpsRequired": True,
|
|
"redirectsDisabled": True,
|
|
"hostAuthority": [
|
|
"credential-allowlist",
|
|
"same-l1-observed-host",
|
|
],
|
|
},
|
|
"crossL1Sharing": False,
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP L1 credential provenance descriptor contract mismatch")
|
|
|
|
route_source = (
|
|
payload_dir / "nodedc-source/server/routes/n8n.js"
|
|
).read_text(encoding="utf-8")
|
|
for marker in (
|
|
"function engineAgentCredentialMayProveL1Provenance",
|
|
"return isGlobalRegistryEntryAllowed(entry)",
|
|
"engineAgentCredentialMayProveL1Provenance(item)",
|
|
"The common candidate itself is",
|
|
):
|
|
if marker not in route_source:
|
|
die(
|
|
"Engine MCP L1 credential provenance route marker missing: "
|
|
f"{marker}"
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
payload_dir,
|
|
entries,
|
|
):
|
|
if (
|
|
tuple(entries)
|
|
!= ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_ARTIFACT_ENTRIES
|
|
):
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime files.txt exact "
|
|
"set/order mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime target is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime target sha256 "
|
|
f"mismatch: {rel}"
|
|
)
|
|
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_DESCRIPTOR_REL,
|
|
"Engine MCP execution plan sandbox runtime descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-l2-execution-plan-sandbox-runtime-v4",
|
|
"component": "engine",
|
|
"scope": "external-mcp-l2-authoring-runtime",
|
|
"mcpVersion": "0.11.0",
|
|
"sourcePath": "nodedc-source/server/l2ExecutionPlan/compiler.js",
|
|
"predecessor": "engine-mcp-l1-credential-provenance-v2",
|
|
"runtimeCompatibility": {
|
|
"n8nVersion": "2.3.2",
|
|
"codeNodeMode": "runOnceForAllItems",
|
|
"sandboxGlobalDependencies": [],
|
|
"responseByteBudget": "bounded-pure-javascript-utf8",
|
|
"returnedItems": "n8n-object-json-envelope",
|
|
},
|
|
"dataBoundary": {
|
|
"providerResponses": "bounded-by-trusted-profile",
|
|
"rawProviderPayloadAtPublish": "forbidden",
|
|
"canonicalFactsOnlyAtPublish": True,
|
|
},
|
|
"providerLogicAuthority": "trusted-provider-package",
|
|
"engineProviderHardcode": False,
|
|
"n8nCoreChanged": False,
|
|
"l1Changed": False,
|
|
"credentialsChanged": False,
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP execution plan sandbox runtime descriptor mismatch")
|
|
|
|
compiler = (
|
|
payload_dir / "nodedc-source/server/l2ExecutionPlan/compiler.js"
|
|
).read_text(encoding="utf-8")
|
|
for marker in (
|
|
"const encodedBytes = (value) => {",
|
|
"encoded.charCodeAt(index)",
|
|
"length += 4",
|
|
"return extracted.map((source) => ({ json:",
|
|
):
|
|
if marker not in compiler:
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime compiler marker "
|
|
f"missing: {marker}"
|
|
)
|
|
if (
|
|
"new TextEncoder().encode(JSON.stringify(value ?? null))" in compiler
|
|
or re.search(r"gelios|robot2b", compiler, re.IGNORECASE)
|
|
):
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime provider-neutral "
|
|
"boundary mismatch"
|
|
)
|
|
|
|
|
|
def validate_engine_mcp_gelios_items_envelope_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_ARTIFACT_ENTRIES:
|
|
die(
|
|
"Engine MCP Gelios items envelope files.txt exact set/order "
|
|
"mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP Gelios items envelope target is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP Gelios items envelope target sha256 mismatch: "
|
|
f"{rel}"
|
|
)
|
|
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_DESCRIPTOR_REL,
|
|
"Engine MCP Gelios items envelope descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-gelios-items-envelope-v12",
|
|
"component": "engine",
|
|
"scope": "external-mcp-provider-package-catalog",
|
|
"mcpVersion": "0.11.0",
|
|
"predecessor": "engine-mcp-l2-execution-plan-sandbox-runtime-v4",
|
|
"providerPackageTransition": {
|
|
"from": "gelios.provider.v11",
|
|
"to": "gelios.provider.v12",
|
|
"capabilityId": "gelios.units.identity.read",
|
|
"dataProductId": "fleet.units.identity.current.v1",
|
|
},
|
|
"responseEnvelope": {
|
|
"collectionPath": "items",
|
|
"evidence": "bounded-structural-profile",
|
|
"observedCardinality": 107,
|
|
"rawValuesCaptured": False,
|
|
},
|
|
"catalogPolicy": {
|
|
"historicalExecutionPackagePreserved": "gelios.provider.v11",
|
|
"activeSecurityAuthority": "gelios.provider.v12",
|
|
"ambiguousIdentityAuthority": "forbidden",
|
|
},
|
|
"boundaries": {
|
|
"compilerChanged": False,
|
|
"n8nCoreChanged": False,
|
|
"l1Changed": False,
|
|
"ontologyChanged": False,
|
|
"credentialsChanged": False,
|
|
"rawProviderValuesIncluded": False,
|
|
},
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP Gelios items envelope descriptor mismatch")
|
|
|
|
execution_catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"Engine MCP Gelios items envelope execution catalog",
|
|
max_bytes=2 * 1024 * 1024,
|
|
)
|
|
security_catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"Engine MCP Gelios items envelope security catalog",
|
|
max_bytes=512 * 1024,
|
|
)
|
|
execution_packages = {
|
|
item.get("id"): item
|
|
for item in execution_catalog.get("packages", [])
|
|
if isinstance(item, dict)
|
|
}
|
|
security_packages = [
|
|
item for item in security_catalog.get("packages", [])
|
|
if isinstance(item, dict)
|
|
]
|
|
gelios_v12 = execution_packages.get("gelios.provider.v12", {})
|
|
identity_profiles = [
|
|
profile for profile in gelios_v12.get("profiles", [])
|
|
if (
|
|
profile.get("id") == "gelios.units.identity.warm.v1"
|
|
and profile.get("dataProductId")
|
|
== "fleet.units.identity.current.v1"
|
|
)
|
|
]
|
|
active_identity = [
|
|
item for item in security_packages
|
|
if item.get("id") == "gelios.provider.v12"
|
|
]
|
|
if (
|
|
"gelios.provider.v11" not in execution_packages
|
|
or gelios_v12.get("version") != "12.0.0"
|
|
or len(identity_profiles) != 1
|
|
or any(item.get("id") == "gelios.provider.v11"
|
|
for item in security_packages)
|
|
or len(active_identity) != 1
|
|
or [
|
|
capability.get("id")
|
|
for capability in active_identity[0].get("capabilities", [])
|
|
] != ["gelios.units.identity.read"]
|
|
):
|
|
die("Engine MCP Gelios items envelope catalog authority mismatch")
|
|
|
|
|
|
def validate_engine_mcp_registered_execution_profiles_slice(
|
|
payload_dir,
|
|
entries,
|
|
):
|
|
if (
|
|
tuple(entries)
|
|
!= ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES
|
|
):
|
|
die(
|
|
"Engine MCP registered execution profiles files.txt exact "
|
|
"set/order mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP registered execution profiles target is missing: "
|
|
f"{rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP registered execution profiles target sha256 "
|
|
f"mismatch: {rel}"
|
|
)
|
|
|
|
descriptor = read_strict_json(
|
|
payload_dir
|
|
/ ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL,
|
|
"Engine MCP registered execution profiles descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
expected_descriptor = {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-registered-execution-profiles-v2",
|
|
"component": "engine",
|
|
"predecessor": "engine-mcp-gelios-items-envelope-v12",
|
|
"scope":
|
|
"external-mcp-registered-execution-profiles-with-node-intelligence-attestation",
|
|
"mcpVersion": "0.12.0",
|
|
"catalog": {
|
|
"schemaVersion": "nodedc.engine.execution-plan-catalog/v1",
|
|
"registeredProfiles": 6,
|
|
"profileRefs": "actor-and-target-scoped-opaque-ttl",
|
|
"executionPlanDescriptorsIncluded": False,
|
|
"providerEndpointsIncluded": False,
|
|
"customerScopeIncluded": False,
|
|
"credentialIdentitiesIncluded": False,
|
|
"credentialValuesIncluded": False,
|
|
},
|
|
"tools": {
|
|
"discovery": "engine_list_l2_execution_profiles",
|
|
"planning": "engine_plan_registered_l2_execution",
|
|
"apply": "engine_apply_l2_execution_plan_materialization",
|
|
},
|
|
"planning": {
|
|
"packageResolution": "server-owned-catalog",
|
|
"targetScope": "server-derived",
|
|
"digests": "server-derived",
|
|
"providerAuth": "target-local-compatible-refs",
|
|
"materializer": "existing-immutable-two-phase",
|
|
},
|
|
"nodeIntelligenceAttestation": {
|
|
"releaseId": ENGINE_NODE_INTELLIGENCE_RELEASE_ID,
|
|
"previousGatewaySha256":
|
|
"4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872",
|
|
"targetGatewaySha256":
|
|
"8f04edc11251de86b825351c92338be9537207887cf802474b6b6d8c3cce4077",
|
|
"sidecarImageChanged": False,
|
|
"nodeIntelligenceSourceChanged": False,
|
|
},
|
|
"foundationSha256": {
|
|
"nodedc-source/server/l2ExecutionPlan/compiler.js":
|
|
"64f5196a83018505c6dac77a0f8674c27941257a874eed9b024c1c94f169be2b",
|
|
"nodedc-source/server/l2ExecutionPlan/materializer.js":
|
|
"dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c",
|
|
"nodedc-source/server/deployTransitions/geliosItemsEnvelopeV12.json":
|
|
"16e39f54ebae776de9acfdf0078c79291310eeca5a1f720eb8b82496f4d419a3",
|
|
},
|
|
"boundaries": {
|
|
"n8nCoreChanged": False,
|
|
"l1Changed": False,
|
|
"l2GraphChanged": False,
|
|
"engineUiChanged": False,
|
|
"databaseChanged": False,
|
|
"credentialsChanged": False,
|
|
"foundryChanged": False,
|
|
"ontologyChanged": False,
|
|
"mcpNginxChanged": False,
|
|
"embeddedAiWorkspaceChanged": False,
|
|
"nodeIntelligenceAttestationChanged": True,
|
|
"nodeIntelligenceImageChanged": False,
|
|
"nodeIntelligenceSourceChanged": False,
|
|
},
|
|
}
|
|
if descriptor != expected_descriptor:
|
|
die("Engine MCP registered execution profiles descriptor mismatch")
|
|
|
|
node_intelligence_descriptor = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
"Engine MCP registered execution profiles node-intelligence descriptor",
|
|
)
|
|
if (
|
|
node_intelligence_descriptor.get("action") != "activate"
|
|
or node_intelligence_descriptor.get("releaseId")
|
|
!= ENGINE_NODE_INTELLIGENCE_RELEASE_ID
|
|
or node_intelligence_descriptor.get("source", {}).get("gatewaySha256")
|
|
!= ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256[
|
|
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
|
]
|
|
):
|
|
die(
|
|
"Engine MCP registered execution profiles node-intelligence "
|
|
"attestation mismatch"
|
|
)
|
|
|
|
catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"Engine MCP registered execution profiles catalog",
|
|
max_bytes=4 * 1024 * 1024,
|
|
)
|
|
registered = [
|
|
(provider_package, profile)
|
|
for provider_package in catalog.get("packages", [])
|
|
if isinstance(provider_package, dict)
|
|
for profile in provider_package.get("profiles", [])
|
|
if (
|
|
isinstance(profile, dict)
|
|
and isinstance(profile.get("executionPlanTemplate"), dict)
|
|
)
|
|
]
|
|
if len(registered) != 6:
|
|
die("Engine MCP registered execution profiles count mismatch")
|
|
for provider_package, profile in registered:
|
|
template = profile["executionPlanTemplate"]
|
|
if (
|
|
profile.get("cadence")
|
|
not in {"hot", "warm", "cold", "on_demand"}
|
|
or profile.get("dataClass")
|
|
not in {"operational", "restricted", "internal", "unclassified"}
|
|
or template.get("compilerVersion") != "1.4.0"
|
|
or template.get("package", {}).get("id")
|
|
!= provider_package.get("id")
|
|
or template.get("connection", {}).get("collectionProfileId")
|
|
!= profile.get("id")
|
|
or template.get("collectionProfile", {}).get("dataProductId")
|
|
!= profile.get("dataProductId")
|
|
or not str(
|
|
template.get("bindings", {})
|
|
.get("provider", {})
|
|
.get("reference", "")
|
|
).startswith("ndc-credref:catalog-build-")
|
|
):
|
|
die("Engine MCP registered execution profile template mismatch")
|
|
|
|
gateway = (
|
|
payload_dir
|
|
/ "nodedc-source/server/routes/engineAgentGateway.js"
|
|
).read_text(encoding="utf-8")
|
|
resolver = (
|
|
payload_dir
|
|
/ "nodedc-source/server/l2ExecutionPlan/registeredProfiles.js"
|
|
).read_text(encoding="utf-8")
|
|
for marker in (
|
|
"const ENGINE_AGENT_MCP_VERSION = '0.12.0'",
|
|
"engine_list_l2_execution_profiles",
|
|
"engine_plan_registered_l2_execution",
|
|
):
|
|
if marker not in gateway:
|
|
die(
|
|
"Engine MCP registered execution profiles gateway marker "
|
|
f"missing: {marker}"
|
|
)
|
|
if (
|
|
"registered_execution_profile_ref_scope_denied" not in resolver
|
|
or "targetBoundPlan" not in resolver
|
|
or re.search(r"gelios|robot2b", resolver, re.IGNORECASE)
|
|
):
|
|
die("Engine MCP registered execution profiles resolver boundary mismatch")
|
|
|
|
|
|
def validate_engine_mcp_gelios_units_items_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES:
|
|
die(
|
|
"Engine MCP Gelios units items envelope files.txt exact "
|
|
"set/order mismatch"
|
|
)
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256.items()
|
|
):
|
|
path = payload_dir / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP Gelios units items envelope target missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP Gelios units items envelope target sha256 "
|
|
f"mismatch: {rel}"
|
|
)
|
|
|
|
descriptor = read_strict_json(
|
|
payload_dir / ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL,
|
|
"Engine MCP Gelios units items envelope descriptor",
|
|
max_bytes=32 * 1024,
|
|
)
|
|
if descriptor != {
|
|
"schemaVersion": "nodedc.engine.deploy-transition/v1",
|
|
"id": "engine-mcp-gelios-units-items-envelope-v12-patch1",
|
|
"component": "engine",
|
|
"predecessor": "engine-mcp-registered-execution-profiles-v2",
|
|
"scope": "server-owned-registered-profile-envelope-correction",
|
|
"mcpVersion": "0.12.0",
|
|
"providerPackage": {
|
|
"id": "gelios.provider.v12",
|
|
"predecessorVersion": "12.0.0",
|
|
"targetVersion": "12.0.1",
|
|
"capabilityId": "gelios.units.current.read",
|
|
"responseCollectionPath": "items",
|
|
},
|
|
"registeredProfile": {
|
|
"id": "gelios.units.profile.cold.v1",
|
|
"dataProductId": "fleet.units.profile.current.v1",
|
|
"compilerVersion": "1.4.0",
|
|
"materializer": "existing-immutable-two-phase",
|
|
},
|
|
"foundationSha256": dict(
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256
|
|
),
|
|
"boundaries": {
|
|
"n8nCoreChanged": False,
|
|
"l1Changed": False,
|
|
"l2GraphChanged": False,
|
|
"engineRuntimeCodeChanged": False,
|
|
"engineUiChanged": False,
|
|
"databaseChanged": False,
|
|
"credentialsChanged": False,
|
|
"foundryChanged": False,
|
|
"ontologyChanged": False,
|
|
"mcpNginxChanged": False,
|
|
"embeddedAiWorkspaceChanged": False,
|
|
"providerEndpointsChanged": False,
|
|
},
|
|
}:
|
|
die("Engine MCP Gelios units items envelope descriptor mismatch")
|
|
|
|
execution_catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
|
"Engine MCP Gelios units items envelope execution catalog",
|
|
max_bytes=4 * 1024 * 1024,
|
|
)
|
|
execution_packages = [
|
|
item for item in execution_catalog.get("packages", [])
|
|
if item.get("id") == "gelios.provider.v12"
|
|
]
|
|
if len(execution_packages) != 1:
|
|
die("Engine MCP Gelios units items execution package cardinality mismatch")
|
|
provider_package = execution_packages[0]
|
|
profiles = provider_package.get("profiles", [])
|
|
technical_profiles = [
|
|
item for item in profiles
|
|
if item.get("id") == "gelios.units.profile.cold.v1"
|
|
]
|
|
units_profiles = [
|
|
item for item in profiles
|
|
if "gelios.units.current.read" in item.get("capabilityIds", [])
|
|
]
|
|
if (
|
|
provider_package.get("version") != "12.0.1"
|
|
or len(profiles) != 6
|
|
or len(technical_profiles) != 1
|
|
or len(units_profiles) != 4
|
|
):
|
|
die("Engine MCP Gelios units items registered profile mismatch")
|
|
for profile in profiles:
|
|
template = profile.get("executionPlanTemplate", {})
|
|
if template.get("compilerVersion") != "1.4.0":
|
|
die("Engine MCP Gelios units items compiler version drift")
|
|
for profile in units_profiles:
|
|
extract_steps = [
|
|
step for step in (
|
|
profile.get("executionPlanTemplate", {}).get("steps", [])
|
|
)
|
|
if (
|
|
step.get("kind") == "extract_items"
|
|
and step.get("config", {}).get("capabilityId")
|
|
== "gelios.units.current.read"
|
|
)
|
|
]
|
|
if (
|
|
len(extract_steps) != 1
|
|
or extract_steps[0].get("config", {})
|
|
.get("response", {}).get("collectionPaths", [None])[:1]
|
|
!= ["items"]
|
|
):
|
|
die(
|
|
"Engine MCP Gelios units items response envelope mismatch: "
|
|
f"{profile.get('id')}"
|
|
)
|
|
|
|
security_catalog = read_strict_json(
|
|
payload_dir
|
|
/ "nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"Engine MCP Gelios units items security catalog",
|
|
max_bytes=2 * 1024 * 1024,
|
|
)
|
|
security_packages = [
|
|
item for item in security_catalog.get("packages", [])
|
|
if item.get("id") == "gelios.provider.v12"
|
|
]
|
|
if (
|
|
len(security_packages) != 1
|
|
or security_packages[0].get("version") != "12.0.1"
|
|
or [
|
|
capability.get("id")
|
|
for capability in security_packages[0].get("capabilities", [])
|
|
] != ["gelios.units.identity.read"]
|
|
):
|
|
die("Engine MCP Gelios units items security authority mismatch")
|
|
|
|
|
|
def validate_engine_agent_full_grant_migration_slice(payload_dir, entries):
|
|
if tuple(entries) != ENGINE_AGENT_FULL_GRANT_MIGRATION_ARTIFACT_ENTRIES:
|
|
die("Engine agent full grant migration files.txt exact set/order mismatch")
|
|
store_path = payload_dir / ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL
|
|
try:
|
|
store_source = store_path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine agent full grant migration store is unreadable")
|
|
if sha256_file(store_path) != ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256:
|
|
die("Engine agent full grant migration target sha256 mismatch")
|
|
required = (
|
|
"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 any(value not in store_source for value in required):
|
|
die("Engine agent full grant migration contract mismatch")
|
|
if re.search(r"gelios|robot2b", store_source, re.IGNORECASE):
|
|
die("Engine agent full grant migration provider logic is forbidden")
|
|
|
|
|
|
def current_engine_n8n_transition_descriptor():
|
|
path = component_root("engine") / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL
|
|
if not path.exists():
|
|
return None
|
|
if path.is_symlink() or not path.is_file():
|
|
die("installed Engine n8n transition descriptor is unsafe")
|
|
return read_engine_n8n_transition_descriptor(path, "installed Engine n8n transition descriptor")
|
|
|
|
|
|
def current_engine_node_intelligence_descriptor():
|
|
path = component_root("engine") / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL
|
|
if not path.exists():
|
|
return None
|
|
if path.is_symlink() or not path.is_file():
|
|
die("installed Engine node-intelligence descriptor is unsafe")
|
|
return read_engine_node_intelligence_descriptor(
|
|
path,
|
|
"installed Engine node-intelligence descriptor",
|
|
)
|
|
|
|
|
|
def validate_installed_engine_node_intelligence_source(
|
|
descriptor,
|
|
expected_gateway_sha256=None,
|
|
):
|
|
if descriptor is None or descriptor.get("action") != "activate":
|
|
die("active Engine node-intelligence descriptor is required")
|
|
root = component_root("engine")
|
|
source_root = root / ENGINE_NODE_INTELLIGENCE_SOURCE_REL
|
|
expected_source_files = {
|
|
"catalog.js": descriptor["source"]["catalogSha256"],
|
|
"upstreamMcpClient.js": descriptor["source"]["upstreamClientSha256"],
|
|
"upstreamProjection.js": descriptor["source"]["upstreamProjectionSha256"],
|
|
}
|
|
try:
|
|
source_stat = source_root.lstat()
|
|
except FileNotFoundError:
|
|
die("installed Engine node-intelligence source is missing")
|
|
if stat.S_ISLNK(source_stat.st_mode) or not stat.S_ISDIR(source_stat.st_mode):
|
|
die("installed Engine node-intelligence source is unsafe")
|
|
actual_files = {
|
|
child.relative_to(source_root).as_posix()
|
|
for child in source_root.rglob("*")
|
|
if child.is_file()
|
|
}
|
|
if actual_files != set(expected_source_files):
|
|
die("installed Engine node-intelligence source file set mismatch")
|
|
for rel, expected_sha256 in expected_source_files.items():
|
|
path = source_root / rel
|
|
if path.is_symlink() or sha256_file(path) != expected_sha256:
|
|
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
|
|
if gateway.is_symlink() or sha256_file(gateway) != gateway_sha256:
|
|
die("installed Engine node-intelligence gateway drift detected")
|
|
override = root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL
|
|
try:
|
|
override_stat = override.lstat()
|
|
override_text = override.read_text(encoding="utf-8")
|
|
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
die("installed Engine node-intelligence Compose override is missing")
|
|
if stat.S_ISLNK(override_stat.st_mode) or not stat.S_ISREG(override_stat.st_mode):
|
|
die("installed Engine node-intelligence Compose override is unsafe")
|
|
if (
|
|
override_text != expected_engine_node_intelligence_compose_override()
|
|
or sha256_file(override) != descriptor["source"]["composeOverrideSha256"]
|
|
):
|
|
die("installed Engine node-intelligence Compose override drift detected")
|
|
readme = root / ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL / "README.md"
|
|
if readme.is_symlink() or sha256_file(readme) != descriptor["source"]["readmeSha256"]:
|
|
die("installed Engine node-intelligence README drift detected")
|
|
return gateway_sha256
|
|
|
|
|
|
def preflight_engine_node_intelligence_predecessor(payload_dir):
|
|
candidate = read_engine_node_intelligence_descriptor(
|
|
payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
|
|
"candidate Engine node-intelligence descriptor",
|
|
)
|
|
root = component_root("engine")
|
|
compose_path = root / "docker-compose.yml"
|
|
gateway_path = root / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
|
for path, label in (
|
|
(compose_path, "Compose"),
|
|
(gateway_path, "gateway"),
|
|
):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine node-intelligence predecessor {label} is missing")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine node-intelligence predecessor {label} is unsafe")
|
|
if sha256_file(compose_path) != candidate["predecessor"]["composeSha256"]:
|
|
die("Engine node-intelligence predecessor Compose drift detected")
|
|
|
|
current = current_engine_node_intelligence_descriptor()
|
|
current_state = (
|
|
current["releaseId"]
|
|
if current is not None and current["action"] == "activate"
|
|
else "inactive"
|
|
)
|
|
if current_state != candidate["expectedCurrent"]:
|
|
die(
|
|
"Engine node-intelligence predecessor state mismatch: "
|
|
f"expected={candidate['expectedCurrent']} actual={current_state}"
|
|
)
|
|
actual_gateway_sha256 = sha256_file(gateway_path)
|
|
if actual_gateway_sha256 != candidate["predecessor"]["gatewaySha256"]:
|
|
die(
|
|
"Engine node-intelligence predecessor gateway drift detected: "
|
|
f"expected={candidate['predecessor']['gatewaySha256']} "
|
|
f"actual={actual_gateway_sha256}"
|
|
)
|
|
if candidate["action"] == "activate":
|
|
if current is not None and current["action"] == "activate":
|
|
die("Engine node-intelligence activation requires an inactive predecessor")
|
|
override = root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL
|
|
if override.exists() or override.is_symlink():
|
|
die("inactive Engine node-intelligence predecessor still has a Compose override")
|
|
else:
|
|
validate_installed_engine_node_intelligence_source(current)
|
|
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine node-intelligence requires the active immutable backend")
|
|
return {
|
|
"action": candidate["action"],
|
|
"current_state": current_state,
|
|
"gateway_sha256": actual_gateway_sha256,
|
|
"backend_mode": backend["mode"],
|
|
"descriptor": candidate,
|
|
}
|
|
|
|
|
|
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):
|
|
release_rel = f"releases/n8n-nodes-ndc/{descriptor['releaseId']}"
|
|
release_dir = N8N_PRIVATE_EXTENSION_RELEASES_ROOT / release_rel
|
|
current = N8N_PRIVATE_EXTENSION_RELEASES_ROOT
|
|
for part in PurePosixPath(release_rel).parts:
|
|
current = current / part
|
|
try:
|
|
current_stat = current.lstat()
|
|
except FileNotFoundError:
|
|
die(f"staged private extension release is missing: {release_rel}")
|
|
if stat.S_ISLNK(current_stat.st_mode):
|
|
die(f"staged private extension release symlink rejected: {current}")
|
|
validate_n8n_private_extension_release(N8N_PRIVATE_EXTENSION_RELEASES_ROOT, [release_rel])
|
|
release = read_strict_json(release_dir / "release.json", "staged private extension release manifest")
|
|
if release.get("releaseId") != descriptor["releaseId"]:
|
|
die("staged private extension release id mismatch")
|
|
if release.get("package", {}).get("version") != descriptor["packageVersion"]:
|
|
die("staged private extension package version mismatch")
|
|
if release.get("package", {}).get("sha256") != descriptor["packageSha256"]:
|
|
die("staged private extension package sha256 mismatch")
|
|
for path in [release_dir, *release_dir.rglob("*")]:
|
|
path_stat = path.lstat()
|
|
if path_stat.st_uid != 0 or path_stat.st_gid != 0:
|
|
die(f"staged private extension release ownership mismatch: {path}")
|
|
expected_mode = 0o555 if stat.S_ISDIR(path_stat.st_mode) else 0o444
|
|
if stat.S_IMODE(path_stat.st_mode) != expected_mode:
|
|
die(f"staged private extension release mode mismatch: {path}")
|
|
return release_dir
|
|
|
|
|
|
def load_artifact(artifact, work_dir):
|
|
safe_extract(artifact, work_dir)
|
|
|
|
manifest_path = work_dir / "manifest.env"
|
|
files_path = work_dir / "files.txt"
|
|
payload_dir = work_dir / "payload"
|
|
|
|
if not manifest_path.is_file():
|
|
die("manifest.env missing after extract")
|
|
if not files_path.is_file():
|
|
die("files.txt missing after extract")
|
|
if not payload_dir.is_dir():
|
|
die("payload directory missing after extract")
|
|
|
|
manifest = parse_manifest(manifest_path)
|
|
entries = parse_files_list(files_path)
|
|
|
|
if is_device_plane_backhaul_vps_enrollment_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
die("vps_initiated_transport_frozen:ADR-0001")
|
|
|
|
for rel in entries:
|
|
allowed_payload_path(manifest["component"], rel)
|
|
if not (payload_dir / rel).exists():
|
|
die(f"files.txt entry missing in payload: {rel}")
|
|
|
|
validate_payload_tree(manifest["component"], payload_dir, entries)
|
|
if manifest["component"] == "mission-core-map-access":
|
|
read_map_access_descriptor(payload_dir, entries)
|
|
if is_gitea_fresh_install_slice(manifest["component"], entries):
|
|
validate_gitea_fresh_install_payload(payload_dir, entries)
|
|
elif is_gitea_incident_salvage_slice(manifest["component"], entries):
|
|
validate_gitea_incident_salvage_payload(payload_dir, entries)
|
|
if is_platform_device_core_hub_trust_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_platform_device_core_hub_trust_payload(payload_dir)
|
|
if is_platform_device_manager_public_route_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_platform_device_manager_public_route_payload(payload_dir)
|
|
if is_device_plane_manager_control_plane_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_manager_release_payload(
|
|
payload_dir,
|
|
expected_release_id=manifest["id"],
|
|
)
|
|
if is_device_plane_control_core_release_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_control_core_release_payload(
|
|
payload_dir,
|
|
expected_release_id=manifest["id"],
|
|
)
|
|
if is_device_plane_edge_core_channel_bootstrap_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
if is_device_plane_edge_core_channel_upgrade_v4_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
|
payload_dir,
|
|
expected_transition_id=manifest["id"],
|
|
)
|
|
elif is_device_plane_edge_core_channel_upgrade_v2_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_edge_core_channel_upgrade_v2_payload(
|
|
payload_dir,
|
|
expected_transition_id=manifest["id"],
|
|
)
|
|
elif is_device_plane_edge_core_channel_upgrade_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_edge_core_channel_upgrade_payload(
|
|
payload_dir,
|
|
expected_transition_id=manifest["id"],
|
|
)
|
|
else:
|
|
validate_device_plane_edge_core_channel_bootstrap_payload(
|
|
payload_dir,
|
|
expected_transition_id=manifest["id"],
|
|
)
|
|
if is_device_plane_manager_failed_control_plane_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_manager_failed_control_plane_payload(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_manager_reconciliation_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_manager_reconciliation_payload(payload_dir)
|
|
if is_device_plane_manager_v2_reconciliation_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_manager_v2_reconciliation_payload(payload_dir)
|
|
if is_device_plane_control_core_v3_reconciliation_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_control_core_v3_reconciliation_payload(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_control_core_incident_audit_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_control_core_incident_audit_payload(payload_dir)
|
|
if is_device_plane_control_core_migration_replay_audit_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
if (
|
|
manifest["id"]
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_PATCH_ID
|
|
):
|
|
die("Device Control Core migration replay audit patch id mismatch")
|
|
validate_device_plane_control_core_migration_replay_audit_payload(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_control_core_migration_replay_recovery_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
if (
|
|
manifest["id"]
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
):
|
|
die("Device Control Core migration recovery patch id mismatch")
|
|
validate_device_plane_control_core_migration_replay_recovery_payload(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
if (
|
|
manifest["id"]
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_PATCH_ID
|
|
):
|
|
die(
|
|
"Device Control Core migration replay checkpoint recovery "
|
|
"patch id mismatch"
|
|
)
|
|
validate_device_plane_control_core_migration_replay_checkpoint_recovery_payload(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_postgres_bootstrap_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_postgres_bootstrap_payload(payload_dir)
|
|
if is_device_plane_foundation_recovery_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_foundation_recovery_payload(payload_dir)
|
|
if is_device_plane_foundation_network_publication_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_foundation_network_publication_payload(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_b2_discovery_ingress_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_b2_discovery_ingress_payload(payload_dir)
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_b2_discovery_rollback_recovery_payload(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_backhaul_target_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_backhaul_target_payload(payload_dir)
|
|
if is_device_plane_backhaul_vps_enrollment_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_device_plane_backhaul_vps_enrollment_payload(payload_dir)
|
|
if manifest["component"] == "n8n-private-extension":
|
|
validate_n8n_private_extension_release(payload_dir, entries)
|
|
if manifest["component"] == "engine":
|
|
touches_private_extension = any(
|
|
rel == "nodedc-source/services/n8n/private-extensions"
|
|
or rel.startswith("nodedc-source/services/n8n/private-extensions/")
|
|
for rel in entries
|
|
)
|
|
if touches_private_extension and not is_engine_n8n_transition(manifest["component"], entries):
|
|
die("Engine private-extension payload requires the canonical transition descriptor")
|
|
if is_engine_n8n_transition(manifest["component"], entries):
|
|
validate_engine_n8n_transition(payload_dir, entries)
|
|
touches_node_intelligence = any(
|
|
rel == ENGINE_NODE_INTELLIGENCE_SOURCE_REL
|
|
or rel.startswith(ENGINE_NODE_INTELLIGENCE_SOURCE_REL + "/")
|
|
or rel == ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL
|
|
or rel.startswith(ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL + "/")
|
|
for rel in entries
|
|
)
|
|
if (
|
|
touches_node_intelligence
|
|
and not is_engine_node_intelligence_transition(manifest["component"], entries)
|
|
and not is_engine_mcp_control_plane_slice(manifest["component"], entries)
|
|
and not is_engine_mcp_ontology_sdk_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_mcp_telemetry_catalog_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_execution_plan_materialization_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_execution_plan_module_ownership_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_normalized_identity_search_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_registered_execution_profiles_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_provider_authority_diagnostics_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
):
|
|
die("Engine node-intelligence payload requires the canonical transition")
|
|
if is_engine_node_intelligence_transition(manifest["component"], entries):
|
|
validate_engine_node_intelligence_transition(payload_dir, entries)
|
|
if is_engine_mcp_control_plane_slice(manifest["component"], entries):
|
|
validate_engine_mcp_control_plane_payload(payload_dir, entries)
|
|
if is_engine_mcp_ontology_sdk_slice(manifest["component"], entries):
|
|
validate_engine_mcp_ontology_sdk_payload(payload_dir, entries)
|
|
if is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], 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):
|
|
validate_engine_composite_provider_v4_slice(payload_dir, entries)
|
|
if is_engine_provider_rotating_slot_slice(manifest["component"], entries):
|
|
validate_engine_provider_rotating_slot_slice(payload_dir, entries)
|
|
if is_engine_provider_authority_diagnostics_slice(manifest["component"], entries):
|
|
validate_engine_provider_authority_diagnostics_slice(payload_dir, entries)
|
|
if is_engine_depttrans_zone_authority_v1_slice(manifest["component"], entries):
|
|
validate_engine_depttrans_zone_authority_v1_slice(payload_dir, entries)
|
|
if is_engine_provider_target_host_policy_slice(manifest["component"], entries):
|
|
validate_engine_provider_target_host_policy_slice(payload_dir, entries)
|
|
if is_engine_mcp_execution_profile_decoder_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_execution_profile_decoder_slice(payload_dir, entries)
|
|
if is_engine_mcp_telemetry_catalog_slice(manifest["component"], entries):
|
|
validate_engine_mcp_telemetry_catalog_slice(payload_dir, entries)
|
|
if is_engine_mcp_execution_plan_materialization_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_execution_plan_materialization_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_execution_plan_module_ownership_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_execution_plan_module_ownership_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_normalized_identity_search_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_normalized_identity_search_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_l1_credential_reuse_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_l1_credential_reuse_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_l1_credential_provenance_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_l1_credential_provenance_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_gelios_items_envelope_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_gelios_items_envelope_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_registered_execution_profiles_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_registered_execution_profiles_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_mcp_gelios_units_items_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_engine_mcp_gelios_units_items_slice(
|
|
payload_dir,
|
|
entries,
|
|
)
|
|
if is_engine_provider_security_catalog_slice(manifest["component"], entries):
|
|
validate_engine_provider_security_catalog_payload(payload_dir, entries)
|
|
if touches_engine_credential_sink(manifest["component"], entries):
|
|
validate_engine_credential_sink_slice(payload_dir, entries)
|
|
if (
|
|
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_autonomy_provider_v5_slice(manifest["component"], entries)
|
|
and not is_engine_composite_provider_v4_slice(manifest["component"], entries)
|
|
and not is_engine_provider_rotating_slot_slice(manifest["component"], entries)
|
|
and not is_engine_provider_authority_diagnostics_slice(manifest["component"], entries)
|
|
and not is_engine_depttrans_zone_authority_v1_slice(manifest["component"], entries)
|
|
and not is_engine_provider_target_host_policy_slice(manifest["component"], entries)
|
|
and not is_engine_mcp_execution_profile_decoder_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_telemetry_catalog_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_execution_plan_materialization_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_execution_plan_module_ownership_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_normalized_identity_search_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_l1_credential_reuse_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_l1_credential_provenance_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_gelios_items_envelope_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_registered_execution_profiles_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and not is_engine_mcp_gelios_units_items_slice(
|
|
manifest["component"],
|
|
entries,
|
|
)
|
|
and (
|
|
touches_engine_data_product_publish_grant(entries)
|
|
or ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL in entries
|
|
)
|
|
):
|
|
validate_engine_data_product_publish_grant_slice(payload_dir, entries)
|
|
elif (
|
|
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_autonomy_provider_v5_slice(manifest["component"], entries)
|
|
and ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL in entries
|
|
):
|
|
validate_engine_agent_full_grant_migration_slice(payload_dir, entries)
|
|
return manifest, entries, payload_dir
|
|
|
|
|
|
def load_state(path):
|
|
if not path.exists():
|
|
return []
|
|
rows = []
|
|
with path.open("r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
rows.append(json.loads(line))
|
|
return rows
|
|
|
|
|
|
def append_jsonl(path, row):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with path.open("a", encoding="utf-8") as f:
|
|
f.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
|
|
|
|
|
|
def state_has_sha(sha):
|
|
return any(row.get("sha256") == sha for row in load_state(STATE_FILE))
|
|
|
|
|
|
def state_has_patch_id(patch_id):
|
|
return any(row.get("id") == patch_id for row in load_state(STATE_FILE))
|
|
|
|
|
|
def failed_state_has_sha(sha):
|
|
return any(
|
|
row.get("sha256") == sha
|
|
for row in load_state(FAILED_STATE_FILE)
|
|
)
|
|
|
|
|
|
def failed_state_has_patch_id(patch_id):
|
|
return any(
|
|
row.get("id") == patch_id
|
|
for row in load_state(FAILED_STATE_FILE)
|
|
)
|
|
|
|
|
|
def reject_failed_artifact_replay(manifest, sha256):
|
|
patch_id = manifest.get("id")
|
|
if failed_state_has_sha(sha256):
|
|
die(
|
|
"artifact SHA is terminal failed and cannot be replayed; "
|
|
"build a new immutable artifact"
|
|
)
|
|
if patch_id and failed_state_has_patch_id(patch_id):
|
|
die(
|
|
"patch id is terminal failed and cannot be reused; "
|
|
"build a new patch id"
|
|
)
|
|
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
def reject_terminal_device_plane_foundation_artifact(manifest, sha256):
|
|
if (
|
|
manifest.get("id") == DEVICE_PLANE_FOUNDATION_FAILED_PATCH_ID
|
|
or sha256 == DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die(
|
|
"Device Plane foundation 001 is terminal failed; "
|
|
"use the exact registered foundation recovery successor"
|
|
)
|
|
if (
|
|
manifest.get("id")
|
|
== DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_PATCH_ID
|
|
or sha256
|
|
== DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die(
|
|
"Device Plane foundation recovery 002 is terminal failed; "
|
|
"use the exact registered network-publication successor"
|
|
)
|
|
|
|
|
|
def reject_terminal_device_plane_manager_artifact(
|
|
manifest,
|
|
sha256,
|
|
entries=None,
|
|
):
|
|
if (
|
|
manifest.get("id") == DEVICE_PLANE_MANAGER_FAILED_PATCH_ID
|
|
or sha256 == DEVICE_PLANE_MANAGER_FAILED_ARTIFACT_SHA256
|
|
or is_device_plane_manager_failed_control_plane_slice(
|
|
manifest.get("component"),
|
|
entries,
|
|
)
|
|
):
|
|
die(
|
|
"Device Manager control-plane 001 is terminal failed; "
|
|
"use reconciliation followed by the exact v2 activation successor"
|
|
)
|
|
if (
|
|
manifest.get("id") == DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID
|
|
or sha256 == DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256
|
|
or is_device_plane_manager_v2_control_plane_slice(
|
|
manifest.get("component"),
|
|
entries,
|
|
)
|
|
):
|
|
die(
|
|
"Device Manager control-plane 003 is terminal failed; "
|
|
"use the exact v2 reconciliation successor followed by the "
|
|
"declarative Device Manager release successor"
|
|
)
|
|
|
|
|
|
def reject_terminal_device_plane_backhaul_artifact(manifest, sha256):
|
|
if (
|
|
manifest.get("id") == DEVICE_PLANE_BACKHAUL_FAILED_PATCH_ID
|
|
or sha256 == DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die(
|
|
"Device Plane backhaul target 001 is terminal failed; "
|
|
"use the exact registered loopback plus private Tailscale Serve "
|
|
"successor"
|
|
)
|
|
|
|
|
|
class DeployLock:
|
|
def __enter__(self):
|
|
try:
|
|
LOCK_DIR.mkdir()
|
|
except FileExistsError:
|
|
die(f"deploy lock is busy: {LOCK_DIR}")
|
|
(LOCK_DIR / "pid").write_text(str(os.getpid()), encoding="utf-8")
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
shutil.rmtree(LOCK_DIR, ignore_errors=True)
|
|
|
|
|
|
def component_root(component):
|
|
return COMPONENTS[component]["payload_root"]
|
|
|
|
|
|
def is_gitea_fresh_install_slice(component, entries):
|
|
return (
|
|
component == "gitea"
|
|
and entries is not None
|
|
and tuple(entries) == GITEA_FRESH_INSTALL_ENTRIES
|
|
)
|
|
|
|
|
|
def is_gitea_incident_salvage_slice(component, entries):
|
|
return (
|
|
component == "gitea"
|
|
and entries is not None
|
|
and tuple(entries) == GITEA_SALVAGE_ENTRIES
|
|
)
|
|
|
|
|
|
def is_gitea_bootstrap_slice(component, entries):
|
|
return is_gitea_fresh_install_slice(
|
|
component,
|
|
entries,
|
|
) or is_gitea_incident_salvage_slice(component, entries)
|
|
|
|
|
|
def is_platform_device_core_hub_trust_slice(component, entries):
|
|
return (
|
|
component == "platform"
|
|
and entries is not None
|
|
and tuple(entries) == PLATFORM_DEVICE_CORE_HUB_TRUST_ENTRIES
|
|
)
|
|
|
|
|
|
def is_platform_device_manager_public_route_slice(component, entries):
|
|
return (
|
|
component == "platform"
|
|
and entries is not None
|
|
and tuple(entries) == PLATFORM_DEVICE_MANAGER_PUBLIC_ROUTE_ENTRIES
|
|
)
|
|
|
|
|
|
def is_launcher_device_core_session_slice(component, entries):
|
|
return (
|
|
component == "launcher"
|
|
and entries is not None
|
|
and tuple(entries) == LAUNCHER_DEVICE_CORE_SESSION_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_control_plane_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) in (
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_ENTRIES,
|
|
)
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v2_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v3_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v4_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v5_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v6_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v7_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V7_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v8_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V8_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v9_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V9_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v10_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V10_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v11_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V11_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v12_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V12_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_release_v13_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V13_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_persistent_release_slice(component, entries):
|
|
return (
|
|
is_device_plane_manager_release_v4_slice(component, entries)
|
|
or is_device_plane_manager_release_v5_slice(component, entries)
|
|
or is_device_plane_manager_release_v6_slice(component, entries)
|
|
or is_device_plane_manager_release_v7_slice(component, entries)
|
|
or is_device_plane_manager_release_v8_slice(component, entries)
|
|
or is_device_plane_manager_release_v9_slice(component, entries)
|
|
or is_device_plane_manager_release_v10_slice(component, entries)
|
|
or is_device_plane_manager_release_v11_slice(component, entries)
|
|
or is_device_plane_manager_release_v12_slice(component, entries)
|
|
or is_device_plane_manager_release_v13_slice(component, entries)
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_only_release_slice(component, entries):
|
|
return (
|
|
is_device_plane_manager_release_v3_slice(component, entries)
|
|
or is_device_plane_manager_persistent_release_slice(component, entries)
|
|
)
|
|
|
|
|
|
def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) in (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES,
|
|
)
|
|
)
|
|
|
|
|
|
def is_device_plane_edge_core_channel_upgrade_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) in (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES,
|
|
)
|
|
)
|
|
|
|
|
|
def is_device_plane_edge_core_channel_upgrade_v2_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_edge_core_channel_upgrade_v4_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_release_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) in (
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES,
|
|
)
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_release_v2_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_release_v3_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_release_v4_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_v3_reconciliation_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_incident_audit_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_migration_replay_audit_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_migration_replay_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_v2_control_plane_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_failed_control_plane_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_reconciliation_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RECONCILIATION_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_manager_v2_reconciliation_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ENTRIES
|
|
)
|
|
|
|
|
|
def expected_platform_device_core_hub_trust_descriptor():
|
|
return {
|
|
"schemaVersion": "nodedc.platform.device-core-hub-trust.v1",
|
|
"action": "activate",
|
|
"serviceSlug": "device-core",
|
|
"launcherCredential": "runner-managed-file",
|
|
"credentialScope": ["handoff.consume", "session.validate"],
|
|
"publicRoute": "unchanged",
|
|
}
|
|
|
|
|
|
def expected_platform_device_manager_public_route_descriptor():
|
|
return {
|
|
"schemaVersion": "nodedc.platform.device-manager-public-route.v1",
|
|
"action": "activate",
|
|
"hostname": "device.nodedc.ru",
|
|
"upstream": "device-manager:18122",
|
|
"transport": "reverse-proxy",
|
|
"rawTcpIngress": "forbidden",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v1_boundaries():
|
|
return {
|
|
"service": "device-manager",
|
|
"publicIngress": "reverse-proxy-only",
|
|
"deviceCoreManagementApi": "file-token-authenticated",
|
|
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
|
"healthGate": "bounded-container-grace+core-contract",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"rollback": "restore-preapply-snapshot",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v2_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v1_boundaries(),
|
|
"edgeChannel": (
|
|
"core-initiated-pinned-mtls-enabled-zero-or-more-registered-edges"
|
|
),
|
|
"edgeChannelIdentity": (
|
|
"runner-managed-host-local-private-key-public-certificate-export"
|
|
),
|
|
"edgeChannelEgress": (
|
|
"dedicated-core-only-bridge-no-host-ingress-"
|
|
"public-ipv4-tcp-8443-registration-policy"
|
|
),
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v3_boundaries():
|
|
return {
|
|
"controlCorePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"edgeChannelPredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"service": "device-manager",
|
|
"publicIngress": "reverse-proxy-only",
|
|
"deviceCoreManagementApi": "file-token-authenticated",
|
|
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
|
"edgeChannel": "preserve-active-v4-core-initiated-pinned-mtls",
|
|
"edgeChannelIdentity": (
|
|
"reuse-runner-managed-host-local-private-key-"
|
|
"public-certificate-export"
|
|
),
|
|
"edgeChannelEgress": (
|
|
"preserve-dedicated-core-only-bridge-no-host-ingress-"
|
|
"public-ipv4-tcp-443-only"
|
|
),
|
|
"healthGate": "bounded-container-grace+core-contract",
|
|
"commandTransport": "typed-service-ping-v1",
|
|
"commandCatalog": "allowlisted-adapter-typed-commands-only",
|
|
"credentialBoundary": (
|
|
"transient-core-memory-then-single-pinned-mtls-command-envelope-"
|
|
"to-edge-never-persisted-never-logged-never-returned"
|
|
),
|
|
"gelios": "untouched-legacy-only",
|
|
"rollback": "restore-preapply-snapshot",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v4_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v3_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"healthGate": "bounded-container-grace+core-contract+persistent-data",
|
|
"presentationPersistence": "runner-managed-host-data-bind",
|
|
"presentationDataHostPath": str(DEVICE_PLANE_MANAGER_DATA_DIR),
|
|
"presentationDataContainerPath": DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR,
|
|
"presentationDataOwnership": "uid-1000-gid-1000-mode-0750",
|
|
"presentationDataLifecycle": (
|
|
"preserve-across-manager-recreate-and-source-rollback"
|
|
),
|
|
"presentationPath": DEVICE_PLANE_MANAGER_PRESENTATION_PATH,
|
|
"mediaRoot": DEVICE_PLANE_MANAGER_MEDIA_ROOT,
|
|
"defaultAccentHex": "#f5f5f5",
|
|
"rollback": "restore-preapply-snapshot-preserve-manager-data",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v5_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v4_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"overviewLayout": "mission-core-landing-stage-v1",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v6_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v5_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"faviconSet": "nodedc-adaptive-v1",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v7_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v6_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V7_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"controlCorePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"commandFormLayout": "aligned-control-row-v1",
|
|
"secondaryEmptyTypography": "help-text-sm-v1",
|
|
"infrastructureHostProjection": (
|
|
"edge-registration-live-channel-v1"
|
|
),
|
|
"ontologyStatus": "generic-host-domain-candidate-not-canonical",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v8_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v6_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V8_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"controlCorePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"commandFormLayout": "aligned-control-row-v1",
|
|
"secondaryEmptyTypography": "help-text-sm-v1",
|
|
"infrastructureHostProjection": "ontology-backed-host-runtime-v1",
|
|
"ontologyFoundation": (
|
|
"ontology-core-device-foundation-20260822-001"
|
|
),
|
|
"ontologyCatalogHash": "229c61c02a790906",
|
|
"assetBinding": "temporal-device-asset-binding-v1",
|
|
"infrastructureRuntime": (
|
|
"host-endpoint-deployment-service-instance-v1"
|
|
),
|
|
"healthEvidence": "ttl-observation-missing-not-unhealthy-v1",
|
|
"interactiveShell": (
|
|
"disabled-pending-managed-session-boundary"
|
|
),
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v9_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v8_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V9_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"controlCorePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"telemetryWorkspace": "mission-core-compute-module-parity-v1",
|
|
"telemetryNavigation": "full-workspace-back-navigation-v1",
|
|
"telemetryPollInterval": "three-seconds",
|
|
"telemetryFreshness": (
|
|
"fifteen-seconds-missing-stale-not-unhealthy"
|
|
),
|
|
"telemetryOntologyProjection": (
|
|
"observation-observed-property-provenance-freshness-v1"
|
|
),
|
|
"telemetryAgent": "telegraf-host-observer-v1",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v10_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v9_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V10_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"controlCorePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v11_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v10_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V11_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"controlCorePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"designSystem": "nodedc-canonical-components-and-tokens-v1",
|
|
"missionCoreReference": "compute-modules-workspace-71c8b04",
|
|
"infrastructureWorkspaceLayout": (
|
|
"mission-core-system-workspace-v1"
|
|
),
|
|
"hostInventoryComposition": "mission-core-compute-host-list-v1",
|
|
"telemetryWorkspace": (
|
|
"mission-core-compute-module-visual-parity-v2"
|
|
),
|
|
"telemetrySurface": "borderless-soft-surface-v1",
|
|
"telemetryStatus": "mission-core-dot-status-v1",
|
|
"telemetryScroll": "reset-on-workspace-transition-v1",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v12_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v11_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V12_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"controlCorePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"telemetryWorkspace": (
|
|
"mission-core-compute-module-adaptive-window-v3"
|
|
),
|
|
"telemetryGraphScale": (
|
|
"adaptive-observed-window-explicit-domain-v1"
|
|
),
|
|
"telemetryCpuMinimumSpan": "five-percentage-points",
|
|
"telemetryMemoryMinimumSpan": "four-percentage-points",
|
|
"telemetryNetworkMissingSemantics": (
|
|
"missing-counters-never-zero-v1"
|
|
),
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_v13_boundaries():
|
|
return {
|
|
**expected_device_plane_manager_release_v12_boundaries(),
|
|
"predecessor": {
|
|
"kind": "release",
|
|
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V13_PREDECESSOR_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"controlCorePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"infrastructureWorkspaceLayout": "mission-core-system-workspace-v2",
|
|
"hostInventoryComposition": "mission-core-compute-host-accordion-v2",
|
|
"hostInventoryOverviewSurface": (
|
|
"separate-summary-soft-surface-v1"
|
|
),
|
|
"hostInventoryCollectionSurface": (
|
|
"separate-host-collection-soft-surface-v1"
|
|
),
|
|
"hostInventoryRow": "compact-centered-accordion-v1",
|
|
"hostInventoryFreshness": "dot-only-v1",
|
|
"hostInventoryRelations": (
|
|
"host-scoped-endpoint-deployment-service-v1"
|
|
),
|
|
"hostInventoryDefaultExpansion": "collapsed",
|
|
"hostInventoryScaleTarget": "five-hundred-collapsed-rows-v1",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_release_boundaries():
|
|
# Compatibility name for the current release builder/tests. Immutable v1
|
|
# predecessors always use expected_device_plane_manager_release_v1_boundaries.
|
|
return expected_device_plane_manager_release_v2_boundaries()
|
|
|
|
|
|
def expected_device_plane_edge_core_channel_bootstrap_descriptor(
|
|
transition_id,
|
|
):
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-edge-core-channel-bootstrap.v1"
|
|
),
|
|
"transitionId": transition_id,
|
|
"action": "activate",
|
|
"managerPredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"failedPredecessor": {
|
|
"patchId": DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"backupId": DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_BACKUP_ID,
|
|
"invalidCoreCertificateSha256Fingerprint": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_INVALID_CERTIFICATE_FINGERPRINT
|
|
),
|
|
},
|
|
"service": "device-control-core",
|
|
"composeActivation": "dedicated-additive-override",
|
|
"identity": (
|
|
"runner-managed-host-local-private-key-"
|
|
"public-certificate-export"
|
|
),
|
|
"identityRecovery": (
|
|
"exact-invalid-unexported-failed-predecessor-only"
|
|
),
|
|
"tlsPurpose": "clientAuth",
|
|
"direction": "core-initiated",
|
|
"publicIngress": "none-on-synology",
|
|
"edgeRegistrations": "preserved",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"preservedServices": [
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"healthGate": "bounded-container-grace+core-edge-contract",
|
|
"rollback": "restore-source-and-preapply-core-runtime",
|
|
}
|
|
|
|
|
|
def expected_device_plane_edge_core_channel_upgrade_descriptor(
|
|
transition_id,
|
|
):
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-edge-core-channel-upgrade.v1"
|
|
),
|
|
"transitionId": transition_id,
|
|
"action": "upgrade",
|
|
"bootstrapPredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"service": "device-control-core",
|
|
"composeActivation": "preserve-dedicated-additive-override",
|
|
"identity": (
|
|
"reuse-existing-runner-managed-host-local-private-key-"
|
|
"public-certificate-export"
|
|
),
|
|
"identityRecovery": "forbidden-valid-existing-identity-required",
|
|
"tlsPurpose": "clientAuth",
|
|
"direction": "core-initiated",
|
|
"endpointPolicy": "public-ipv4-standard-https-tcp-443-only",
|
|
"publicIngress": "none-on-synology",
|
|
"edgeRegistrations": (
|
|
"preserved-requires-explicit-443-reconciliation"
|
|
),
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"preservedServices": [
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"healthGate": "bounded-container-grace+core-edge-contract",
|
|
"rollback": (
|
|
"restore-bootstrap-018-source-and-preapply-core-runtime"
|
|
),
|
|
}
|
|
|
|
|
|
def expected_device_plane_edge_core_channel_upgrade_v2_descriptor(
|
|
transition_id,
|
|
):
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-edge-core-channel-upgrade.v2"
|
|
),
|
|
"transitionId": transition_id,
|
|
"action": "upgrade",
|
|
"upgradePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"service": "device-control-core",
|
|
"composeActivation": "preserve-dedicated-additive-override",
|
|
"identity": (
|
|
"reuse-existing-runner-managed-host-local-private-key-"
|
|
"public-certificate-export"
|
|
),
|
|
"identityRecovery": "forbidden-valid-existing-identity-required",
|
|
"tlsPurpose": "clientAuth",
|
|
"direction": "core-initiated",
|
|
"endpointPolicy": "public-ipv4-standard-https-tcp-443-only",
|
|
"publicIngress": "none-on-synology",
|
|
"edgeRegistrations": "preserved",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"preservedServices": [
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"healthGate": "bounded-container-grace+core-edge-contract",
|
|
"rollback": (
|
|
"restore-upgrade-019-source-and-preapply-core-runtime"
|
|
),
|
|
}
|
|
|
|
|
|
def expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
|
transition_id,
|
|
):
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-edge-core-channel-upgrade.v4"
|
|
),
|
|
"transitionId": transition_id,
|
|
"action": "upgrade",
|
|
"upgradePredecessor": {
|
|
"patchId": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"failedAttempt": {
|
|
"patchId": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"backupId": (
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_BACKUP_ID
|
|
),
|
|
},
|
|
"service": "device-control-core",
|
|
"composeActivation": (
|
|
"replace-core-network-membership-with-private-plus-egress"
|
|
),
|
|
"identity": (
|
|
"reuse-existing-runner-managed-host-local-private-key-"
|
|
"public-certificate-export"
|
|
),
|
|
"identityRecovery": "forbidden-valid-existing-identity-required",
|
|
"tlsPurpose": "clientAuth",
|
|
"direction": "core-initiated",
|
|
"endpointPolicy": "public-ipv4-standard-https-tcp-443-only",
|
|
"coreNetworks": [
|
|
"device-plane-private",
|
|
"device-plane-egress",
|
|
],
|
|
"removedCoreNetwork": "device-plane-control",
|
|
"composeCompatibility": "synology-compose-v2.20-no-gw-priority",
|
|
"publicIngress": "none-on-synology",
|
|
"edgeRegistrations": "preserved",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"preservedServices": [
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"healthGate": (
|
|
"bounded-container-grace+core-edge-contract+"
|
|
"exact-private-egress-network-boundary"
|
|
),
|
|
"rollback": (
|
|
"restore-upgrade-v2-021-source-and-preapply-core-runtime"
|
|
),
|
|
}
|
|
|
|
|
|
def expected_device_plane_control_core_release_descriptor(
|
|
release_id,
|
|
predecessor,
|
|
*,
|
|
schema_version="v1",
|
|
):
|
|
descriptor = {
|
|
"schemaVersion": (
|
|
f"nodedc.device-plane.device-control-core-release.{schema_version}"
|
|
),
|
|
"releaseId": release_id,
|
|
"action": "upgrade",
|
|
"predecessor": predecessor,
|
|
"service": "device-control-core",
|
|
"composeActivation": "preserve-active-v4-topology",
|
|
"identity": (
|
|
"reuse-existing-runner-managed-host-local-private-key-"
|
|
"public-certificate-export"
|
|
),
|
|
"identityRecovery": "forbidden-valid-existing-identity-required",
|
|
"tlsPurpose": "clientAuth",
|
|
"direction": "core-initiated",
|
|
"endpointPolicy": "public-ipv4-standard-https-tcp-443-only",
|
|
"coreNetworks": [
|
|
"device-plane-private",
|
|
"device-plane-egress",
|
|
],
|
|
"publicIngress": "none-on-synology",
|
|
"edgeRegistrations": "preserved",
|
|
"commandTransport": (
|
|
"typed-service-ping-v1"
|
|
if schema_version in ("v2", "v3", "v4")
|
|
else "disabled"
|
|
),
|
|
"gelios": (
|
|
"untouched-legacy-only"
|
|
if schema_version in ("v2", "v3", "v4")
|
|
else "untouched"
|
|
),
|
|
"preservedServices": [
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"healthGate": (
|
|
"bounded-container-grace+core-edge-contract+"
|
|
"exact-private-egress-network-boundary"
|
|
),
|
|
"rollback": "restore-preapply-source-and-core-runtime",
|
|
}
|
|
if schema_version in ("v2", "v3", "v4"):
|
|
descriptor["commandCatalog"] = (
|
|
"allowlisted-adapter-typed-commands-only"
|
|
)
|
|
descriptor["credentialBoundary"] = (
|
|
"transient-core-memory-then-single-pinned-mtls-command-envelope-"
|
|
"to-edge-never-persisted-never-logged-never-returned"
|
|
)
|
|
if schema_version in ("v3", "v4"):
|
|
descriptor.update({
|
|
"telemetryTransport": (
|
|
"edge-channel-host-telemetry-observed-v1"
|
|
),
|
|
"telemetryContract": (
|
|
"nodedc.infrastructure.host-telemetry.v1"
|
|
),
|
|
"telemetryStorage": (
|
|
"device-control-core-postgres-seven-day-retention"
|
|
),
|
|
"ontologyProjection": (
|
|
"observation-observed-property-provenance-freshness-v1"
|
|
),
|
|
"telemetryFreshness": (
|
|
"fifteen-seconds-missing-stale-not-unhealthy"
|
|
),
|
|
})
|
|
if schema_version == "v4":
|
|
descriptor.update({
|
|
"recoveryPredecessor": (
|
|
"terminal-applied-046-exact-source-runtime-database"
|
|
),
|
|
"databasePreflight": (
|
|
"final-migration-016-validated-and-host-telemetry-table-absent"
|
|
),
|
|
"databaseRowMutation": "none-before-core-startup-migrations",
|
|
"databaseSchemaOutcome": (
|
|
"migration-017-host-telemetry-table-present"
|
|
),
|
|
"runtimePredecessor": "healthy-recovery-046-core-generation",
|
|
})
|
|
return descriptor
|
|
|
|
|
|
def expected_device_plane_manager_failed_control_plane_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-manager-control-plane.v1"
|
|
),
|
|
"action": "activate",
|
|
"service": "device-manager",
|
|
"publicIngress": "reverse-proxy-only",
|
|
"deviceCoreManagementApi": "file-token-authenticated",
|
|
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_reconciliation_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-manager-control-plane-"
|
|
"reconciliation.v1"
|
|
),
|
|
"mode": "failed-control-plane-baseline-adoption",
|
|
"failedPatchId": DEVICE_PLANE_MANAGER_FAILED_PATCH_ID,
|
|
"failedArtifactSha256": (
|
|
DEVICE_PLANE_MANAGER_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"backupId": DEVICE_PLANE_MANAGER_RECONCILIATION_BACKUP_ID,
|
|
"sourceAction": "publish-reconciliation-marker-only",
|
|
"runtimeAction": "read-only-acceptance",
|
|
"preservedServices": [
|
|
"device-control-core",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"absentService": "device-manager",
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"publicIngress": "disabled",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"rollback": "marker-only-runtime-unchanged",
|
|
}
|
|
|
|
|
|
def expected_device_plane_manager_v2_reconciliation_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-manager-control-plane-"
|
|
"v2-reconciliation.v1"
|
|
),
|
|
"mode": "failed-v2-control-plane-baseline-adoption",
|
|
"failedPatchId": DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID,
|
|
"failedArtifactSha256": (
|
|
DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"backupId": DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID,
|
|
"failureClass": "deterministic-runtime-module-resolution",
|
|
"missingModule": (
|
|
"/packages/external-provider-contract/src/"
|
|
"credential-reference.mjs"
|
|
),
|
|
"correctiveAction": (
|
|
"runtime-local-contract-adapter+staged-module-import-gate"
|
|
),
|
|
"sourceAction": "publish-reconciliation-marker-only",
|
|
"runtimeAction": "read-only-acceptance",
|
|
"preservedServices": [
|
|
"device-control-core",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"absentService": "device-manager",
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"publicIngress": "disabled",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"rollback": "marker-only-runtime-unchanged",
|
|
}
|
|
|
|
|
|
def expected_device_plane_control_core_v3_reconciliation_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-control-core-release-v3-"
|
|
"reconciliation.v1"
|
|
),
|
|
"mode": "failed-release-v3-exact-preapply-image-restore",
|
|
"failedPatchId": DEVICE_PLANE_CONTROL_CORE_V3_FAILED_PATCH_ID,
|
|
"failedArtifactSha256": (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"failedArtifact": DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT,
|
|
"backupId": DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_BACKUP_ID,
|
|
"predecessorPatchId": "device-control-core-release-v2-20260822-038",
|
|
"predecessorArtifactSha256": (
|
|
"e2d062b82b022dba662522b5d6e192026ac964d78950d903295ca3cbbc95ab28"
|
|
),
|
|
"preapplyImageId": DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID,
|
|
"sourceAction": "accept-byte-exact-restored-preapply-source",
|
|
"runtimeAction": (
|
|
"retag-exact-preapply-image+recreate-device-control-core-only"
|
|
),
|
|
"preservedServices": [
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"publicIngress": "disabled",
|
|
"edgeChannel": "core-initiated-pinned-mtls-registered-edges-only",
|
|
"commandTransport": "typed-service-ping-v1",
|
|
"gelios": "untouched-legacy-only",
|
|
"rollback": "marker+exact-preapply-image-runtime",
|
|
}
|
|
|
|
|
|
def expected_device_plane_control_core_incident_audit_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.device-control-core-incident-audit.v1"
|
|
),
|
|
"mode": "double-rollback-failed-read-only-audit",
|
|
"allowedOperation": "canonical-plan-only",
|
|
"applyAllowed": False,
|
|
"failedAttempts": [
|
|
{
|
|
"patchId": DEVICE_PLANE_CONTROL_CORE_V3_FAILED_PATCH_ID,
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"backupId": (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_BACKUP_ID
|
|
),
|
|
},
|
|
{
|
|
"patchId": (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"backupId": (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_BACKUP_ID
|
|
),
|
|
},
|
|
],
|
|
"readOnlyEvidence": [
|
|
"device-control-core-runtime-inventory",
|
|
"device-control-core-bounded-container-logs",
|
|
"device-postgres-schema-presence",
|
|
"device-postgres-wait-activity",
|
|
],
|
|
"runtimeMutation": "none",
|
|
"sourceMutation": "none",
|
|
"databaseMutation": "none",
|
|
"networkMutation": "none",
|
|
"secretRead": "none",
|
|
"preservedServices": [
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
}
|
|
|
|
|
|
def expected_device_plane_control_core_migration_replay_audit_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane."
|
|
"device-control-core-migration-replay-audit.v1"
|
|
),
|
|
"mode": "rejected-recovery-044-live-invariants-read-only-audit",
|
|
"allowedOperation": "canonical-plan-only",
|
|
"applyAllowed": False,
|
|
"rejectedRecovery": {
|
|
"patchId": (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256
|
|
),
|
|
},
|
|
"readOnlyEvidence": [
|
|
"invalid-command-kind-count",
|
|
"triggering-receipt-count",
|
|
"constraint-validated",
|
|
"constraint-covers-final-command-kinds",
|
|
"host-telemetry-table-absent",
|
|
],
|
|
"runtimeMutation": "none",
|
|
"sourceMutation": "none",
|
|
"databaseMutation": "none",
|
|
"networkMutation": "none",
|
|
"secretRead": "none",
|
|
"preservedServices": [
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
}
|
|
|
|
|
|
def expected_device_plane_control_core_migration_replay_recovery_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane."
|
|
"device-control-core-migration-replay-recovery.v1"
|
|
),
|
|
"mode": "double-rollback-failed-migration-014-forward-repair",
|
|
"failedIncidentAudit": (
|
|
DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_PATCH_ID
|
|
),
|
|
"sourcePredecessor": {
|
|
"path": DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL,
|
|
"sha256": (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256
|
|
),
|
|
},
|
|
"sourceTarget": {
|
|
"path": DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL,
|
|
"sha256": DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256,
|
|
},
|
|
"rootCause": (
|
|
"intermediate-command-kind-check-revalidated-historical-receipts"
|
|
),
|
|
"repair": "migration-014-add-constraint-not-valid",
|
|
"databasePreflight": (
|
|
"all-live-command-kinds-covered-by-final-migration-016"
|
|
),
|
|
"databaseRowMutation": "none",
|
|
"databaseSchemaOutcome": (
|
|
"final-migration-016-validated-command-kind-check"
|
|
),
|
|
"runtimeAction": "build+recreate-device-control-core-only",
|
|
"runtimePredecessor": "proven-degraded-double-rollback-state",
|
|
"preservedServices": [
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"publicIngress": "disabled",
|
|
"edgeChannel": (
|
|
"core-initiated-pinned-mtls-registered-edges-only"
|
|
),
|
|
"rollback": "source+exact-degraded-predecessor-image-runtime",
|
|
}
|
|
|
|
|
|
def expected_device_plane_control_core_migration_replay_checkpoint_recovery_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane."
|
|
"device-control-core-migration-replay-checkpoint-recovery.v2"
|
|
),
|
|
"mode": "terminal-044-replay-checkpoint-forward-repair",
|
|
"failedIncidentAudit": (
|
|
DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_PATCH_ID
|
|
),
|
|
"failedRecovery": {
|
|
"patchId": (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
),
|
|
"artifactSha256": (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256
|
|
),
|
|
"failure": "preflight-replay-checkpoint-race",
|
|
"startedApply": False,
|
|
},
|
|
"sourcePredecessor": {
|
|
"path": DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL,
|
|
"sha256": (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256
|
|
),
|
|
},
|
|
"sourceTarget": {
|
|
"path": DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL,
|
|
"sha256": DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256,
|
|
},
|
|
"rootCause": (
|
|
"restarting-core-cycles-exact-committed-migration-checkpoints"
|
|
),
|
|
"repair": "migration-014-add-constraint-not-valid",
|
|
"databasePreflight": (
|
|
"exact-replay-checkpoint-005-007-009-011-and-final-compatible-rows"
|
|
),
|
|
"databaseRowMutation": "none",
|
|
"databaseSchemaOutcome": (
|
|
"exact-final-migration-016-validated-command-kind-check"
|
|
),
|
|
"runtimeAction": "build+recreate-device-control-core-only",
|
|
"runtimePredecessor": (
|
|
"proven-degraded-restarting-exact-preapply-image"
|
|
),
|
|
"preservedServices": [
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
],
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"publicIngress": "disabled",
|
|
"edgeChannel": (
|
|
"core-initiated-pinned-mtls-registered-edges-only"
|
|
),
|
|
"rollback": "source+exact-degraded-predecessor-image-runtime",
|
|
}
|
|
|
|
|
|
def validate_platform_device_core_hub_trust_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / PLATFORM_DEVICE_CORE_HUB_TRUST_REL,
|
|
"Platform Device Core Hub trust descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_platform_device_core_hub_trust_descriptor():
|
|
die("Platform Device Core Hub trust descriptor mismatch")
|
|
compose = (
|
|
payload_dir / "platform/docker-compose.platform-http.yml"
|
|
).read_text(encoding="utf-8")
|
|
for required in (
|
|
"NODEDC_DEVICE_CORE_INTERNAL_TOKEN_FILE: "
|
|
"/run/nodedc-secrets/device-core-internal-token",
|
|
"source: /volume1/docker/nodedc-platform/secrets/"
|
|
"device-core-internal-token",
|
|
"create_host_path: false",
|
|
):
|
|
if required not in compose:
|
|
die(f"Platform Device Core Hub trust boundary missing: {required}")
|
|
return descriptor
|
|
|
|
|
|
def validate_platform_device_manager_public_route_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / PLATFORM_DEVICE_MANAGER_PUBLIC_ROUTE_REL,
|
|
"Platform Device Manager public route descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_platform_device_manager_public_route_descriptor():
|
|
die("Platform Device Manager public route descriptor mismatch")
|
|
caddy = (payload_dir / "platform/Caddyfile.http").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
for required in (
|
|
"http://device.nodedc.ru",
|
|
"reverse_proxy device-manager:18122",
|
|
"header_up X-Forwarded-Proto https",
|
|
):
|
|
if required not in caddy:
|
|
die(f"Platform Device Manager route boundary missing: {required}")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
*,
|
|
schema_version,
|
|
boundaries,
|
|
expected_release_id=None,
|
|
):
|
|
required_keys = {
|
|
"schemaVersion",
|
|
"releaseId",
|
|
"action",
|
|
"predecessor",
|
|
*boundaries,
|
|
}
|
|
if set(descriptor) != required_keys:
|
|
missing = ",".join(sorted(required_keys - set(descriptor))) or "none"
|
|
extra = ",".join(sorted(set(descriptor) - required_keys)) or "none"
|
|
die(
|
|
"Device Manager release descriptor key set mismatch: "
|
|
f"schema={schema_version} missing={missing} extra={extra}"
|
|
)
|
|
if descriptor.get("schemaVersion") != schema_version:
|
|
die("Device Manager release descriptor schema mismatch")
|
|
release_id = descriptor.get("releaseId")
|
|
if (
|
|
not isinstance(release_id, str)
|
|
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", release_id)
|
|
or (expected_release_id is not None and release_id != expected_release_id)
|
|
):
|
|
die("Device Manager release id mismatch")
|
|
action = descriptor.get("action")
|
|
predecessor = descriptor.get("predecessor")
|
|
if (
|
|
action not in ("activate", "upgrade")
|
|
or not isinstance(predecessor, dict)
|
|
or set(predecessor) != {"kind", "patchId", "artifactSha256"}
|
|
or predecessor.get("kind") not in ("reconciliation", "release")
|
|
or not isinstance(predecessor.get("patchId"), str)
|
|
or not re.fullmatch(
|
|
r"[A-Za-z0-9._-]{1,96}",
|
|
predecessor["patchId"],
|
|
)
|
|
or predecessor["patchId"] == release_id
|
|
or not isinstance(predecessor.get("artifactSha256"), str)
|
|
or not re.fullmatch(
|
|
r"[a-f0-9]{64}",
|
|
predecessor["artifactSha256"],
|
|
)
|
|
or (action == "activate")
|
|
!= (predecessor["kind"] == "reconciliation")
|
|
):
|
|
die("Device Manager release predecessor mismatch")
|
|
if any(descriptor.get(key) != value for key, value in boundaries.items()):
|
|
die("Device Manager release security boundary mismatch")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
*,
|
|
descriptor_rel,
|
|
schema_version,
|
|
boundaries,
|
|
compose_sha256,
|
|
edge_channel,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir / descriptor_rel,
|
|
"Device Manager release descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version=schema_version,
|
|
boundaries=boundaries,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
compose_path = payload_dir / DEVICE_PLANE_MANAGER_COMPOSE_REL
|
|
if sha256_file(compose_path) != compose_sha256:
|
|
die("Device Manager control-plane Compose mismatch")
|
|
compose = compose_path.read_text(
|
|
encoding="utf-8"
|
|
)
|
|
required_compose = [
|
|
"device-manager:",
|
|
'DEVICE_MANAGEMENT_API_ENABLED: "true"',
|
|
"DEVICE_MANAGEMENT_CORE_TOKEN_FILE: "
|
|
"/run/nodedc-secrets/management-core-token",
|
|
"NODEDC_LAUNCHER_INTERNAL_TOKEN_FILE: "
|
|
"/run/nodedc-secrets/device-core-internal-token",
|
|
"NODEDC_DEVICE_CORE_TOKEN_FILE: "
|
|
"/run/nodedc-secrets/management-core-token",
|
|
"name: nodedc-platform_edge",
|
|
]
|
|
if edge_channel:
|
|
required_compose.extend((
|
|
'DEVICE_EDGE_CHANNEL_ENABLED: "true"',
|
|
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: "
|
|
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem",
|
|
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: "
|
|
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem",
|
|
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: "
|
|
"/run/nodedc-secrets/device-edge-channel/peers",
|
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
|
"device-edge-channel/core-private-key.pem",
|
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
|
"device-edge-channel/core-certificate.pem",
|
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
|
"device-edge-channel/peers",
|
|
"name: nodedc-device-plane-egress",
|
|
))
|
|
if schema_version in (
|
|
"nodedc.device-plane.device-manager-release.v4",
|
|
"nodedc.device-plane.device-manager-release.v5",
|
|
"nodedc.device-plane.device-manager-release.v6",
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
):
|
|
required_compose.extend((
|
|
"NODEDC_DEVICE_MANAGER_PRESENTATION_PATH: "
|
|
f"{DEVICE_PLANE_MANAGER_PRESENTATION_PATH}",
|
|
"NODEDC_DEVICE_MANAGER_MEDIA_ROOT: "
|
|
f"{DEVICE_PLANE_MANAGER_MEDIA_ROOT}",
|
|
f"source: {DEVICE_PLANE_MANAGER_DATA_DIR}",
|
|
f"target: {DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}",
|
|
"read_only: false",
|
|
"create_host_path: false",
|
|
))
|
|
for required in required_compose:
|
|
if required not in compose:
|
|
die(f"Device Manager control-plane boundary missing: {required}")
|
|
for forbidden in (
|
|
"NODEDC_INTERNAL_ACCESS_TOKEN:",
|
|
"NODEDC_PLATFORM_SERVICE_TOKEN:",
|
|
"0.0.0.0:18122",
|
|
):
|
|
if forbidden in compose:
|
|
die(f"Device Manager control-plane boundary violation: {forbidden}")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_control_plane_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
return validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v1",
|
|
boundaries=expected_device_plane_manager_release_v1_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
|
|
|
|
def validate_device_plane_manager_release_v2_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
return validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v2",
|
|
boundaries=expected_device_plane_manager_release_v2_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256,
|
|
edge_channel=True,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
|
|
|
|
def validate_device_plane_manager_release_v3_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
return validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v3",
|
|
boundaries=expected_device_plane_manager_release_v3_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
|
|
|
|
def validate_device_plane_manager_release_v4_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
return validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v4",
|
|
boundaries=expected_device_plane_manager_release_v4_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
|
|
|
|
def validate_device_plane_manager_release_v5_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
return validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v5",
|
|
boundaries=expected_device_plane_manager_release_v5_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
|
|
|
|
def validate_device_plane_manager_v6_favicon_bundle(payload_dir):
|
|
dist = payload_dir / "services/device-manager/dist"
|
|
for relative_path, expected_sha256 in (
|
|
DEVICE_PLANE_MANAGER_FAVICON_SHA256.items()
|
|
):
|
|
path = dist / relative_path
|
|
if (
|
|
not path.is_file()
|
|
or path.is_symlink()
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Device Manager canonical favicon mismatch: "
|
|
f"{relative_path}"
|
|
)
|
|
index_path = dist / "index.html"
|
|
if not index_path.is_file() or index_path.is_symlink():
|
|
die("Device Manager favicon HTML entrypoint mismatch")
|
|
index_html = index_path.read_text(encoding="utf-8")
|
|
for required_link in (
|
|
'href="/favicon/icon-adaptive.svg"',
|
|
'href="/favicon/favicon.ico"',
|
|
'href="/favicon/apple-touch-icon.png"',
|
|
'href="/favicon/icon-192.png"',
|
|
'href="/favicon/icon-512.png"',
|
|
'href="/favicon/manifest.webmanifest.json"',
|
|
):
|
|
if required_link not in index_html:
|
|
die(f"Device Manager favicon link missing: {required_link}")
|
|
return "nodedc-adaptive-v1"
|
|
|
|
|
|
def validate_device_plane_manager_release_v6_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v6",
|
|
boundaries=expected_device_plane_manager_release_v6_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_v7_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V7_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v7",
|
|
boundaries=expected_device_plane_manager_release_v7_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V7_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_v8_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V8_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v8",
|
|
boundaries=expected_device_plane_manager_release_v8_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V8_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_v9_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V9_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v9",
|
|
boundaries=expected_device_plane_manager_release_v9_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V9_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_v10_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V10_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v10",
|
|
boundaries=expected_device_plane_manager_release_v10_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V10_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_v11_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V11_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v11",
|
|
boundaries=expected_device_plane_manager_release_v11_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V11_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_v12_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V12_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v12",
|
|
boundaries=expected_device_plane_manager_release_v12_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V12_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_v13_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
descriptor = validate_device_plane_manager_release_payload_contract(
|
|
payload_dir,
|
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V13_REL,
|
|
schema_version="nodedc.device-plane.device-manager-release.v13",
|
|
boundaries=expected_device_plane_manager_release_v13_boundaries(),
|
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V13_COMPOSE_SHA256,
|
|
edge_channel=False,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_release_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
v1 = payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
|
|
v2 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
|
|
v3 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V3_REL
|
|
v4 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V4_REL
|
|
v5 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V5_REL
|
|
v6 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V6_REL
|
|
v7 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V7_REL
|
|
v8 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V8_REL
|
|
v9 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V9_REL
|
|
v10 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V10_REL
|
|
v11 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V11_REL
|
|
v12 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V12_REL
|
|
v13 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V13_REL
|
|
present = [
|
|
path for path in (
|
|
v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13
|
|
)
|
|
if path.exists() or path.is_symlink()
|
|
]
|
|
if len(present) != 1:
|
|
die("Device Manager release descriptor cardinality mismatch")
|
|
if present[0] == v13:
|
|
return validate_device_plane_manager_release_v13_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v12:
|
|
return validate_device_plane_manager_release_v12_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v11:
|
|
return validate_device_plane_manager_release_v11_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v10:
|
|
return validate_device_plane_manager_release_v10_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v9:
|
|
return validate_device_plane_manager_release_v9_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v8:
|
|
return validate_device_plane_manager_release_v8_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v7:
|
|
return validate_device_plane_manager_release_v7_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v6:
|
|
return validate_device_plane_manager_release_v6_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v5:
|
|
return validate_device_plane_manager_release_v5_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v4:
|
|
return validate_device_plane_manager_release_v4_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v3:
|
|
return validate_device_plane_manager_release_v3_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
if present[0] == v2:
|
|
return validate_device_plane_manager_release_v2_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
return validate_device_plane_manager_control_plane_payload(
|
|
payload_dir,
|
|
expected_release_id=expected_release_id,
|
|
)
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_bootstrap_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_transition_id=None,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
|
|
"Device Edge Core channel bootstrap descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
transition_id = descriptor.get("transitionId")
|
|
if (
|
|
not isinstance(transition_id, str)
|
|
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", transition_id)
|
|
or (
|
|
expected_transition_id is not None
|
|
and transition_id != expected_transition_id
|
|
)
|
|
or descriptor
|
|
!= expected_device_plane_edge_core_channel_bootstrap_descriptor(
|
|
transition_id
|
|
)
|
|
):
|
|
die("Device Edge Core channel bootstrap descriptor mismatch")
|
|
validate_device_plane_edge_core_channel_compose(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_upgrade_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_transition_id=None,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
|
|
"Device Edge Core channel upgrade descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
transition_id = descriptor.get("transitionId")
|
|
if (
|
|
not isinstance(transition_id, str)
|
|
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", transition_id)
|
|
or (
|
|
expected_transition_id is not None
|
|
and transition_id != expected_transition_id
|
|
)
|
|
or descriptor
|
|
!= expected_device_plane_edge_core_channel_upgrade_descriptor(
|
|
transition_id
|
|
)
|
|
):
|
|
die("Device Edge Core channel upgrade descriptor mismatch")
|
|
validate_device_plane_edge_core_channel_compose(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_upgrade_v2_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_transition_id=None,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
|
|
"Device Edge Core channel upgrade v2 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
transition_id = descriptor.get("transitionId")
|
|
if (
|
|
not isinstance(transition_id, str)
|
|
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", transition_id)
|
|
or (
|
|
expected_transition_id is not None
|
|
and transition_id != expected_transition_id
|
|
)
|
|
or descriptor
|
|
!= expected_device_plane_edge_core_channel_upgrade_v2_descriptor(
|
|
transition_id
|
|
)
|
|
):
|
|
die("Device Edge Core channel upgrade v2 descriptor mismatch")
|
|
validate_device_plane_edge_core_channel_compose(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_transition_id=None,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
|
"Device Edge Core channel upgrade v4 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
transition_id = descriptor.get("transitionId")
|
|
if (
|
|
not isinstance(transition_id, str)
|
|
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", transition_id)
|
|
or (
|
|
expected_transition_id is not None
|
|
and transition_id != expected_transition_id
|
|
)
|
|
or descriptor
|
|
!= expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
|
transition_id
|
|
)
|
|
):
|
|
die("Device Edge Core channel upgrade v4 descriptor mismatch")
|
|
validate_device_plane_edge_core_channel_upgrade_v4_base_compose(
|
|
payload_dir
|
|
)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_control_core_release_payload(
|
|
payload_dir,
|
|
*,
|
|
expected_release_id=None,
|
|
):
|
|
v1 = payload_dir / DEVICE_PLANE_CONTROL_CORE_RELEASE_REL
|
|
v2 = payload_dir / DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL
|
|
v3 = payload_dir / DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL
|
|
v4 = payload_dir / DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL
|
|
present = [
|
|
path for path in (v1, v2, v3, v4)
|
|
if path.exists() or path.is_symlink()
|
|
]
|
|
if len(present) != 1:
|
|
die("Device Control Core release descriptor cardinality mismatch")
|
|
schema_version = (
|
|
"v4" if present[0] == v4
|
|
else "v3" if present[0] == v3
|
|
else "v2" if present[0] == v2
|
|
else "v1"
|
|
)
|
|
descriptor = read_strict_json(
|
|
present[0],
|
|
"Device Control Core release descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
release_id = descriptor.get("releaseId")
|
|
predecessor = descriptor.get("predecessor")
|
|
valid_predecessor = (
|
|
isinstance(predecessor, dict)
|
|
and set(predecessor) == {"kind", "patchId", "artifactSha256"}
|
|
and predecessor.get("kind") in (
|
|
"edge-core-channel-upgrade-v4",
|
|
"release",
|
|
"migration-replay-checkpoint-recovery",
|
|
)
|
|
and isinstance(predecessor.get("patchId"), str)
|
|
and re.fullmatch(
|
|
r"[A-Za-z0-9._-]{1,96}",
|
|
predecessor["patchId"],
|
|
)
|
|
and isinstance(predecessor.get("artifactSha256"), str)
|
|
and re.fullmatch(r"[0-9a-f]{64}", predecessor["artifactSha256"])
|
|
)
|
|
if valid_predecessor and predecessor["kind"] == (
|
|
"edge-core-channel-upgrade-v4"
|
|
):
|
|
valid_predecessor = (
|
|
predecessor["patchId"]
|
|
== DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_PATCH_ID
|
|
and predecessor["artifactSha256"]
|
|
== DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_ARTIFACT_SHA256
|
|
)
|
|
elif valid_predecessor:
|
|
if predecessor["kind"] == "migration-replay-checkpoint-recovery":
|
|
valid_predecessor = (
|
|
schema_version == "v4"
|
|
and predecessor["patchId"]
|
|
== DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_PATCH_ID
|
|
and predecessor["artifactSha256"]
|
|
== DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ARTIFACT_SHA256
|
|
)
|
|
else:
|
|
valid_predecessor = (
|
|
predecessor["patchId"].startswith(
|
|
"device-control-core-release-"
|
|
)
|
|
and predecessor["patchId"] != release_id
|
|
)
|
|
if (
|
|
not isinstance(release_id, str)
|
|
or not re.fullmatch(
|
|
r"device-control-core-release(?:-v[234])?-[A-Za-z0-9._-]{1,67}",
|
|
release_id,
|
|
)
|
|
or (
|
|
expected_release_id is not None
|
|
and release_id != expected_release_id
|
|
)
|
|
or not valid_predecessor
|
|
or (
|
|
schema_version == "v4"
|
|
and release_id != DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_PATCH_ID
|
|
)
|
|
or descriptor
|
|
!= expected_device_plane_control_core_release_descriptor(
|
|
release_id,
|
|
predecessor,
|
|
schema_version=schema_version,
|
|
)
|
|
):
|
|
die("Device Control Core release descriptor mismatch")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_compose(payload_dir):
|
|
compose = payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL
|
|
if sha256_file(compose) != DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_SHA256:
|
|
die("Device Edge Core channel Compose mismatch")
|
|
compose_text = compose.read_text(encoding="utf-8")
|
|
required = (
|
|
"device-control-core:",
|
|
'DEVICE_EDGE_CHANNEL_ENABLED: "true"',
|
|
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: "
|
|
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem",
|
|
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: "
|
|
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem",
|
|
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: "
|
|
"/run/nodedc-secrets/device-edge-channel/peers",
|
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
|
"device-edge-channel/core-private-key.pem",
|
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
|
"device-edge-channel/core-certificate.pem",
|
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
|
"device-edge-channel/peers",
|
|
"name: nodedc-device-plane-egress",
|
|
)
|
|
if any(value not in compose_text for value in required):
|
|
die("Device Edge Core channel Compose boundary missing")
|
|
forbidden = (
|
|
"device-manager:",
|
|
"device-gateway:",
|
|
"device-postgres:",
|
|
"ports:",
|
|
"PRIVATE KEY",
|
|
"0.0.0.0:8443",
|
|
"0.0.0.0:443",
|
|
"9921:9921",
|
|
)
|
|
if any(value in compose_text for value in forbidden):
|
|
die("Device Edge Core channel Compose boundary violation")
|
|
return compose
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_upgrade_v4_base_compose(
|
|
payload_dir,
|
|
):
|
|
compose = payload_dir / "docker-compose.device-plane.yml"
|
|
if (
|
|
sha256_file(compose)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_BASE_COMPOSE_SHA256
|
|
):
|
|
die("Device Edge Core channel v4 base Compose mismatch")
|
|
compose_text = compose.read_text(encoding="utf-8")
|
|
try:
|
|
core_block = compose_text.split(
|
|
"\n device-control-core:", 1
|
|
)[1].split("\n device-gateway:", 1)[0]
|
|
gateway_block = compose_text.split(
|
|
"\n device-gateway:", 1
|
|
)[1].split("\nnetworks:", 1)[0]
|
|
except IndexError:
|
|
die("Device Edge Core channel v4 service topology is incomplete")
|
|
if (
|
|
" - device-plane-private" not in core_block
|
|
or " - device-plane-control" in core_block
|
|
or " - device-plane-private" not in gateway_block
|
|
or " - device-plane-control" not in gateway_block
|
|
or "gw_priority:" in compose_text
|
|
or "network_mode:" in compose_text
|
|
or "privileged:" in compose_text
|
|
):
|
|
die("Device Edge Core channel v4 network boundary mismatch")
|
|
return compose
|
|
|
|
|
|
def installed_device_plane_manager_compose_sha256():
|
|
root = DEVICE_PLANE_ROOT
|
|
v1 = root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
|
|
v2 = root / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
|
|
v3 = root / DEVICE_PLANE_MANAGER_RELEASE_V3_REL
|
|
v4 = root / DEVICE_PLANE_MANAGER_RELEASE_V4_REL
|
|
v5 = root / DEVICE_PLANE_MANAGER_RELEASE_V5_REL
|
|
v6 = root / DEVICE_PLANE_MANAGER_RELEASE_V6_REL
|
|
v7 = root / DEVICE_PLANE_MANAGER_RELEASE_V7_REL
|
|
v8 = root / DEVICE_PLANE_MANAGER_RELEASE_V8_REL
|
|
v9 = root / DEVICE_PLANE_MANAGER_RELEASE_V9_REL
|
|
v10 = root / DEVICE_PLANE_MANAGER_RELEASE_V10_REL
|
|
v11 = root / DEVICE_PLANE_MANAGER_RELEASE_V11_REL
|
|
v12 = root / DEVICE_PLANE_MANAGER_RELEASE_V12_REL
|
|
v13 = root / DEVICE_PLANE_MANAGER_RELEASE_V13_REL
|
|
|
|
if v13.exists() or v13.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v13,
|
|
"installed Device Manager release v13 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v13",
|
|
boundaries=expected_device_plane_manager_release_v13_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V13_COMPOSE_SHA256
|
|
|
|
if v12.exists() or v12.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v12,
|
|
"installed Device Manager release v12 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v12",
|
|
boundaries=expected_device_plane_manager_release_v12_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V12_COMPOSE_SHA256
|
|
|
|
if v11.exists() or v11.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v11,
|
|
"installed Device Manager release v11 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v11",
|
|
boundaries=expected_device_plane_manager_release_v11_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V11_COMPOSE_SHA256
|
|
|
|
if v10.exists() or v10.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v10,
|
|
"installed Device Manager release v10 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v10",
|
|
boundaries=expected_device_plane_manager_release_v10_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V10_COMPOSE_SHA256
|
|
|
|
if v9.exists() or v9.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v9,
|
|
"installed Device Manager release v9 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v9",
|
|
boundaries=expected_device_plane_manager_release_v9_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V9_COMPOSE_SHA256
|
|
|
|
# A successful v2 overlay intentionally leaves the immutable v1 release
|
|
# descriptor as predecessor evidence. Prefer the highest installed
|
|
# generation, but validate its exact schema/security boundary before using
|
|
# the generation-specific Compose digest. During a failed v2 apply,
|
|
# rollback removes the candidate-only v2 descriptor before rebuilding the
|
|
# restored v1 runtime, so the same lookup follows the restored source.
|
|
if v8.exists() or v8.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v8,
|
|
"installed Device Manager release v8 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v8",
|
|
boundaries=expected_device_plane_manager_release_v8_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V8_COMPOSE_SHA256
|
|
|
|
if v7.exists() or v7.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v7,
|
|
"installed Device Manager release v7 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v7",
|
|
boundaries=expected_device_plane_manager_release_v7_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V7_COMPOSE_SHA256
|
|
|
|
if v6.exists() or v6.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v6,
|
|
"installed Device Manager release v6 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v6",
|
|
boundaries=expected_device_plane_manager_release_v6_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256
|
|
|
|
if v5.exists() or v5.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v5,
|
|
"installed Device Manager release v5 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v5",
|
|
boundaries=expected_device_plane_manager_release_v5_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256
|
|
|
|
if v4.exists() or v4.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v4,
|
|
"installed Device Manager release v4 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v4",
|
|
boundaries=expected_device_plane_manager_release_v4_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256
|
|
|
|
if v3.exists() or v3.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v3,
|
|
"installed Device Manager release v3 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v3",
|
|
boundaries=expected_device_plane_manager_release_v3_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256
|
|
|
|
if v2.exists() or v2.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v2,
|
|
"installed Device Manager release v2 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v2",
|
|
boundaries=expected_device_plane_manager_release_v2_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256
|
|
|
|
if v1.exists() or v1.is_symlink():
|
|
descriptor = read_strict_json(
|
|
v1,
|
|
"installed Device Manager release v1 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
validate_device_plane_manager_release_descriptor(
|
|
descriptor,
|
|
schema_version="nodedc.device-plane.device-manager-release.v1",
|
|
boundaries=expected_device_plane_manager_release_v1_boundaries(),
|
|
)
|
|
return DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256
|
|
|
|
die("installed Device Manager release descriptor is missing")
|
|
|
|
|
|
def validate_device_plane_manager_failed_control_plane_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL,
|
|
"failed Device Manager control-plane descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_manager_failed_control_plane_descriptor()
|
|
):
|
|
die("failed Device Manager control-plane descriptor mismatch")
|
|
compose_path = payload_dir / DEVICE_PLANE_MANAGER_COMPOSE_REL
|
|
if sha256_file(compose_path) != DEVICE_PLANE_MANAGER_COMPOSE_SHA256:
|
|
die("failed Device Manager control-plane Compose mismatch")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_reconciliation_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_MANAGER_RECONCILIATION_REL,
|
|
"Device Manager control-plane reconciliation descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_device_plane_manager_reconciliation_descriptor():
|
|
die("Device Manager control-plane reconciliation descriptor mismatch")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_manager_v2_reconciliation_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
|
|
"Device Manager v2 control-plane reconciliation descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_manager_v2_reconciliation_descriptor()
|
|
):
|
|
die(
|
|
"Device Manager v2 control-plane reconciliation "
|
|
"descriptor mismatch"
|
|
)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_control_core_v3_reconciliation_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_REL,
|
|
"Device Control Core v3 reconciliation descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_control_core_v3_reconciliation_descriptor()
|
|
):
|
|
die("Device Control Core v3 reconciliation descriptor mismatch")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_control_core_incident_audit_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_CONTROL_CORE_INCIDENT_AUDIT_REL,
|
|
"Device Control Core incident audit descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_device_plane_control_core_incident_audit_descriptor():
|
|
die("Device Control Core incident audit descriptor mismatch")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_control_core_migration_replay_audit_payload(
|
|
payload_dir,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_REL,
|
|
"Device Control Core migration replay audit descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_control_core_migration_replay_audit_descriptor()
|
|
):
|
|
die("Device Control Core migration replay audit descriptor mismatch")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_control_core_migration_replay_recovery_payload(
|
|
payload_dir,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir
|
|
/ DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL,
|
|
"Device Control Core migration replay recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_control_core_migration_replay_recovery_descriptor()
|
|
):
|
|
die("Device Control Core migration replay recovery descriptor mismatch")
|
|
validate_device_plane_control_core_migration_014_repair(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_control_core_migration_replay_checkpoint_recovery_payload(
|
|
payload_dir,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir
|
|
/ DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL,
|
|
"Device Control Core migration replay checkpoint recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_control_core_migration_replay_checkpoint_recovery_descriptor()
|
|
):
|
|
die(
|
|
"Device Control Core migration replay checkpoint recovery "
|
|
"descriptor mismatch"
|
|
)
|
|
validate_device_plane_control_core_migration_014_repair(payload_dir)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_control_core_migration_014_repair(payload_dir):
|
|
migration = payload_dir / DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
|
|
try:
|
|
migration_stat = migration.lstat()
|
|
migration_text = migration.read_text(encoding="utf-8")
|
|
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
die("Device Control Core migration 014 repair is unreadable")
|
|
if (
|
|
stat.S_ISLNK(migration_stat.st_mode)
|
|
or not stat.S_ISREG(migration_stat.st_mode)
|
|
or sha256_file(migration)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256
|
|
or ")) not valid;" not in migration_text.lower()
|
|
or re.search(
|
|
r"(?im)^\s*(?:delete|update|insert|truncate)\b",
|
|
migration_text,
|
|
)
|
|
):
|
|
die("Device Control Core migration 014 repair mismatch")
|
|
|
|
|
|
def is_device_plane_postgres_bootstrap_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_POSTGRES_BOOTSTRAP_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_foundation_recovery_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_FOUNDATION_RECOVERY_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_foundation_network_publication_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_b2_discovery_ingress_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_B2_DISCOVERY_INGRESS_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_backhaul_target_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries) == DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES
|
|
)
|
|
|
|
|
|
def is_device_plane_backhaul_vps_enrollment_slice(component, entries):
|
|
return (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_ENTRIES
|
|
)
|
|
|
|
|
|
def expected_device_plane_backhaul_vps_enrollment_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.backhaul-vps-enrollment.v1"
|
|
),
|
|
"mode": "rotate-backhaul-client-mini-to-vps",
|
|
"predecessorPatchId": (
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_PATCH_ID
|
|
),
|
|
"predecessorArtifactSha256": (
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
"sourceAction": "publish-vps-enrollment-marker-only",
|
|
"runtimeAction": (
|
|
"rotate-authorized-key-and-recreate-backhaul-target"
|
|
),
|
|
"selectedServices": [DEVICE_PLANE_BACKHAUL_TARGET_SERVICE],
|
|
"preservedServices": list(DEVICE_PLANE_RUNTIME_SERVICES),
|
|
"previousEnrollment": "device-edge-backhaul.pub",
|
|
"nextEnrollment": "device-edge-vps-backhaul.pub",
|
|
"nextKeyFingerprint": (
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_FINGERPRINT
|
|
),
|
|
"permittedTarget": DEVICE_PLANE_BACKHAUL_PERMITTED_TARGET,
|
|
"tailnetAddress": DEVICE_PLANE_BACKHAUL_TAILNET_ADDRESS,
|
|
"dockerPortPublication": "disabled",
|
|
"routerNatFirewall": "unchanged",
|
|
"edgePublicIngress": "disabled",
|
|
"funnel": "disabled",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"rollback": (
|
|
"restore-previous-authorized-key-and-recreate-target"
|
|
),
|
|
}
|
|
|
|
|
|
def validate_device_plane_backhaul_vps_enrollment_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_REL,
|
|
"Device Plane VPS backhaul enrollment descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_device_plane_backhaul_vps_enrollment_descriptor():
|
|
die("Device Plane VPS backhaul enrollment descriptor mismatch")
|
|
return descriptor
|
|
|
|
|
|
def expected_device_plane_backhaul_target_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.backhaul-target-tailnet-serve.v1"
|
|
),
|
|
"mode": "failed-backhaul-target-to-loopback-tailnet-serve",
|
|
"failedPatchId": DEVICE_PLANE_BACKHAUL_FAILED_PATCH_ID,
|
|
"failedArtifactSha256": (
|
|
DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"failedBackupId": DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_ID,
|
|
"predecessorPatchId": (
|
|
DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_PATCH_ID
|
|
),
|
|
"predecessorArtifactSha256": (
|
|
DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
"sourceAction": "publish-loopback-backhaul-target-source",
|
|
"runtimeAction": (
|
|
"build-create-target-and-register-private-tailnet-serve"
|
|
),
|
|
"composeOverlay": DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
|
"selectedServices": [DEVICE_PLANE_BACKHAUL_TARGET_SERVICE],
|
|
"preservedServices": list(DEVICE_PLANE_RUNTIME_SERVICES),
|
|
"loopbackListenAddress": DEVICE_PLANE_BACKHAUL_LOOPBACK_ADDRESS,
|
|
"listenPort": DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
"tailnetAddress": DEVICE_PLANE_BACKHAUL_TAILNET_ADDRESS,
|
|
"tailnetExposure": "tailscale-serve-private",
|
|
"tailscaleServeTarget": (
|
|
DEVICE_PLANE_BACKHAUL_TAILSCALE_SERVE_TARGET
|
|
),
|
|
"permittedTarget": DEVICE_PLANE_BACKHAUL_PERMITTED_TARGET,
|
|
"networkMode": "host",
|
|
"dockerPortPublication": "disabled",
|
|
"routerNatFirewall": "unchanged",
|
|
"edgePublicIngress": "disabled",
|
|
"funnel": "disabled",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"runtimeTrust": "runner-managed",
|
|
"rollback": "remove-tailnet-serve-target-and-restore-source",
|
|
}
|
|
|
|
|
|
def validate_device_plane_backhaul_target_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_BACKHAUL_TARGET_REL,
|
|
"Device Plane backhaul target descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_device_plane_backhaul_target_descriptor():
|
|
die("Device Plane backhaul target descriptor mismatch")
|
|
compose = payload_dir / DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL
|
|
if (
|
|
compose.is_symlink()
|
|
or not compose.is_file()
|
|
or sha256_file(compose)
|
|
!= DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_SHA256
|
|
):
|
|
die("Device Plane backhaul target Compose mismatch")
|
|
return descriptor
|
|
|
|
|
|
def read_device_plane_ed25519_enrollment_public_key(path, comment, label):
|
|
try:
|
|
path_stat = path.lstat()
|
|
text = path.read_text(encoding="ascii")
|
|
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
die(
|
|
f"{label} is missing or unreadable: "
|
|
f"{path}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_size > 1024
|
|
):
|
|
die(f"{label} is unsafe")
|
|
if text != text.strip() + "\n" or "\n" in text.strip():
|
|
die(f"{label} must be one line")
|
|
parts = text.strip().split()
|
|
if len(parts) not in (2, 3) or parts[0] != "ssh-ed25519":
|
|
die(f"{label} type mismatch")
|
|
try:
|
|
blob = base64.b64decode(parts[1], validate=True)
|
|
except Exception:
|
|
die(f"{label} encoding mismatch")
|
|
expected_prefix = b"\x00\x00\x00\x0bssh-ed25519\x00\x00\x00\x20"
|
|
if len(blob) != len(expected_prefix) + 32 or not blob.startswith(
|
|
expected_prefix
|
|
):
|
|
die(f"{label} shape mismatch")
|
|
normalized = f"ssh-ed25519 {parts[1]} {comment}"
|
|
return {
|
|
"line": normalized,
|
|
"sha256": hashlib.sha256((normalized + "\n").encode("ascii")).hexdigest(),
|
|
"fingerprint": (
|
|
"SHA256:"
|
|
+ base64.b64encode(hashlib.sha256(blob).digest())
|
|
.decode("ascii")
|
|
.rstrip("=")
|
|
),
|
|
}
|
|
|
|
|
|
def read_device_plane_backhaul_enrollment_public_key():
|
|
return read_device_plane_ed25519_enrollment_public_key(
|
|
DEVICE_PLANE_BACKHAUL_ENROLLMENT_PUBLIC_KEY_FILE,
|
|
"nodedc-device-edge-backhaul",
|
|
"Device Plane Edge enrollment public key",
|
|
)
|
|
|
|
|
|
def read_device_plane_backhaul_vps_enrollment_public_key():
|
|
enrollment = read_device_plane_ed25519_enrollment_public_key(
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PUBLIC_KEY_FILE,
|
|
"nodedc-device-edge-vps-backhaul",
|
|
"Device Plane VPS Edge enrollment public key",
|
|
)
|
|
if (
|
|
enrollment["fingerprint"]
|
|
!= DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_FINGERPRINT
|
|
):
|
|
die("Device Plane VPS Edge enrollment fingerprint mismatch")
|
|
return enrollment
|
|
|
|
|
|
def device_plane_tailscale_drop_privileges(uid, gid):
|
|
def demote():
|
|
os.setgroups([])
|
|
os.setgid(gid)
|
|
os.setuid(uid)
|
|
|
|
return demote
|
|
|
|
|
|
def validate_device_plane_tailscale_cli():
|
|
privilege_path = DEVICE_PLANE_TAILSCALE_PRIVILEGE
|
|
try:
|
|
privilege_stat = privilege_path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Synology Tailscale package privilege contract is missing: "
|
|
f"{privilege_path}"
|
|
)
|
|
if (
|
|
not stat.S_ISREG(privilege_stat.st_mode)
|
|
or privilege_stat.st_uid != 0
|
|
or privilege_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)
|
|
):
|
|
die("Synology Tailscale package privilege boundary mismatch")
|
|
privilege = read_strict_json(
|
|
privilege_path,
|
|
"Synology Tailscale package privilege contract",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
defaults = privilege.get("defaults")
|
|
if (
|
|
not isinstance(defaults, dict)
|
|
or defaults.get("run-as") != "package"
|
|
or privilege.get("username") != DEVICE_PLANE_TAILSCALE_USER
|
|
or privilege.get("groupname") != DEVICE_PLANE_TAILSCALE_GROUP
|
|
):
|
|
die("Synology Tailscale package identity contract mismatch")
|
|
try:
|
|
account = pwd.getpwnam(DEVICE_PLANE_TAILSCALE_USER)
|
|
group = grp.getgrnam(DEVICE_PLANE_TAILSCALE_GROUP)
|
|
except KeyError:
|
|
die("Synology Tailscale package account is missing")
|
|
if (
|
|
account.pw_uid <= 0
|
|
or group.gr_gid <= 0
|
|
or account.pw_gid != group.gr_gid
|
|
):
|
|
die("Synology Tailscale package account boundary mismatch")
|
|
|
|
try:
|
|
binary_lstat = DEVICE_PLANE_TAILSCALE.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Synology Tailscale CLI is missing: "
|
|
f"{DEVICE_PLANE_TAILSCALE}"
|
|
)
|
|
if (
|
|
not stat.S_ISREG(binary_lstat.st_mode)
|
|
or binary_lstat.st_uid not in (0, account.pw_uid)
|
|
or binary_lstat.st_mode
|
|
& (
|
|
stat.S_IWGRP
|
|
| stat.S_IWOTH
|
|
| stat.S_ISUID
|
|
| stat.S_ISGID
|
|
)
|
|
or binary_lstat.st_size < 1024 * 1024
|
|
or binary_lstat.st_size > 256 * 1024 * 1024
|
|
):
|
|
die(
|
|
"Synology Tailscale CLI file boundary mismatch: "
|
|
f"uid={binary_lstat.st_uid} "
|
|
f"mode={stat.S_IMODE(binary_lstat.st_mode):04o} "
|
|
f"size={binary_lstat.st_size}"
|
|
)
|
|
if binary_lstat.st_uid == account.pw_uid:
|
|
executable = bool(binary_lstat.st_mode & stat.S_IXUSR)
|
|
elif binary_lstat.st_gid == group.gr_gid:
|
|
executable = bool(binary_lstat.st_mode & stat.S_IXGRP)
|
|
else:
|
|
executable = bool(binary_lstat.st_mode & stat.S_IXOTH)
|
|
if not executable:
|
|
die("Synology Tailscale CLI is not executable by package account")
|
|
|
|
context = {
|
|
"binary": str(DEVICE_PLANE_TAILSCALE),
|
|
"uid": account.pw_uid,
|
|
"gid": group.gr_gid,
|
|
"binarySha256": sha256_file(DEVICE_PLANE_TAILSCALE),
|
|
}
|
|
try:
|
|
help_result = subprocess.run(
|
|
[context["binary"], "serve", "--help"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=15,
|
|
preexec_fn=device_plane_tailscale_drop_privileges(
|
|
context["uid"],
|
|
context["gid"],
|
|
),
|
|
)
|
|
except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
die("Synology Tailscale Serve capability is unavailable")
|
|
help_text = help_result.stdout + help_result.stderr
|
|
for required in ("--tcp", "--bg", "--yes"):
|
|
if required not in help_text:
|
|
die(
|
|
"Synology Tailscale Serve capability mismatch: "
|
|
f"{required}"
|
|
)
|
|
return context
|
|
|
|
|
|
def run_device_plane_tailscale(args, **kwargs):
|
|
context = validate_device_plane_tailscale_cli()
|
|
return subprocess.run(
|
|
[context["binary"], *args],
|
|
preexec_fn=device_plane_tailscale_drop_privileges(
|
|
context["uid"],
|
|
context["gid"],
|
|
),
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def read_device_plane_tailscale_json(*args, allow_no_config=False):
|
|
try:
|
|
result = run_device_plane_tailscale(
|
|
args,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=20,
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
die("Synology Tailscale state query failed")
|
|
combined = (result.stdout + "\n" + result.stderr).strip()
|
|
if result.returncode != 0:
|
|
if allow_no_config and "no serve config" in combined.lower():
|
|
return {}
|
|
die(
|
|
"Synology Tailscale state query failed: "
|
|
f"exit={result.returncode}"
|
|
)
|
|
if len(result.stdout.encode("utf-8")) > 4 * 1024 * 1024:
|
|
die("Synology Tailscale state response is too large")
|
|
try:
|
|
value = json.loads(result.stdout or "{}")
|
|
except json.JSONDecodeError:
|
|
die("Synology Tailscale state response is invalid JSON")
|
|
if not isinstance(value, dict):
|
|
die("Synology Tailscale state response must be an object")
|
|
return value
|
|
|
|
|
|
def device_plane_tailscale_handlers_for_port(config, port):
|
|
matches = []
|
|
|
|
def walk(value, path):
|
|
if not isinstance(value, dict):
|
|
return
|
|
tcp = value.get("TCP")
|
|
if tcp is not None:
|
|
if not isinstance(tcp, dict):
|
|
die("Synology Tailscale TCP Serve state is invalid")
|
|
handler = tcp.get(str(port))
|
|
if handler is not None:
|
|
matches.append((tuple(path), handler))
|
|
for key in ("Foreground", "Services"):
|
|
nested = value.get(key)
|
|
if nested is None:
|
|
continue
|
|
if not isinstance(nested, dict):
|
|
die("Synology Tailscale nested Serve state is invalid")
|
|
for name, child in nested.items():
|
|
walk(child, (*path, key, str(name)))
|
|
|
|
walk(config, ())
|
|
return matches
|
|
|
|
|
|
def device_plane_tailscale_funnel_uses_port(config, port):
|
|
allow_funnel = config.get("AllowFunnel") or {}
|
|
if not isinstance(allow_funnel, dict):
|
|
die("Synology Tailscale Funnel state is invalid")
|
|
suffix = f":{port}"
|
|
return any(
|
|
enabled is True
|
|
and (str(host_port) == str(port) or str(host_port).endswith(suffix))
|
|
for host_port, enabled in allow_funnel.items()
|
|
)
|
|
|
|
|
|
def remove_device_plane_tailscale_port(config, port):
|
|
normalized = json.loads(json.dumps(config))
|
|
tcp = normalized.get("TCP")
|
|
if isinstance(tcp, dict):
|
|
tcp.pop(str(port), None)
|
|
if not tcp:
|
|
normalized.pop("TCP", None)
|
|
return normalized
|
|
|
|
|
|
def validate_device_plane_tailscale_runtime(require_target):
|
|
status = read_device_plane_tailscale_json("status", "--json")
|
|
self_state = status.get("Self")
|
|
if not isinstance(self_state, dict):
|
|
die("Synology Tailscale self state is missing")
|
|
addresses = self_state.get("TailscaleIPs") or []
|
|
if (
|
|
not isinstance(addresses, list)
|
|
or DEVICE_PLANE_BACKHAUL_TAILNET_ADDRESS not in addresses
|
|
or self_state.get("Online") is not True
|
|
or status.get("BackendState") != "Running"
|
|
):
|
|
die("Synology Tailscale tailnet state mismatch")
|
|
|
|
serve = read_device_plane_tailscale_json(
|
|
"serve",
|
|
"status",
|
|
"--json",
|
|
allow_no_config=True,
|
|
)
|
|
handlers = device_plane_tailscale_handlers_for_port(
|
|
serve,
|
|
DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
)
|
|
if device_plane_tailscale_funnel_uses_port(
|
|
serve,
|
|
DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
):
|
|
die("Synology Tailscale Funnel exposure is forbidden")
|
|
if require_target:
|
|
expected_handler = {
|
|
"TCPForward": (
|
|
f"{DEVICE_PLANE_BACKHAUL_LOOPBACK_ADDRESS}:"
|
|
f"{DEVICE_PLANE_BACKHAUL_LISTEN_PORT}"
|
|
)
|
|
}
|
|
if handlers != [((), expected_handler)]:
|
|
die("Synology Tailscale private Serve target mismatch")
|
|
elif handlers:
|
|
die("Synology Tailscale port 2222 is already configured")
|
|
return {
|
|
"self": self_state,
|
|
"serve": serve,
|
|
}
|
|
|
|
|
|
def enable_device_plane_tailscale_serve(expected_before):
|
|
current = validate_device_plane_tailscale_runtime(
|
|
require_target=False
|
|
)["serve"]
|
|
if current != expected_before:
|
|
die("Synology Tailscale Serve state changed before activation")
|
|
run_device_plane_tailscale(
|
|
[
|
|
"serve",
|
|
"--bg",
|
|
"--yes",
|
|
f"--tcp={DEVICE_PLANE_BACKHAUL_LISTEN_PORT}",
|
|
DEVICE_PLANE_BACKHAUL_TAILSCALE_SERVE_TARGET,
|
|
],
|
|
check=True,
|
|
timeout=30,
|
|
)
|
|
after = validate_device_plane_tailscale_runtime(
|
|
require_target=True
|
|
)["serve"]
|
|
if remove_device_plane_tailscale_port(
|
|
after,
|
|
DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
) != expected_before:
|
|
die("Synology Tailscale Serve activation changed unrelated state")
|
|
return after
|
|
|
|
|
|
def disable_device_plane_tailscale_serve(expected_after):
|
|
current = read_device_plane_tailscale_json(
|
|
"serve",
|
|
"status",
|
|
"--json",
|
|
allow_no_config=True,
|
|
)
|
|
handlers = device_plane_tailscale_handlers_for_port(
|
|
current,
|
|
DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
)
|
|
if not handlers:
|
|
if current != expected_after:
|
|
die("Synology Tailscale Serve rollback state mismatch")
|
|
return False
|
|
expected_handler = {
|
|
"TCPForward": (
|
|
f"{DEVICE_PLANE_BACKHAUL_LOOPBACK_ADDRESS}:"
|
|
f"{DEVICE_PLANE_BACKHAUL_LISTEN_PORT}"
|
|
)
|
|
}
|
|
if (
|
|
handlers != [((), expected_handler)]
|
|
or device_plane_tailscale_funnel_uses_port(
|
|
current,
|
|
DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
)
|
|
or remove_device_plane_tailscale_port(
|
|
current,
|
|
DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
) != expected_after
|
|
):
|
|
die("Synology Tailscale Serve rollback refuses unexpected state")
|
|
run_device_plane_tailscale(
|
|
[
|
|
"serve",
|
|
f"--tcp={DEVICE_PLANE_BACKHAUL_LISTEN_PORT}",
|
|
"off",
|
|
],
|
|
check=True,
|
|
timeout=30,
|
|
)
|
|
final = validate_device_plane_tailscale_runtime(
|
|
require_target=False
|
|
)["serve"]
|
|
if final != expected_after:
|
|
die("Synology Tailscale Serve rollback changed unrelated state")
|
|
return True
|
|
|
|
|
|
def expected_failed_device_plane_backhaul_target_descriptor():
|
|
return {
|
|
"schemaVersion": "nodedc.device-plane.backhaul-target.v1",
|
|
"mode": "restricted-edge-backhaul-target",
|
|
"predecessorPatchId": (
|
|
DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_PATCH_ID
|
|
),
|
|
"predecessorArtifactSha256": (
|
|
DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
"sourceAction": "publish-restricted-backhaul-target-source",
|
|
"runtimeAction": "build-and-create-device-backhaul-target",
|
|
"composeOverlay": DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
|
"selectedServices": [DEVICE_PLANE_BACKHAUL_TARGET_SERVICE],
|
|
"preservedServices": list(DEVICE_PLANE_RUNTIME_SERVICES),
|
|
"listenAddress": DEVICE_PLANE_BACKHAUL_TAILNET_ADDRESS,
|
|
"listenPort": DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
"permittedTarget": DEVICE_PLANE_BACKHAUL_PERMITTED_TARGET,
|
|
"networkMode": "host",
|
|
"edgeIngress": "disabled",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"runtimeTrust": "runner-managed",
|
|
"rollback": "remove-target-and-restore-source",
|
|
}
|
|
|
|
|
|
def validate_device_plane_backhaul_failed_evidence():
|
|
backup_dir = BACKUPS_DIR / DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_ID
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane failed backhaul backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Plane failed backhaul backup is unsafe")
|
|
if {child.name for child in backup_dir.iterdir()} != set(
|
|
DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_SHA256
|
|
):
|
|
die("Device Plane failed backhaul backup file set mismatch")
|
|
for name, expected_sha256 in (
|
|
DEVICE_PLANE_BACKHAUL_FAILED_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_sha256
|
|
):
|
|
die(
|
|
"Device Plane failed backhaul backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
|
|
failed_artifact = FAILED_DIR / DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane failed backhaul 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)
|
|
!= DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Plane failed backhaul 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("Device Plane failed backhaul journal is unreadable")
|
|
if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISREG(
|
|
state_stat.st_mode
|
|
):
|
|
die("Device Plane failed backhaul journal is unsafe")
|
|
records = []
|
|
for line in state_lines:
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
die("Device Plane failed backhaul journal contains invalid JSON")
|
|
if (
|
|
isinstance(value, dict)
|
|
and value.get("id") == DEVICE_PLANE_BACKHAUL_FAILED_PATCH_ID
|
|
):
|
|
records.append(value)
|
|
if len(records) != 1:
|
|
die("Device Plane failed backhaul journal evidence count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact") != DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT
|
|
or record.get("backup_id")
|
|
!= DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_ID
|
|
or record.get("component") != "device-plane"
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not True
|
|
or record.get("rollback_status")
|
|
!= (
|
|
"ok:device-plane-overlay:source-restored-target-removed-"
|
|
"preserved-runtime-unchanged:3"
|
|
)
|
|
or record.get("status") != "failed"
|
|
or record.get("message") != DEVICE_PLANE_BACKHAUL_FAILED_MESSAGE
|
|
):
|
|
die("Device Plane failed backhaul journal evidence mismatch")
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-plane-failed-backhaul-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_work = Path(directory)
|
|
safe_extract(failed_artifact, failed_work)
|
|
failed_manifest = parse_manifest(failed_work / "manifest.env")
|
|
failed_entries = parse_files_list(failed_work / "files.txt")
|
|
failed_payload = failed_work / "payload"
|
|
expected_payload_paths = {
|
|
"deployment",
|
|
DEVICE_PLANE_BACKHAUL_FAILED_TARGET_REL,
|
|
DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
|
"services",
|
|
"services/device-backhaul-target",
|
|
"services/device-backhaul-target/Dockerfile",
|
|
"services/device-backhaul-target/sshd_config",
|
|
}
|
|
actual_payload_paths = {
|
|
path.relative_to(failed_payload).as_posix()
|
|
for path in failed_payload.rglob("*")
|
|
}
|
|
if (
|
|
failed_manifest.get("id")
|
|
!= DEVICE_PLANE_BACKHAUL_FAILED_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries)
|
|
!= DEVICE_PLANE_BACKHAUL_FAILED_TARGET_ENTRIES
|
|
or actual_payload_paths != expected_payload_paths
|
|
):
|
|
die("Device Plane failed backhaul artifact contract mismatch")
|
|
failed_descriptor = read_strict_json(
|
|
failed_payload / DEVICE_PLANE_BACKHAUL_FAILED_TARGET_REL,
|
|
"failed Device Plane backhaul descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
failed_descriptor
|
|
!= expected_failed_device_plane_backhaul_target_descriptor()
|
|
):
|
|
die("Device Plane failed backhaul descriptor mismatch")
|
|
return {
|
|
"backup": backup_dir,
|
|
"failedArtifact": failed_artifact,
|
|
}
|
|
|
|
|
|
def validate_device_plane_backhaul_target_evidence(payload_dir):
|
|
descriptor = validate_device_plane_backhaul_target_payload(payload_dir)
|
|
failed = validate_device_plane_backhaul_failed_evidence()
|
|
if not state_has_patch_id(
|
|
DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_PATCH_ID
|
|
):
|
|
die("Device Plane backhaul predecessor patch is not applied")
|
|
if not state_has_sha(
|
|
DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_ARTIFACT_SHA256
|
|
):
|
|
die("Device Plane backhaul predecessor artifact is not applied")
|
|
|
|
root = component_root("device-plane")
|
|
compose = root / "docker-compose.device-plane.yml"
|
|
if (
|
|
compose.is_symlink()
|
|
or not compose.is_file()
|
|
or sha256_file(compose) != DEVICE_PLANE_B2_DISCOVERY_INGRESS_COMPOSE_SHA256
|
|
):
|
|
die("Device Plane backhaul predecessor Compose drift detected")
|
|
installed_b2 = read_strict_json(
|
|
root / DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL,
|
|
"installed Device Plane B2 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_b2 != expected_device_plane_b2_discovery_ingress_descriptor():
|
|
die("Device Plane backhaul predecessor B2 descriptor mismatch")
|
|
for rel in (
|
|
DEVICE_PLANE_BACKHAUL_FAILED_TARGET_REL,
|
|
DEVICE_PLANE_BACKHAUL_TARGET_REL,
|
|
DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
|
"services/device-backhaul-target",
|
|
):
|
|
target = root / rel
|
|
if target.exists() or target.is_symlink():
|
|
die(
|
|
"Device Plane failed backhaul source was not restored: "
|
|
f"{rel}"
|
|
)
|
|
if device_plane_service_container_ids(
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE
|
|
):
|
|
die("Device Plane backhaul target container already exists")
|
|
|
|
runtime = device_plane_runtime_inventory(DEVICE_PLANE_RUNTIME_SERVICES)
|
|
names = device_plane_inventory_service_names(runtime)
|
|
if set(names) != set(DEVICE_PLANE_RUNTIME_SERVICES):
|
|
die("Device Plane backhaul predecessor runtime is incomplete")
|
|
for item in runtime["services"]:
|
|
if (
|
|
item["status"] != "running"
|
|
or item["running"] is not True
|
|
or item["health"] != "healthy"
|
|
or item["restartCount"] != 0
|
|
):
|
|
die(
|
|
"Device Plane backhaul predecessor service is unhealthy: "
|
|
f"{item['service']}"
|
|
)
|
|
assert_loopback_tcp_port_open(9921)
|
|
enrollment = read_device_plane_backhaul_enrollment_public_key()
|
|
tailscale_cli = validate_device_plane_tailscale_cli()
|
|
tailscale = validate_device_plane_tailscale_runtime(
|
|
require_target=False
|
|
)
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"failedBackup": str(failed["backup"]),
|
|
"failedArtifact": str(failed["failedArtifact"]),
|
|
"runtime": runtime,
|
|
"enrollmentPublicKeySha256": enrollment["sha256"],
|
|
"tailscaleCli": tailscale_cli,
|
|
"tailscaleServeBefore": tailscale["serve"],
|
|
}
|
|
|
|
|
|
def validate_device_plane_backhaul_vps_enrollment_evidence(payload_dir):
|
|
descriptor = validate_device_plane_backhaul_vps_enrollment_payload(
|
|
payload_dir
|
|
)
|
|
if not state_has_patch_id(
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_PATCH_ID
|
|
):
|
|
die("Device Plane VPS enrollment predecessor patch is not applied")
|
|
if not state_has_sha(
|
|
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_ARTIFACT_SHA256
|
|
):
|
|
die("Device Plane VPS enrollment predecessor artifact is not applied")
|
|
root = component_root("device-plane")
|
|
marker = root / DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_REL
|
|
if marker.exists() or marker.is_symlink():
|
|
die("Device Plane VPS enrollment marker already exists")
|
|
runtime = device_plane_runtime_inventory(DEVICE_PLANE_RUNTIME_SERVICES)
|
|
validate_device_plane_backhaul_target_runtime(runtime)
|
|
previous = read_device_plane_backhaul_enrollment_public_key()
|
|
next_enrollment = read_device_plane_backhaul_vps_enrollment_public_key()
|
|
if previous["line"] == next_enrollment["line"]:
|
|
die("Device Plane VPS enrollment key is not a new identity")
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"runtime": runtime,
|
|
"previousEnrollmentPublicKeySha256": previous["sha256"],
|
|
"nextEnrollmentPublicKeySha256": next_enrollment["sha256"],
|
|
"nextKeyFingerprint": next_enrollment["fingerprint"],
|
|
}
|
|
|
|
|
|
def expected_device_plane_foundation_recovery_descriptor():
|
|
return {
|
|
"schemaVersion": "nodedc.device-plane.foundation-recovery.v1",
|
|
"mode": "failed-foundation-live-runtime-adoption",
|
|
"failedPatchId": DEVICE_PLANE_FOUNDATION_FAILED_PATCH_ID,
|
|
"failedArtifactSha256": (
|
|
DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"backupId": DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_ID,
|
|
"sourceAction": "publish-exact-failed-artifact-source",
|
|
"runtimeAction": "read-only-acceptance",
|
|
"preservedServices": [
|
|
"device-control-core",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
],
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"rollback": "source-only-runtime-unchanged",
|
|
}
|
|
|
|
|
|
def validate_device_plane_foundation_recovery_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_FOUNDATION_RECOVERY_REL,
|
|
"Device Plane foundation recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_device_plane_foundation_recovery_descriptor():
|
|
die("Device Plane foundation recovery descriptor mismatch")
|
|
return descriptor
|
|
|
|
|
|
def expected_device_plane_foundation_network_publication_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.foundation-network-publication.v1"
|
|
),
|
|
"mode": "failed-foundation-network-publication-correction",
|
|
"failedRecoveryPatchId": (
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_PATCH_ID
|
|
),
|
|
"failedRecoveryArtifactSha256": (
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"failedRecoveryBackupId": (
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_BACKUP_ID
|
|
),
|
|
"sourceAction": "publish-network-corrected-foundation-source",
|
|
"runtimeAction": "recreate-stateless-services-no-build",
|
|
"selectedServices": [
|
|
"device-control-core",
|
|
"device-gateway",
|
|
],
|
|
"preservedServices": ["device-postgres"],
|
|
"privateNetwork": DEVICE_PLANE_PRIVATE_NETWORK,
|
|
"controlNetwork": DEVICE_PLANE_CONTROL_NETWORK,
|
|
"publishedLoopbackPorts": [
|
|
"127.0.0.1:18120:18120",
|
|
"127.0.0.1:18121:18121",
|
|
],
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"rollback": (
|
|
"restore-partial-source-and-internal-only-stateless-runtime"
|
|
),
|
|
}
|
|
|
|
|
|
def validate_device_plane_foundation_network_publication_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_REL,
|
|
"Device Plane foundation network-publication descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
expected = (
|
|
expected_device_plane_foundation_network_publication_descriptor()
|
|
)
|
|
if descriptor != expected:
|
|
die(
|
|
"Device Plane foundation network-publication descriptor mismatch"
|
|
)
|
|
compose = payload_dir / "docker-compose.device-plane.yml"
|
|
if (
|
|
compose.is_symlink()
|
|
or not compose.is_file()
|
|
or sha256_file(compose)
|
|
!= DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_COMPOSE_SHA256
|
|
):
|
|
die("Device Plane network-publication Compose mismatch")
|
|
return descriptor
|
|
|
|
|
|
def expected_device_plane_b2_discovery_ingress_descriptor():
|
|
return {
|
|
"schemaVersion": "nodedc.device-plane.b2-discovery-ingress.v1",
|
|
"mode": "verified-b2-loopback-discovery-only",
|
|
"predecessorPatchId": (
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_PREDECESSOR_PATCH_ID
|
|
),
|
|
"predecessorArtifactSha256": (
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
"sourceAction": "publish-verified-b2-loopback-discovery-source",
|
|
"runtimeAction": "build-and-recreate-stateless-services",
|
|
"selectedServices": [
|
|
"device-control-core",
|
|
"device-gateway",
|
|
],
|
|
"preservedServices": ["device-postgres"],
|
|
"privateNetwork": DEVICE_PLANE_PRIVATE_NETWORK,
|
|
"controlNetwork": DEVICE_PLANE_CONTROL_NETWORK,
|
|
"publishedPorts": [
|
|
"127.0.0.1:18120:18120",
|
|
"127.0.0.1:18121:18121",
|
|
"127.0.0.1:9921:9921/tcp",
|
|
],
|
|
"protocolProfile": "arusnavi.b2.internal.v1",
|
|
"framingSpecification": (
|
|
"arusnavi.internal.protocol-sheet.gid-12.v1"
|
|
),
|
|
"identityTrust": "claimed-not-ownership-proof",
|
|
"discoveryLifecycle": "quarantine",
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"rollback": "restore-source-and-predecessor-stateless-runtime",
|
|
}
|
|
|
|
|
|
def expected_device_plane_b2_discovery_rollback_recovery_descriptor():
|
|
return {
|
|
"schemaVersion": (
|
|
"nodedc.device-plane.b2-discovery-loopback-recovery.v1"
|
|
),
|
|
"mode": "failed-b2-loopback-build-reconciliation",
|
|
"failedPatchId": DEVICE_PLANE_B2_DISCOVERY_FAILED_PATCH_ID,
|
|
"failedArtifactSha256": (
|
|
DEVICE_PLANE_B2_DISCOVERY_FAILED_ARTIFACT_SHA256
|
|
),
|
|
"failedBackupId": (
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_BACKUP_ID
|
|
),
|
|
"sourceAction": "publish-reconciliation-marker-only",
|
|
"runtimeAction": "read-only-acceptance",
|
|
"preservedServices": [
|
|
"device-control-core",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
],
|
|
"expectedLoopbackPorts": [
|
|
"127.0.0.1:18120:18120",
|
|
"127.0.0.1:18121:18121",
|
|
],
|
|
"closedPort": "127.0.0.1:9921/tcp",
|
|
"databaseVolume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"commandTransport": "disabled",
|
|
"gelios": "untouched",
|
|
"rollback": "marker-only-runtime-unchanged",
|
|
}
|
|
|
|
|
|
def validate_device_plane_b2_discovery_rollback_recovery_payload(
|
|
payload_dir,
|
|
):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_REL,
|
|
"Device Plane B2 discovery rollback recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
expected = (
|
|
expected_device_plane_b2_discovery_rollback_recovery_descriptor()
|
|
)
|
|
if descriptor != expected:
|
|
die(
|
|
"Device Plane B2 discovery rollback recovery descriptor "
|
|
"mismatch"
|
|
)
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_b2_discovery_ingress_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL,
|
|
"Device Plane B2 discovery ingress descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_device_plane_b2_discovery_ingress_descriptor():
|
|
die("Device Plane B2 discovery ingress descriptor mismatch")
|
|
compose = payload_dir / "docker-compose.device-plane.yml"
|
|
if (
|
|
compose.is_symlink()
|
|
or not compose.is_file()
|
|
or sha256_file(compose)
|
|
!= DEVICE_PLANE_B2_DISCOVERY_INGRESS_COMPOSE_SHA256
|
|
):
|
|
die("Device Plane B2 discovery ingress Compose mismatch")
|
|
return descriptor
|
|
|
|
|
|
def validate_device_plane_b2_discovery_ingress_evidence(payload_dir):
|
|
descriptor = validate_device_plane_b2_discovery_ingress_payload(
|
|
payload_dir
|
|
)
|
|
if not state_has_patch_id(
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_PREDECESSOR_PATCH_ID
|
|
):
|
|
die("Device Plane B2 discovery ingress predecessor patch missing")
|
|
if not state_has_sha(
|
|
DEVICE_PLANE_B2_DISCOVERY_INGRESS_PREDECESSOR_ARTIFACT_SHA256
|
|
):
|
|
die("Device Plane B2 discovery ingress predecessor artifact missing")
|
|
if not state_has_patch_id(
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_PATCH_ID
|
|
):
|
|
die("Device Plane B2 rollback recovery patch is not applied")
|
|
if not state_has_sha(
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_ARTIFACT_SHA256
|
|
):
|
|
die("Device Plane B2 rollback recovery artifact is not applied")
|
|
validate_device_plane_foundation_network_publication_installed_source()
|
|
runtime = validate_device_plane_foundation_runtime(
|
|
network_publication=True
|
|
)
|
|
assert_loopback_tcp_port_closed(9921)
|
|
target_descriptor = (
|
|
component_root("device-plane")
|
|
/ DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL
|
|
)
|
|
if target_descriptor.exists() or target_descriptor.is_symlink():
|
|
die("Device Plane B2 discovery ingress descriptor already installed")
|
|
recovery_descriptor = read_strict_json(
|
|
component_root("device-plane")
|
|
/ DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_REL,
|
|
"installed Device Plane B2 rollback recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
recovery_descriptor
|
|
!= expected_device_plane_b2_discovery_rollback_recovery_descriptor()
|
|
):
|
|
die("installed Device Plane B2 rollback recovery mismatch")
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def validate_device_plane_b2_discovery_rollback_recovery_evidence(
|
|
payload_dir,
|
|
):
|
|
descriptor = (
|
|
validate_device_plane_b2_discovery_rollback_recovery_payload(
|
|
payload_dir
|
|
)
|
|
)
|
|
backup_dir = (
|
|
BACKUPS_DIR
|
|
/ DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_BACKUP_ID
|
|
)
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane B2 failed-apply backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Plane B2 failed-apply backup is unsafe")
|
|
expected_backup = (
|
|
DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_BACKUP_SHA256
|
|
)
|
|
if {child.name for child in backup_dir.iterdir()} != set(
|
|
expected_backup
|
|
):
|
|
die("Device Plane B2 failed-apply backup file set mismatch")
|
|
for name, expected_sha256 in expected_backup.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_sha256
|
|
):
|
|
die(
|
|
"Device Plane B2 failed-apply backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
|
|
failed_artifact = (
|
|
FAILED_DIR / DEVICE_PLANE_B2_DISCOVERY_FAILED_ARTIFACT
|
|
)
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane B2 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)
|
|
!= DEVICE_PLANE_B2_DISCOVERY_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Plane B2 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("Device Plane B2 failed journal is unreadable")
|
|
if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISREG(
|
|
state_stat.st_mode
|
|
):
|
|
die("Device Plane B2 failed journal is unsafe")
|
|
records = []
|
|
for line in state_lines:
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
die("Device Plane B2 failed journal contains invalid JSON")
|
|
if (
|
|
isinstance(value, dict)
|
|
and value.get("id") == DEVICE_PLANE_B2_DISCOVERY_FAILED_PATCH_ID
|
|
):
|
|
records.append(value)
|
|
if len(records) != 1:
|
|
die("Device Plane B2 failed journal evidence count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact")
|
|
!= DEVICE_PLANE_B2_DISCOVERY_FAILED_ARTIFACT
|
|
or record.get("backup_id")
|
|
!= DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_BACKUP_ID
|
|
or record.get("component") != "device-plane"
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_B2_DISCOVERY_FAILED_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not True
|
|
or record.get("rollback_status") != "failed:CalledProcessError"
|
|
or record.get("status") != "failed"
|
|
or record.get("message")
|
|
!= (
|
|
"Command '['/usr/local/bin/docker', 'build', '--no-cache', "
|
|
"'--network=host', '-f', "
|
|
"'services/device-control-core/Dockerfile', '-t', "
|
|
"'nodedc/device-control-core:local', '.']' returned non-zero "
|
|
"exit status 1."
|
|
)
|
|
):
|
|
die("Device Plane B2 failed journal evidence mismatch")
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-plane-failed-b2-loopback-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, _failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id")
|
|
!= DEVICE_PLANE_B2_DISCOVERY_FAILED_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries)
|
|
!= DEVICE_PLANE_B2_DISCOVERY_FAILED_ENTRIES
|
|
):
|
|
die("Device Plane B2 failed artifact contract mismatch")
|
|
|
|
validate_device_plane_foundation_network_publication_installed_source()
|
|
runtime = validate_device_plane_foundation_runtime(
|
|
network_publication=True
|
|
)
|
|
assert_loopback_tcp_port_closed(9921)
|
|
root = component_root("device-plane")
|
|
failed_descriptor = root / DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL
|
|
if failed_descriptor.exists() or failed_descriptor.is_symlink():
|
|
die("Device Plane B2 failed descriptor remains installed")
|
|
target_descriptor = (
|
|
root / DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_REL
|
|
)
|
|
if target_descriptor.exists() or target_descriptor.is_symlink():
|
|
die("Device Plane B2 rollback recovery already installed")
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"backup": backup_dir,
|
|
"failedArtifact": failed_artifact,
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def validate_device_plane_manager_reconciliation_backup():
|
|
backup_dir = (
|
|
BACKUPS_DIR / DEVICE_PLANE_MANAGER_RECONCILIATION_BACKUP_ID
|
|
)
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Manager failed-apply backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Manager failed-apply backup is unsafe")
|
|
expected = DEVICE_PLANE_MANAGER_RECONCILIATION_BACKUP_SHA256
|
|
if {child.name for child in backup_dir.iterdir()} != set(expected):
|
|
die("Device Manager failed-apply backup file set mismatch")
|
|
for name, expected_sha256 in expected.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_sha256
|
|
):
|
|
die(
|
|
"Device Manager failed-apply backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
existing = tuple(read_backup_path_list(
|
|
backup_dir / "existing-files.txt"
|
|
))
|
|
missing = tuple(read_backup_path_list(
|
|
backup_dir / "missing-files.txt"
|
|
))
|
|
validate_backup_partition(
|
|
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES,
|
|
existing,
|
|
missing,
|
|
"Device Manager reconciliation",
|
|
)
|
|
if (
|
|
existing != DEVICE_PLANE_MANAGER_RECONCILIATION_EXISTING
|
|
or missing != DEVICE_PLANE_MANAGER_RECONCILIATION_MISSING
|
|
):
|
|
die("Device Manager failed-apply backup partition mismatch")
|
|
return backup_dir
|
|
|
|
|
|
def validate_device_plane_manager_reconciled_baseline(
|
|
backup_dir,
|
|
*,
|
|
marker_installed,
|
|
):
|
|
root = component_root("device-plane")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-manager-reconciled-baseline-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
backup_root = Path(directory)
|
|
materialize_backup_tree(
|
|
backup_dir / "source-before.tgz",
|
|
backup_root,
|
|
set(DEVICE_PLANE_MANAGER_RECONCILIATION_EXISTING),
|
|
)
|
|
backup_source = collect_exact_files(
|
|
backup_root,
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_EXISTING,
|
|
"Device Manager pre-apply source",
|
|
)
|
|
live_source = collect_exact_files(
|
|
root,
|
|
DEVICE_PLANE_MANAGER_RECONCILIATION_EXISTING,
|
|
"Device Manager reconciled live source",
|
|
)
|
|
if live_source != backup_source:
|
|
die("Device Manager rollback source does not match backup")
|
|
for rel in (
|
|
*DEVICE_PLANE_MANAGER_RECONCILIATION_MISSING,
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
|
):
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Device Manager candidate-only source remains installed: "
|
|
f"{rel}"
|
|
)
|
|
|
|
marker = root / DEVICE_PLANE_MANAGER_RECONCILIATION_REL
|
|
if marker_installed:
|
|
descriptor = read_strict_json(
|
|
marker,
|
|
"installed Device Manager reconciliation descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_device_plane_manager_reconciliation_descriptor():
|
|
die("installed Device Manager reconciliation descriptor mismatch")
|
|
elif marker.exists() or marker.is_symlink():
|
|
die("Device Manager reconciliation descriptor already installed")
|
|
|
|
if device_plane_service_container_ids("device-manager"):
|
|
die("Device Manager candidate container remains installed")
|
|
runtime_before = read_strict_json(
|
|
backup_dir / "runtime-before.json",
|
|
"Device Manager pre-apply runtime inventory",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
runtime = validate_device_plane_b2_discovery_ingress_runtime(
|
|
runtime_before,
|
|
preserved_stateless_services=("device-gateway",),
|
|
)
|
|
core_ids = device_plane_service_container_ids("device-control-core")
|
|
if len(core_ids) != 1:
|
|
die("Device Manager reconciled Core service count mismatch")
|
|
core = inspect_device_plane_container(core_ids[0])
|
|
core_environment = container_environment(
|
|
core,
|
|
"Device Manager reconciled Control Core",
|
|
)
|
|
if (
|
|
"DEVICE_MANAGEMENT_API_ENABLED" in core_environment
|
|
or "DEVICE_MANAGEMENT_CORE_TOKEN_FILE" in core_environment
|
|
or any(
|
|
mount.get("Destination")
|
|
== "/run/nodedc-secrets/management-core-token"
|
|
for mount in core.get("Mounts") or []
|
|
)
|
|
):
|
|
die("Device Manager management boundary remains active after rollback")
|
|
return runtime
|
|
|
|
|
|
def validate_device_plane_manager_reconciliation_evidence(payload_dir):
|
|
descriptor = validate_device_plane_manager_reconciliation_payload(
|
|
payload_dir
|
|
)
|
|
backup_dir = validate_device_plane_manager_reconciliation_backup()
|
|
failed_artifact = FAILED_DIR / DEVICE_PLANE_MANAGER_FAILED_ARTIFACT
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Manager 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)
|
|
!= DEVICE_PLANE_MANAGER_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Manager failed artifact evidence mismatch")
|
|
|
|
records = [
|
|
value
|
|
for value in load_state(FAILED_STATE_FILE)
|
|
if value.get("id") == DEVICE_PLANE_MANAGER_FAILED_PATCH_ID
|
|
]
|
|
if len(records) != 1:
|
|
die("Device Manager failed journal evidence count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact") != DEVICE_PLANE_MANAGER_FAILED_ARTIFACT
|
|
or record.get("backup_id")
|
|
!= DEVICE_PLANE_MANAGER_RECONCILIATION_BACKUP_ID
|
|
or record.get("component") != "device-plane"
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_MANAGER_FAILED_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not True
|
|
or record.get("rollback_status") != "failed:DeployError"
|
|
or record.get("status") != "failed"
|
|
or record.get("message")
|
|
!= (
|
|
"container healthcheck failed for "
|
|
"500bc061b97d9007a33dd51d35ca1168eae7f6b019165b143d3bd908220656c0: "
|
|
"unhealthy"
|
|
)
|
|
):
|
|
die("Device Manager failed journal evidence mismatch")
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-manager-failed-artifact-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, _failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id") != DEVICE_PLANE_MANAGER_FAILED_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries)
|
|
!= DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES
|
|
):
|
|
die("Device Manager failed artifact contract mismatch")
|
|
|
|
runtime = validate_device_plane_manager_reconciled_baseline(
|
|
backup_dir,
|
|
marker_installed=False,
|
|
)
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"backup": backup_dir,
|
|
"failedArtifact": failed_artifact,
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def validate_device_plane_manager_v3_active_baseline(descriptor):
|
|
if descriptor.get("schemaVersion") not in (
|
|
"nodedc.device-plane.device-manager-release.v3",
|
|
"nodedc.device-plane.device-manager-release.v4",
|
|
"nodedc.device-plane.device-manager-release.v5",
|
|
"nodedc.device-plane.device-manager-release.v6",
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
):
|
|
return None
|
|
|
|
def successful_applied_artifact(reference, label):
|
|
patch_id = reference["patchId"]
|
|
artifact_sha256 = reference["artifactSha256"]
|
|
artifact_name = f"nodedc-device-plane-{patch_id}.tgz"
|
|
artifact = APPLIED_DIR / artifact_name
|
|
if (
|
|
not artifact.is_file()
|
|
or artifact.is_symlink()
|
|
or sha256_file(artifact) != artifact_sha256
|
|
):
|
|
die(f"Device Manager v3 {label} applied artifact mismatch")
|
|
records = [
|
|
row for row in load_state(STATE_FILE)
|
|
if row.get("id") == patch_id
|
|
and row.get("sha256") == artifact_sha256
|
|
]
|
|
if (
|
|
len(records) != 1
|
|
or records[0].get("status") != "ok"
|
|
or records[0].get("component") != "device-plane"
|
|
or records[0].get("artifact") != artifact_name
|
|
):
|
|
die(f"Device Manager v3 {label} applied journal mismatch")
|
|
backup_id = records[0].get("backup_id")
|
|
backup = BACKUPS_DIR / backup_id if isinstance(backup_id, str) else None
|
|
if (
|
|
backup is None
|
|
or safe_name(backup_id) != backup_id
|
|
or not backup.is_dir()
|
|
or backup.is_symlink()
|
|
):
|
|
die(f"Device Manager v3 {label} backup is unsafe")
|
|
return artifact
|
|
|
|
core_reference = descriptor["controlCorePredecessor"]
|
|
edge_reference = descriptor["edgeChannelPredecessor"]
|
|
core_artifact = successful_applied_artifact(
|
|
core_reference,
|
|
"Device Control Core predecessor",
|
|
)
|
|
edge_artifact = successful_applied_artifact(
|
|
edge_reference,
|
|
"Device Edge channel predecessor",
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-manager-v3-core-baseline-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
core_manifest, core_entries, core_payload = load_artifact(
|
|
core_artifact,
|
|
Path(directory),
|
|
)
|
|
core_entry_set = tuple(core_entries)
|
|
if (
|
|
core_manifest.get("id") != core_reference["patchId"]
|
|
or core_manifest.get("component") != "device-plane"
|
|
or core_manifest.get("type") != "app-overlay"
|
|
or core_entry_set not in (
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES,
|
|
)
|
|
):
|
|
die("Device Manager v3 Device Control Core predecessor type mismatch")
|
|
expected_core_descriptor = (
|
|
validate_device_plane_control_core_release_payload(
|
|
core_payload,
|
|
expected_release_id=core_reference["patchId"],
|
|
)
|
|
)
|
|
installed_core_descriptor_rel = {
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES:
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES:
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL,
|
|
}.get(core_entry_set, DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL)
|
|
installed_core_descriptor = read_strict_json(
|
|
DEVICE_PLANE_ROOT / installed_core_descriptor_rel,
|
|
"installed Device Manager v3 Device Control Core predecessor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_core_descriptor != expected_core_descriptor:
|
|
die("Device Manager v3 Device Control Core predecessor is not current")
|
|
expected_core_source = collect_exact_files(
|
|
core_payload,
|
|
core_entry_set,
|
|
"Device Manager v3 Device Control Core predecessor source",
|
|
)
|
|
actual_core_source = collect_exact_files(
|
|
DEVICE_PLANE_ROOT,
|
|
core_entry_set,
|
|
"installed Device Manager v3 Device Control Core predecessor source",
|
|
)
|
|
if actual_core_source != expected_core_source:
|
|
die("installed Device Manager v3 Device Control Core source drift detected")
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-manager-v3-edge-baseline-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
edge_manifest, edge_entries, edge_payload = load_artifact(
|
|
edge_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
edge_manifest.get("id") != edge_reference["patchId"]
|
|
or edge_manifest.get("component") != "device-plane"
|
|
or edge_manifest.get("type") != "app-overlay"
|
|
or tuple(edge_entries)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES
|
|
):
|
|
die("Device Manager v3 Device Edge channel predecessor type mismatch")
|
|
expected_edge_descriptor = (
|
|
validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
|
edge_payload,
|
|
expected_transition_id=edge_reference["patchId"],
|
|
)
|
|
)
|
|
installed_edge_descriptor = read_strict_json(
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
|
"installed Device Manager v3 Device Edge channel predecessor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_edge_descriptor != expected_edge_descriptor:
|
|
die("Device Manager v3 Device Edge channel predecessor is not current")
|
|
expected_base_compose_sha256 = sha256_file(
|
|
edge_payload / "docker-compose.device-plane.yml"
|
|
)
|
|
installed_base_compose = DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml"
|
|
if (
|
|
installed_base_compose.is_symlink()
|
|
or not installed_base_compose.is_file()
|
|
or sha256_file(installed_base_compose) != expected_base_compose_sha256
|
|
):
|
|
die("installed Device Manager v3 Device Edge topology drift detected")
|
|
|
|
identity_state = inspect_device_edge_channel_core_identity_state()
|
|
if identity_state != "valid-reuse-at-apply":
|
|
die("Device Manager v3 requires the valid active Edge identity")
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
return {
|
|
"controlCoreArtifact": core_artifact,
|
|
"edgeChannelArtifact": edge_artifact,
|
|
"identityState": identity_state,
|
|
}
|
|
|
|
|
|
def device_plane_manager_preserved_runtime_health_services(descriptor):
|
|
if descriptor.get("schemaVersion") in (
|
|
"nodedc.device-plane.device-manager-release.v3",
|
|
"nodedc.device-plane.device-manager-release.v4",
|
|
"nodedc.device-plane.device-manager-release.v5",
|
|
"nodedc.device-plane.device-manager-release.v6",
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
):
|
|
return (
|
|
"device-control-core",
|
|
"device-postgres",
|
|
)
|
|
return ("device-postgres",)
|
|
|
|
|
|
def validate_device_plane_manager_preserved_runtime_health(descriptor):
|
|
services = device_plane_manager_preserved_runtime_health_services(
|
|
descriptor
|
|
)
|
|
for service in services:
|
|
healthcheck_compose_service("device-plane", service)
|
|
return services
|
|
|
|
|
|
def validate_device_plane_manager_activation_predecessor(
|
|
payload_dir,
|
|
*,
|
|
preflight_phase,
|
|
):
|
|
if preflight_phase not in ("plan", "apply"):
|
|
die("Device Manager predecessor preflight phase is invalid")
|
|
descriptor = validate_device_plane_manager_release_payload(
|
|
payload_dir
|
|
)
|
|
predecessor = descriptor["predecessor"]
|
|
patch_id = predecessor["patchId"]
|
|
sha256 = predecessor["artifactSha256"]
|
|
artifact_name = f"nodedc-device-plane-{patch_id}.tgz"
|
|
artifact = APPLIED_DIR / artifact_name
|
|
try:
|
|
artifact_stat = artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Manager predecessor applied artifact is missing")
|
|
if (
|
|
stat.S_ISLNK(artifact_stat.st_mode)
|
|
or not stat.S_ISREG(artifact_stat.st_mode)
|
|
or sha256_file(artifact) != sha256
|
|
):
|
|
die("Device Manager predecessor applied artifact mismatch")
|
|
|
|
id_records = [
|
|
value
|
|
for value in load_state(STATE_FILE)
|
|
if value.get("id") == patch_id
|
|
]
|
|
sha_records = [
|
|
value
|
|
for value in load_state(STATE_FILE)
|
|
if value.get("sha256") == sha256
|
|
]
|
|
if (
|
|
len(id_records) != 1
|
|
or len(sha_records) != 1
|
|
or id_records[0] != sha_records[0]
|
|
):
|
|
die("Device Manager predecessor applied journal identity mismatch")
|
|
record = id_records[0]
|
|
backup_id = record.get("backup_id")
|
|
if (
|
|
record.get("artifact") != artifact_name
|
|
or record.get("component") != "device-plane"
|
|
or record.get("id") != patch_id
|
|
or record.get("sha256") != sha256
|
|
or record.get("status") != "ok"
|
|
or not isinstance(backup_id, str)
|
|
or not backup_id
|
|
or safe_name(backup_id) != backup_id
|
|
):
|
|
die("Device Manager predecessor applied journal mismatch")
|
|
backup_dir = BACKUPS_DIR / backup_id
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Manager predecessor backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Manager predecessor backup is unsafe")
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-manager-applied-predecessor-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
predecessor_manifest, predecessor_entries, predecessor_payload = (
|
|
load_artifact(artifact, Path(directory))
|
|
)
|
|
if (
|
|
predecessor_manifest.get("id") != patch_id
|
|
or predecessor_manifest.get("component") != "device-plane"
|
|
or predecessor_manifest.get("type") != "app-overlay"
|
|
):
|
|
die("Device Manager predecessor artifact manifest mismatch")
|
|
root = component_root("device-plane")
|
|
if predecessor["kind"] == "reconciliation":
|
|
if (
|
|
descriptor["action"] != "activate"
|
|
or tuple(predecessor_entries)
|
|
!= DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ENTRIES
|
|
):
|
|
die("Device Manager reconciliation predecessor type mismatch")
|
|
predecessor_descriptor = (
|
|
validate_device_plane_manager_v2_reconciliation_payload(
|
|
predecessor_payload
|
|
)
|
|
)
|
|
installed_descriptor = read_strict_json(
|
|
root / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
|
|
"installed Device Manager reconciliation predecessor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_descriptor != predecessor_descriptor:
|
|
die("Device Manager reconciliation predecessor is not current")
|
|
baseline_backup = (
|
|
validate_device_plane_manager_v2_reconciliation_backup()
|
|
)
|
|
runtime = validate_device_plane_manager_v2_reconciled_baseline(
|
|
baseline_backup,
|
|
marker_installed=True,
|
|
)
|
|
mode = "reconciled-manager-forward-activation"
|
|
else:
|
|
if (
|
|
descriptor["action"] != "upgrade"
|
|
or tuple(predecessor_entries) not in (
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_ENTRIES,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_ENTRIES,
|
|
)
|
|
):
|
|
die("Device Manager release predecessor type mismatch")
|
|
predecessor_descriptor = (
|
|
validate_device_plane_manager_release_payload(
|
|
predecessor_payload,
|
|
expected_release_id=patch_id,
|
|
)
|
|
)
|
|
predecessor_descriptor_rel = {
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V7_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V8_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V9_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V10_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V11_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V12_REL,
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_ENTRIES:
|
|
DEVICE_PLANE_MANAGER_RELEASE_V13_REL,
|
|
}.get(
|
|
tuple(predecessor_entries),
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
|
)
|
|
installed_descriptor = read_strict_json(
|
|
root / predecessor_descriptor_rel,
|
|
"installed Device Manager release predecessor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_descriptor != predecessor_descriptor:
|
|
die("Device Manager release predecessor is not current")
|
|
runtime = {
|
|
"accepted": True,
|
|
"preservedRuntimeHealth": "deferred-to-apply",
|
|
}
|
|
if preflight_phase == "apply":
|
|
runtime["preservedRuntimeHealth"] = (
|
|
validate_device_plane_manager_preserved_runtime_health(
|
|
descriptor
|
|
)
|
|
)
|
|
v3_baseline = validate_device_plane_manager_v3_active_baseline(
|
|
descriptor
|
|
)
|
|
if v3_baseline is not None:
|
|
runtime["v3Baseline"] = v3_baseline
|
|
mode = "active-manager-forward-upgrade"
|
|
return {
|
|
"mode": mode,
|
|
"descriptor": descriptor,
|
|
"predecessorArtifact": artifact,
|
|
"predecessorRecord": record,
|
|
"predecessorBackup": backup_dir,
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def inspect_device_edge_channel_core_identity_state():
|
|
private_key = DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE
|
|
certificate = DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE
|
|
private_exists = private_key.exists() or private_key.is_symlink()
|
|
certificate_exists = certificate.exists() or certificate.is_symlink()
|
|
if private_exists != certificate_exists:
|
|
die("Device Edge channel Core identity is incomplete")
|
|
if not private_exists:
|
|
return "absent-create-at-apply"
|
|
if device_edge_channel_invalid_identity_is_exact_recoverable():
|
|
return "failed-016-invalid-unexported-recover-at-apply"
|
|
validate_device_edge_channel_certificate_extensions(certificate)
|
|
run_openssl([
|
|
"verify", "-purpose", "sslclient", "-CAfile", str(certificate),
|
|
str(certificate),
|
|
], "Device Edge channel Core existing certificate purpose validation")
|
|
certificate_public = capture_openssl(
|
|
["x509", "-in", str(certificate), "-pubkey", "-noout"],
|
|
"Device Edge channel existing certificate public key",
|
|
)
|
|
private_public = capture_openssl(
|
|
["pkey", "-in", str(private_key), "-pubout"],
|
|
"Device Edge channel existing private key public derivation",
|
|
)
|
|
if certificate_public != private_public:
|
|
die("Device Edge channel Core certificate/private key mismatch")
|
|
return "valid-reuse-at-apply"
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_bootstrap_predecessor(
|
|
payload_dir,
|
|
):
|
|
if (
|
|
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL
|
|
).exists():
|
|
return validate_device_plane_edge_core_channel_upgrade_v4_predecessor(
|
|
payload_dir
|
|
)
|
|
if (
|
|
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL
|
|
).exists():
|
|
return validate_device_plane_edge_core_channel_upgrade_v2_predecessor(
|
|
payload_dir
|
|
)
|
|
if (
|
|
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL
|
|
).exists():
|
|
return validate_device_plane_edge_core_channel_upgrade_predecessor(
|
|
payload_dir
|
|
)
|
|
descriptor = validate_device_plane_edge_core_channel_bootstrap_payload(
|
|
payload_dir
|
|
)
|
|
manager = descriptor["managerPredecessor"]
|
|
manager_artifact_name = (
|
|
f"nodedc-device-plane-{manager['patchId']}.tgz"
|
|
)
|
|
manager_artifact = APPLIED_DIR / manager_artifact_name
|
|
if (
|
|
not manager_artifact.is_file()
|
|
or manager_artifact.is_symlink()
|
|
or sha256_file(manager_artifact) != manager["artifactSha256"]
|
|
):
|
|
die("Device Edge Core channel manager predecessor mismatch")
|
|
manager_records = [
|
|
row for row in load_state(STATE_FILE)
|
|
if row.get("id") == manager["patchId"]
|
|
and row.get("sha256") == manager["artifactSha256"]
|
|
]
|
|
if (
|
|
len(manager_records) != 1
|
|
or manager_records[0].get("status") != "ok"
|
|
or manager_records[0].get("component") != "device-plane"
|
|
or manager_records[0].get("artifact") != manager_artifact_name
|
|
):
|
|
die("Device Edge Core channel manager journal mismatch")
|
|
installed_manager = read_strict_json(
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
|
"installed Device Manager predecessor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-edge-manager-predecessor-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
predecessor_manifest, predecessor_entries, predecessor_payload = (
|
|
load_artifact(manager_artifact, Path(directory))
|
|
)
|
|
if (
|
|
predecessor_manifest.get("id") != manager["patchId"]
|
|
or tuple(predecessor_entries)
|
|
!= DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
|
|
):
|
|
die("Device Edge Core channel manager artifact type mismatch")
|
|
expected_manager = validate_device_plane_manager_control_plane_payload(
|
|
predecessor_payload,
|
|
expected_release_id=manager["patchId"],
|
|
)
|
|
if installed_manager != expected_manager:
|
|
die("Device Edge Core channel manager predecessor is not current")
|
|
if (
|
|
sha256_file(DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_COMPOSE_REL)
|
|
!= DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256
|
|
):
|
|
die("Device Edge Core channel stable Manager Compose drift detected")
|
|
|
|
failed = descriptor["failedPredecessor"]
|
|
failed_artifact = FAILED_DIR / DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_ARTIFACT
|
|
failed_backup = BACKUPS_DIR / failed["backupId"]
|
|
if (
|
|
not failed_artifact.is_file()
|
|
or failed_artifact.is_symlink()
|
|
or sha256_file(failed_artifact) != failed["artifactSha256"]
|
|
or not failed_backup.is_dir()
|
|
or failed_backup.is_symlink()
|
|
):
|
|
die("Device Edge Core channel failed predecessor evidence mismatch")
|
|
failed_records = [
|
|
row for row in load_state(FAILED_STATE_FILE)
|
|
if row.get("id") == failed["patchId"]
|
|
and row.get("sha256") == failed["artifactSha256"]
|
|
]
|
|
if (
|
|
len(failed_records) != 1
|
|
or failed_records[0].get("status") != "failed"
|
|
or failed_records[0].get("component") != "device-plane"
|
|
or failed_records[0].get("backup_id") != failed["backupId"]
|
|
or failed_records[0].get("started_apply") is not True
|
|
):
|
|
die("Device Edge Core channel failed journal mismatch")
|
|
installed_bootstrap = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL
|
|
)
|
|
installed_override = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL
|
|
)
|
|
if (
|
|
installed_bootstrap.exists()
|
|
or installed_bootstrap.is_symlink()
|
|
or installed_override.exists()
|
|
or installed_override.is_symlink()
|
|
):
|
|
die("Device Edge Core channel bootstrap is already installed")
|
|
identity_state = inspect_device_edge_channel_core_identity_state()
|
|
for service in (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service("device-plane", service)
|
|
return {
|
|
"mode": "separate-edge-core-channel-bootstrap",
|
|
"descriptor": descriptor,
|
|
"identityState": identity_state,
|
|
"managerArtifact": manager_artifact,
|
|
"failedArtifact": failed_artifact,
|
|
"failedBackup": failed_backup,
|
|
}
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_upgrade_predecessor(
|
|
payload_dir,
|
|
):
|
|
descriptor = validate_device_plane_edge_core_channel_upgrade_payload(
|
|
payload_dir
|
|
)
|
|
installed_upgrade = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL
|
|
)
|
|
if installed_upgrade.exists() or installed_upgrade.is_symlink():
|
|
die("Device Edge Core channel upgrade is already installed")
|
|
predecessor = descriptor["bootstrapPredecessor"]
|
|
artifact_name = f"nodedc-device-plane-{predecessor['patchId']}.tgz"
|
|
artifact = APPLIED_DIR / artifact_name
|
|
if (
|
|
not artifact.is_file()
|
|
or artifact.is_symlink()
|
|
or sha256_file(artifact) != predecessor["artifactSha256"]
|
|
):
|
|
die("Device Edge Core channel bootstrap predecessor mismatch")
|
|
records = [
|
|
row for row in load_state(STATE_FILE)
|
|
if row.get("id") == predecessor["patchId"]
|
|
and row.get("sha256") == predecessor["artifactSha256"]
|
|
]
|
|
if (
|
|
len(records) != 1
|
|
or records[0].get("status") != "ok"
|
|
or records[0].get("component") != "device-plane"
|
|
or records[0].get("artifact") != artifact_name
|
|
):
|
|
die("Device Edge Core channel bootstrap journal mismatch")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-edge-core-channel-upgrade-predecessor-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
manifest, entries, predecessor_payload = load_artifact(
|
|
artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
manifest.get("id") != predecessor["patchId"]
|
|
or manifest.get("component") != "device-plane"
|
|
or manifest.get("type") != "app-overlay"
|
|
or tuple(entries)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES
|
|
):
|
|
die("Device Edge Core channel bootstrap artifact type mismatch")
|
|
validate_device_plane_edge_core_channel_bootstrap_payload(
|
|
predecessor_payload,
|
|
expected_transition_id=predecessor["patchId"],
|
|
)
|
|
expected_source = collect_exact_files(
|
|
predecessor_payload,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES,
|
|
"Device Edge Core channel bootstrap predecessor source",
|
|
)
|
|
actual_source = collect_exact_files(
|
|
DEVICE_PLANE_ROOT,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES,
|
|
"installed Device Edge Core channel bootstrap source",
|
|
)
|
|
if actual_source != expected_source:
|
|
die("installed Device Edge Core channel bootstrap source drift detected")
|
|
identity_state = inspect_device_edge_channel_core_identity_state()
|
|
if identity_state != "valid-reuse-at-apply":
|
|
die("Device Edge Core channel upgrade requires the valid active identity")
|
|
for service in (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service("device-plane", service)
|
|
return {
|
|
"mode": "edge-core-channel-standard-https-443-upgrade",
|
|
"descriptor": descriptor,
|
|
"identityState": identity_state,
|
|
"bootstrapArtifact": artifact,
|
|
}
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_upgrade_v2_predecessor(
|
|
payload_dir,
|
|
):
|
|
descriptor = validate_device_plane_edge_core_channel_upgrade_v2_payload(
|
|
payload_dir
|
|
)
|
|
installed_upgrade = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL
|
|
)
|
|
if installed_upgrade.exists() or installed_upgrade.is_symlink():
|
|
die("Device Edge Core channel upgrade v2 is already installed")
|
|
predecessor = descriptor["upgradePredecessor"]
|
|
artifact_name = f"nodedc-device-plane-{predecessor['patchId']}.tgz"
|
|
artifact = APPLIED_DIR / artifact_name
|
|
if (
|
|
not artifact.is_file()
|
|
or artifact.is_symlink()
|
|
or sha256_file(artifact) != predecessor["artifactSha256"]
|
|
):
|
|
die("Device Edge Core channel upgrade v2 predecessor mismatch")
|
|
records = [
|
|
row for row in load_state(STATE_FILE)
|
|
if row.get("id") == predecessor["patchId"]
|
|
and row.get("sha256") == predecessor["artifactSha256"]
|
|
]
|
|
if (
|
|
len(records) != 1
|
|
or records[0].get("status") != "ok"
|
|
or records[0].get("component") != "device-plane"
|
|
or records[0].get("artifact") != artifact_name
|
|
):
|
|
die("Device Edge Core channel upgrade v2 predecessor journal mismatch")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-edge-core-channel-upgrade-v2-predecessor-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
manifest, entries, predecessor_payload = load_artifact(
|
|
artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
manifest.get("id") != predecessor["patchId"]
|
|
or manifest.get("component") != "device-plane"
|
|
or manifest.get("type") != "app-overlay"
|
|
or tuple(entries)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES
|
|
):
|
|
die("Device Edge Core channel upgrade v2 predecessor type mismatch")
|
|
validate_device_plane_edge_core_channel_upgrade_payload(
|
|
predecessor_payload,
|
|
expected_transition_id=predecessor["patchId"],
|
|
)
|
|
expected_source = collect_exact_files(
|
|
predecessor_payload,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES,
|
|
"Device Edge Core channel upgrade v2 predecessor source",
|
|
)
|
|
actual_source = collect_exact_files(
|
|
DEVICE_PLANE_ROOT,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES,
|
|
"installed Device Edge Core channel upgrade v1 source",
|
|
)
|
|
if actual_source != expected_source:
|
|
die("installed Device Edge Core channel upgrade v1 source drift detected")
|
|
installed_bootstrap = read_strict_json(
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
|
|
"installed Device Edge Core channel bootstrap predecessor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_bootstrap != (
|
|
expected_device_plane_edge_core_channel_bootstrap_descriptor(
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID
|
|
)
|
|
):
|
|
die("installed Device Edge Core channel bootstrap predecessor drift detected")
|
|
identity_state = inspect_device_edge_channel_core_identity_state()
|
|
if identity_state != "valid-reuse-at-apply":
|
|
die("Device Edge Core channel upgrade v2 requires the valid active identity")
|
|
for service in (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service("device-plane", service)
|
|
return {
|
|
"mode": "edge-core-channel-forward-upgrade-v2",
|
|
"descriptor": descriptor,
|
|
"identityState": identity_state,
|
|
"upgradeArtifact": artifact,
|
|
}
|
|
|
|
|
|
def validate_device_plane_edge_core_channel_upgrade_v4_predecessor(
|
|
payload_dir,
|
|
):
|
|
descriptor = validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
|
payload_dir
|
|
)
|
|
installed_upgrade = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL
|
|
)
|
|
if installed_upgrade.exists() or installed_upgrade.is_symlink():
|
|
die("Device Edge Core channel upgrade v4 is already installed")
|
|
predecessor = descriptor["upgradePredecessor"]
|
|
artifact_name = f"nodedc-device-plane-{predecessor['patchId']}.tgz"
|
|
artifact = APPLIED_DIR / artifact_name
|
|
if (
|
|
not artifact.is_file()
|
|
or artifact.is_symlink()
|
|
or sha256_file(artifact) != predecessor["artifactSha256"]
|
|
):
|
|
die("Device Edge Core channel upgrade v4 predecessor mismatch")
|
|
records = [
|
|
row for row in load_state(STATE_FILE)
|
|
if row.get("id") == predecessor["patchId"]
|
|
and row.get("sha256") == predecessor["artifactSha256"]
|
|
]
|
|
if (
|
|
len(records) != 1
|
|
or records[0].get("status") != "ok"
|
|
or records[0].get("component") != "device-plane"
|
|
or records[0].get("artifact") != artifact_name
|
|
):
|
|
die("Device Edge Core channel upgrade v4 predecessor journal mismatch")
|
|
failed = descriptor["failedAttempt"]
|
|
failed_artifact = (
|
|
FAILED_DIR / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT
|
|
)
|
|
failed_backup = BACKUPS_DIR / failed["backupId"]
|
|
if (
|
|
not failed_artifact.is_file()
|
|
or failed_artifact.is_symlink()
|
|
or sha256_file(failed_artifact) != failed["artifactSha256"]
|
|
or not failed_backup.is_dir()
|
|
or failed_backup.is_symlink()
|
|
):
|
|
die("Device Edge Core channel upgrade v4 failed-attempt evidence mismatch")
|
|
failed_records = [
|
|
row for row in load_state(FAILED_STATE_FILE)
|
|
if row.get("id") == failed["patchId"]
|
|
and row.get("sha256") == failed["artifactSha256"]
|
|
]
|
|
if (
|
|
len(failed_records) != 1
|
|
or failed_records[0].get("status") != "failed"
|
|
or failed_records[0].get("component") != "device-plane"
|
|
or failed_records[0].get("backup_id") != failed["backupId"]
|
|
or failed_records[0].get("started_apply") is not True
|
|
or failed_records[0].get("rollback_status")
|
|
!= "ok:device-plane-overlay:source+runtime-restored:8"
|
|
or failed_records[0].get("message")
|
|
!= "Device Control Core Edge channel network boundary mismatch"
|
|
):
|
|
die("Device Edge Core channel upgrade v4 failed-attempt journal mismatch")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-edge-core-channel-upgrade-v4-predecessor-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
manifest, entries, predecessor_payload = load_artifact(
|
|
artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
manifest.get("id") != predecessor["patchId"]
|
|
or manifest.get("component") != "device-plane"
|
|
or manifest.get("type") != "app-overlay"
|
|
or tuple(entries)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES
|
|
):
|
|
die("Device Edge Core channel upgrade v4 predecessor type mismatch")
|
|
validate_device_plane_edge_core_channel_upgrade_v2_payload(
|
|
predecessor_payload,
|
|
expected_transition_id=predecessor["patchId"],
|
|
)
|
|
expected_source = collect_exact_files(
|
|
predecessor_payload,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
|
"Device Edge Core channel upgrade v4 predecessor source",
|
|
)
|
|
actual_source = collect_exact_files(
|
|
DEVICE_PLANE_ROOT,
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
|
"installed Device Edge Core channel upgrade v2 source",
|
|
)
|
|
if actual_source != expected_source:
|
|
die("installed Device Edge Core channel upgrade v2 source drift detected")
|
|
installed_base_compose = DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml"
|
|
if (
|
|
installed_base_compose.is_symlink()
|
|
or not installed_base_compose.is_file()
|
|
or sha256_file(installed_base_compose)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_BASE_COMPOSE_SHA256
|
|
):
|
|
die("installed Device Edge Core channel v4 base predecessor drift detected")
|
|
identity_state = inspect_device_edge_channel_core_identity_state()
|
|
if identity_state != "valid-reuse-at-apply":
|
|
die("Device Edge Core channel upgrade v4 requires the valid active identity")
|
|
for service in (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service("device-plane", service)
|
|
return {
|
|
"mode": "edge-core-channel-private-plus-egress-upgrade-v4",
|
|
"descriptor": descriptor,
|
|
"identityState": identity_state,
|
|
"upgradeArtifact": artifact,
|
|
"failedArtifact": failed_artifact,
|
|
"failedBackup": failed_backup,
|
|
}
|
|
|
|
|
|
def device_plane_control_core_preserved_runtime_health_services(descriptor):
|
|
services = tuple(descriptor.get("preservedServices") or ())
|
|
expected = (
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
)
|
|
if services != expected:
|
|
die("Device Control Core preserved service set mismatch")
|
|
return services
|
|
|
|
|
|
def validate_device_plane_control_core_preserved_runtime_health(descriptor):
|
|
services = device_plane_control_core_preserved_runtime_health_services(
|
|
descriptor
|
|
)
|
|
for service in services:
|
|
healthcheck_compose_service("device-plane", service)
|
|
return services
|
|
|
|
|
|
def validate_device_plane_control_core_release_v4_recovery_backup():
|
|
backup_dir = (
|
|
BACKUPS_DIR
|
|
/ DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_BACKUP_ID
|
|
)
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Control Core recovery 046 backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Control Core recovery 046 backup is unsafe")
|
|
expected = (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_BACKUP_SHA256
|
|
)
|
|
if {child.name for child in backup_dir.iterdir()} != set(expected):
|
|
die("Device Control Core recovery 046 backup file set mismatch")
|
|
for name, expected_sha256 in expected.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_sha256
|
|
):
|
|
die(
|
|
"Device Control Core recovery 046 backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
existing = tuple(
|
|
read_backup_path_list(backup_dir / "existing-files.txt")
|
|
)
|
|
missing = tuple(
|
|
read_backup_path_list(backup_dir / "missing-files.txt")
|
|
)
|
|
validate_backup_partition(
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ENTRIES,
|
|
existing,
|
|
missing,
|
|
"Device Control Core recovery 046",
|
|
)
|
|
if (
|
|
existing != (DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL,)
|
|
or missing
|
|
!= (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL,
|
|
)
|
|
):
|
|
die("Device Control Core recovery 046 backup partition mismatch")
|
|
return backup_dir
|
|
|
|
|
|
def validate_device_plane_control_core_release_v4_recovered_source(
|
|
recovery_payload,
|
|
):
|
|
base_artifact_name = (
|
|
"nodedc-device-plane-"
|
|
f"{DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_BASE_PATCH_ID}.tgz"
|
|
)
|
|
base_artifact = APPLIED_DIR / base_artifact_name
|
|
if (
|
|
not base_artifact.is_file()
|
|
or base_artifact.is_symlink()
|
|
or sha256_file(base_artifact)
|
|
!= DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_BASE_ARTIFACT_SHA256
|
|
):
|
|
die("Device Control Core release v4 recovered base artifact mismatch")
|
|
base_records = [
|
|
row for row in load_state(STATE_FILE)
|
|
if row.get("id") == DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_BASE_PATCH_ID
|
|
and row.get("sha256")
|
|
== DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_BASE_ARTIFACT_SHA256
|
|
]
|
|
if (
|
|
len(base_records) != 1
|
|
or base_records[0].get("status") != "ok"
|
|
or base_records[0].get("component") != "device-plane"
|
|
or base_records[0].get("artifact") != base_artifact_name
|
|
):
|
|
die("Device Control Core release v4 recovered base journal mismatch")
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-control-core-release-v4-base-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
base_manifest, base_entries, base_payload = load_artifact(
|
|
base_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
base_manifest.get("id")
|
|
!= DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_BASE_PATCH_ID
|
|
or base_manifest.get("component") != "device-plane"
|
|
or base_manifest.get("type") != "app-overlay"
|
|
or tuple(base_entries)
|
|
!= DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES
|
|
):
|
|
die("Device Control Core release v4 recovered base type mismatch")
|
|
validate_device_plane_control_core_release_payload(
|
|
base_payload,
|
|
expected_release_id=(
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_BASE_PATCH_ID
|
|
),
|
|
)
|
|
expected_source = collect_exact_files(
|
|
base_payload,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
|
"Device Control Core release v4 recovered base source",
|
|
)
|
|
recovery_source = collect_exact_files(
|
|
recovery_payload,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ENTRIES,
|
|
"Device Control Core release v4 recovery overlay",
|
|
)
|
|
expected_source.update(recovery_source)
|
|
live_entries = (
|
|
*DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL,
|
|
)
|
|
actual_source = collect_exact_files(
|
|
DEVICE_PLANE_ROOT,
|
|
live_entries,
|
|
"installed Device Control Core release v4 recovered source",
|
|
)
|
|
if actual_source != expected_source:
|
|
die("installed Device Control Core recovery 046 source drift detected")
|
|
for rel in (
|
|
"packages/infrastructure-telemetry-contract",
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_REL,
|
|
):
|
|
path = DEVICE_PLANE_ROOT / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Device Control Core recovery 046 candidate-only source "
|
|
f"appeared: {rel}"
|
|
)
|
|
installed_recovery = read_strict_json(
|
|
(
|
|
DEVICE_PLANE_ROOT
|
|
/ DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL
|
|
),
|
|
"installed Device Control Core recovery 046 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
installed_recovery
|
|
!= expected_device_plane_control_core_migration_replay_checkpoint_recovery_descriptor()
|
|
):
|
|
die("installed Device Control Core recovery 046 descriptor mismatch")
|
|
return base_artifact
|
|
|
|
|
|
def validate_device_plane_control_core_release_v4_recovered_runtime(
|
|
backup_dir,
|
|
):
|
|
before = read_strict_json(
|
|
backup_dir / "runtime-before.json",
|
|
"Device Control Core recovery 046 pre-apply runtime inventory",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
expected_services = {
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
}
|
|
if set(device_plane_inventory_service_names(before)) != expected_services:
|
|
die("Device Control Core recovery 046 runtime evidence mismatch")
|
|
current = device_plane_runtime_inventory(tuple(sorted(expected_services)))
|
|
if set(device_plane_inventory_service_names(current)) != expected_services:
|
|
die("Device Control Core recovery 046 live runtime is incomplete")
|
|
expected = {item["service"]: item for item in before["services"]}
|
|
observed = {item["service"]: item for item in current["services"]}
|
|
for service in expected_services - {"device-control-core"}:
|
|
item = observed[service]
|
|
if (
|
|
item["containerId"] != expected[service]["containerId"]
|
|
or item["imageId"] != expected[service]["imageId"]
|
|
or item["status"] != "running"
|
|
or item["running"] is not True
|
|
or item["health"] != "healthy"
|
|
or item["restartCount"] != expected[service]["restartCount"]
|
|
):
|
|
die(
|
|
"Device Control Core recovery 046 changed preserved service: "
|
|
f"{service}"
|
|
)
|
|
core = observed["device-control-core"]
|
|
local_image_id = inspect_optional_local_image(
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
"Device Control Core recovery 046 active local image",
|
|
)
|
|
if (
|
|
core["containerId"] == expected["device-control-core"]["containerId"]
|
|
or core["imageId"] == DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID
|
|
or core["imageId"] != local_image_id
|
|
or core["status"] != "running"
|
|
or core["running"] is not True
|
|
or core["health"] != "healthy"
|
|
or core["restartCount"] != 0
|
|
):
|
|
die("Device Control Core recovery 046 runtime mismatch")
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
return {"before": before, "current": current, "core": core}
|
|
|
|
|
|
def validate_device_plane_control_core_release_predecessor(
|
|
payload_dir,
|
|
*,
|
|
preflight_phase,
|
|
):
|
|
if preflight_phase not in ("plan", "apply"):
|
|
die("Device Control Core predecessor preflight phase is invalid")
|
|
descriptor = validate_device_plane_control_core_release_payload(
|
|
payload_dir
|
|
)
|
|
predecessor = descriptor["predecessor"]
|
|
artifact_name = f"nodedc-device-plane-{predecessor['patchId']}.tgz"
|
|
artifact = APPLIED_DIR / artifact_name
|
|
if (
|
|
not artifact.is_file()
|
|
or artifact.is_symlink()
|
|
or sha256_file(artifact) != predecessor["artifactSha256"]
|
|
):
|
|
die("Device Control Core release predecessor mismatch")
|
|
records = [
|
|
row for row in load_state(STATE_FILE)
|
|
if row.get("id") == predecessor["patchId"]
|
|
and row.get("sha256") == predecessor["artifactSha256"]
|
|
]
|
|
if (
|
|
len(records) != 1
|
|
or records[0].get("status") != "ok"
|
|
or records[0].get("component") != "device-plane"
|
|
or records[0].get("artifact") != artifact_name
|
|
):
|
|
die("Device Control Core release predecessor journal mismatch")
|
|
|
|
installed_v1_release = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_REL
|
|
)
|
|
installed_v2_release = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL
|
|
)
|
|
installed_v3_release = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL
|
|
)
|
|
installed_v4_release = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL
|
|
)
|
|
source_entries = None
|
|
expected_source = None
|
|
recovery_backup = None
|
|
recovery_runtime = None
|
|
recovery_database = None
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-control-core-release-predecessor-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
manifest, entries, predecessor_payload = load_artifact(
|
|
artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
manifest.get("id") != predecessor["patchId"]
|
|
or manifest.get("component") != "device-plane"
|
|
or manifest.get("type") != "app-overlay"
|
|
):
|
|
die("Device Control Core release predecessor type mismatch")
|
|
if predecessor["kind"] == "edge-core-channel-upgrade-v4":
|
|
if tuple(entries) != DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES:
|
|
die("Device Control Core release v4 predecessor type mismatch")
|
|
validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
|
predecessor_payload,
|
|
expected_transition_id=predecessor["patchId"],
|
|
)
|
|
source_entries = tuple(
|
|
rel for rel in DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES
|
|
if rel not in (
|
|
"docker-compose.device-plane.yml",
|
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
|
)
|
|
)
|
|
if (
|
|
installed_v1_release.exists()
|
|
or installed_v1_release.is_symlink()
|
|
or installed_v2_release.exists()
|
|
or installed_v2_release.is_symlink()
|
|
or installed_v3_release.exists()
|
|
or installed_v3_release.is_symlink()
|
|
or installed_v4_release.exists()
|
|
or installed_v4_release.is_symlink()
|
|
):
|
|
die("Device Control Core first release is already installed")
|
|
elif predecessor["kind"] == "migration-replay-checkpoint-recovery":
|
|
if (
|
|
tuple(entries)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ENTRIES
|
|
):
|
|
die(
|
|
"Device Control Core release v4 recovery predecessor "
|
|
"type mismatch"
|
|
)
|
|
validate_device_plane_control_core_migration_replay_checkpoint_recovery_payload(
|
|
predecessor_payload
|
|
)
|
|
validate_device_plane_control_core_release_v4_recovered_source(
|
|
predecessor_payload
|
|
)
|
|
else:
|
|
if tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES:
|
|
installed_release = installed_v1_release
|
|
source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES
|
|
elif tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES:
|
|
installed_release = installed_v2_release
|
|
source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES
|
|
elif tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES:
|
|
installed_release = installed_v3_release
|
|
source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES
|
|
elif tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES:
|
|
installed_release = installed_v4_release
|
|
source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES
|
|
else:
|
|
die("Device Control Core release predecessor type mismatch")
|
|
expected_installed = validate_device_plane_control_core_release_payload(
|
|
predecessor_payload,
|
|
expected_release_id=predecessor["patchId"],
|
|
)
|
|
installed_descriptor = read_strict_json(
|
|
installed_release,
|
|
"installed Device Control Core release predecessor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_descriptor != expected_installed:
|
|
die("Device Control Core release predecessor is not current")
|
|
if source_entries is not None:
|
|
expected_source = collect_exact_files(
|
|
predecessor_payload,
|
|
source_entries,
|
|
"Device Control Core release predecessor source",
|
|
)
|
|
if source_entries is not None:
|
|
actual_source = collect_exact_files(
|
|
DEVICE_PLANE_ROOT,
|
|
source_entries,
|
|
"installed Device Control Core release predecessor source",
|
|
)
|
|
if actual_source != expected_source:
|
|
die("installed Device Control Core release source drift detected")
|
|
|
|
component_compose_files("device-plane")
|
|
identity_state = inspect_device_edge_channel_core_identity_state()
|
|
if identity_state != "valid-reuse-at-apply":
|
|
die("Device Control Core release requires the valid active identity")
|
|
if predecessor["kind"] == "migration-replay-checkpoint-recovery":
|
|
recovery_backup = (
|
|
validate_device_plane_control_core_release_v4_recovery_backup()
|
|
)
|
|
recovery_runtime = (
|
|
validate_device_plane_control_core_release_v4_recovered_runtime(
|
|
recovery_backup
|
|
)
|
|
)
|
|
recovery_database = (
|
|
collect_device_plane_control_core_migration_replay_database_evidence(
|
|
expected_state="final",
|
|
)
|
|
)
|
|
selected_runtime = recovery_runtime["core"]
|
|
else:
|
|
selected_runtime = (
|
|
validate_device_plane_control_core_selected_predecessor_runtime()
|
|
)
|
|
preserved_runtime_health = "deferred-to-apply"
|
|
if preflight_phase == "apply":
|
|
preserved_runtime_health = (
|
|
validate_device_plane_control_core_preserved_runtime_health(
|
|
descriptor
|
|
)
|
|
)
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
return {
|
|
"mode": (
|
|
"recovery-046-host-telemetry-forward-release"
|
|
if predecessor["kind"]
|
|
== "migration-replay-checkpoint-recovery"
|
|
else "active-device-control-core-forward-release"
|
|
),
|
|
"descriptor": descriptor,
|
|
"identityState": identity_state,
|
|
"predecessorArtifact": artifact,
|
|
"selectedRuntime": selected_runtime,
|
|
"preservedRuntimeHealth": preserved_runtime_health,
|
|
"recoveryBackup": recovery_backup,
|
|
"recoveryRuntime": recovery_runtime,
|
|
"recoveryDatabase": recovery_database,
|
|
}
|
|
|
|
|
|
def validate_device_plane_manager_v2_reconciliation_backup():
|
|
backup_dir = (
|
|
BACKUPS_DIR / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID
|
|
)
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Manager v2 failed-apply backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Manager v2 failed-apply backup is unsafe")
|
|
expected = DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_SHA256
|
|
if {child.name for child in backup_dir.iterdir()} != set(expected):
|
|
die("Device Manager v2 failed-apply backup file set mismatch")
|
|
for name, expected_sha256 in expected.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_sha256
|
|
):
|
|
die(
|
|
"Device Manager v2 failed-apply backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
existing = tuple(read_backup_path_list(
|
|
backup_dir / "existing-files.txt"
|
|
))
|
|
missing = tuple(read_backup_path_list(
|
|
backup_dir / "missing-files.txt"
|
|
))
|
|
validate_backup_partition(
|
|
DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES,
|
|
existing,
|
|
missing,
|
|
"Device Manager v2 reconciliation",
|
|
)
|
|
if (
|
|
existing != DEVICE_PLANE_MANAGER_V2_RECONCILIATION_EXISTING
|
|
or missing != DEVICE_PLANE_MANAGER_V2_RECONCILIATION_MISSING
|
|
):
|
|
die("Device Manager v2 failed-apply backup partition mismatch")
|
|
return backup_dir
|
|
|
|
|
|
def validate_device_plane_manager_v2_reconciled_baseline(
|
|
backup_dir,
|
|
*,
|
|
marker_installed,
|
|
):
|
|
prior_backup = validate_device_plane_manager_reconciliation_backup()
|
|
runtime = validate_device_plane_manager_reconciled_baseline(
|
|
prior_backup,
|
|
marker_installed=True,
|
|
)
|
|
root = component_root("device-plane")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-manager-v2-reconciled-baseline-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
backup_root = Path(directory)
|
|
materialize_backup_tree(
|
|
backup_dir / "source-before.tgz",
|
|
backup_root,
|
|
set(DEVICE_PLANE_MANAGER_V2_RECONCILIATION_EXISTING),
|
|
)
|
|
backup_source = collect_exact_files(
|
|
backup_root,
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_EXISTING,
|
|
"Device Manager v2 pre-apply source",
|
|
)
|
|
live_source = collect_exact_files(
|
|
root,
|
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_EXISTING,
|
|
"Device Manager v2 reconciled live source",
|
|
)
|
|
if live_source != backup_source:
|
|
die("Device Manager v2 rollback source does not match backup")
|
|
for rel in (
|
|
*DEVICE_PLANE_MANAGER_V2_RECONCILIATION_MISSING,
|
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
|
):
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Device Manager v2 candidate-only source remains installed: "
|
|
f"{rel}"
|
|
)
|
|
|
|
marker = root / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL
|
|
if marker_installed:
|
|
descriptor = read_strict_json(
|
|
marker,
|
|
"installed Device Manager v2 reconciliation descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_manager_v2_reconciliation_descriptor()
|
|
):
|
|
die("installed Device Manager v2 reconciliation descriptor mismatch")
|
|
elif marker.exists() or marker.is_symlink():
|
|
die("Device Manager v2 reconciliation descriptor already installed")
|
|
return runtime
|
|
|
|
|
|
def validate_device_plane_manager_v2_reconciliation_evidence(payload_dir):
|
|
descriptor = validate_device_plane_manager_v2_reconciliation_payload(
|
|
payload_dir
|
|
)
|
|
backup_dir = validate_device_plane_manager_v2_reconciliation_backup()
|
|
failed_artifact = FAILED_DIR / DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Manager v2 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)
|
|
!= DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Manager v2 failed artifact evidence mismatch")
|
|
|
|
records = [
|
|
value
|
|
for value in load_state(FAILED_STATE_FILE)
|
|
if value.get("id") == DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID
|
|
]
|
|
if len(records) != 1:
|
|
die("Device Manager v2 failed journal evidence count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact") != DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT
|
|
or record.get("backup_id")
|
|
!= DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID
|
|
or record.get("component") != "device-plane"
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not True
|
|
or record.get("rollback_status") != "failed:DeployError"
|
|
or record.get("status") != "failed"
|
|
or record.get("message")
|
|
!= (
|
|
"container healthcheck grace exhausted for "
|
|
"f8593303db6b71468716ed2428e54aab18d7303b34adbe1a93cd6df86effa1cf: "
|
|
"unhealthy"
|
|
)
|
|
):
|
|
die("Device Manager v2 failed journal evidence mismatch")
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-manager-v2-failed-artifact-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, _failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id") != DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries)
|
|
!= DEVICE_PLANE_MANAGER_V2_CONTROL_PLANE_ENTRIES
|
|
):
|
|
die("Device Manager v2 failed artifact contract mismatch")
|
|
|
|
runtime = validate_device_plane_manager_v2_reconciled_baseline(
|
|
backup_dir,
|
|
marker_installed=False,
|
|
)
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"backup": backup_dir,
|
|
"failedArtifact": failed_artifact,
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def validate_device_plane_control_core_v3_reconciliation_backup():
|
|
backup_dir = (
|
|
BACKUPS_DIR / DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_BACKUP_ID
|
|
)
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Control Core v3 failed-apply backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Control Core v3 failed-apply backup is unsafe")
|
|
expected = DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_BACKUP_SHA256
|
|
if {child.name for child in backup_dir.iterdir()} != set(expected):
|
|
die("Device Control Core v3 failed-apply backup file set mismatch")
|
|
for name, expected_sha256 in expected.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_sha256
|
|
):
|
|
die(
|
|
"Device Control Core v3 failed-apply backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
existing = tuple(read_backup_path_list(
|
|
backup_dir / "existing-files.txt"
|
|
))
|
|
missing = tuple(read_backup_path_list(
|
|
backup_dir / "missing-files.txt"
|
|
))
|
|
validate_backup_partition(
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES,
|
|
existing,
|
|
missing,
|
|
"Device Control Core v3 reconciliation",
|
|
)
|
|
if (
|
|
existing != DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_EXISTING
|
|
or missing != DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_MISSING
|
|
):
|
|
die("Device Control Core v3 failed-apply backup partition mismatch")
|
|
return backup_dir
|
|
|
|
|
|
def validate_device_plane_control_core_v3_restored_source(
|
|
backup_dir,
|
|
*,
|
|
marker_installed,
|
|
):
|
|
root = component_root("device-plane")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-control-core-v3-restored-source-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
backup_root = Path(directory)
|
|
materialize_backup_tree(
|
|
backup_dir / "source-before.tgz",
|
|
backup_root,
|
|
set(DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_EXISTING),
|
|
)
|
|
backup_source = collect_exact_files(
|
|
backup_root,
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_EXISTING,
|
|
"Device Control Core v3 pre-apply source",
|
|
)
|
|
live_source = collect_exact_files(
|
|
root,
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_EXISTING,
|
|
"Device Control Core v3 restored live source",
|
|
)
|
|
if live_source != backup_source:
|
|
die("Device Control Core v3 rollback source does not match backup")
|
|
for rel in DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_MISSING:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Device Control Core v3 candidate-only source remains installed: "
|
|
f"{rel}"
|
|
)
|
|
marker = root / DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_REL
|
|
if marker_installed:
|
|
descriptor = read_strict_json(
|
|
marker,
|
|
"installed Device Control Core v3 reconciliation descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_control_core_v3_reconciliation_descriptor()
|
|
):
|
|
die("installed Device Control Core v3 reconciliation mismatch")
|
|
elif marker.exists() or marker.is_symlink():
|
|
die("Device Control Core v3 reconciliation already installed")
|
|
|
|
|
|
def validate_device_plane_control_core_v3_reconciliation_runtime(
|
|
backup_dir,
|
|
*,
|
|
require_recovered,
|
|
):
|
|
runtime_before = read_strict_json(
|
|
backup_dir / "runtime-before.json",
|
|
"Device Control Core v3 pre-apply runtime inventory",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
expected_names = {
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
}
|
|
if set(device_plane_inventory_service_names(runtime_before)) != expected_names:
|
|
die("Device Control Core v3 pre-apply runtime inventory mismatch")
|
|
before = {item["service"]: item for item in runtime_before["services"]}
|
|
if (
|
|
before["device-control-core"]["imageId"]
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID
|
|
or before["device-control-core"]["health"] != "healthy"
|
|
or before["device-control-core"]["running"] is not True
|
|
):
|
|
die("Device Control Core v3 pre-apply image evidence mismatch")
|
|
current = device_plane_runtime_inventory(tuple(sorted(expected_names)))
|
|
if set(device_plane_inventory_service_names(current)) != expected_names:
|
|
die("Device Control Core v3 current runtime inventory mismatch")
|
|
observed = {item["service"]: item for item in current["services"]}
|
|
for service in expected_names - {"device-control-core"}:
|
|
expected_item = before[service]
|
|
actual_item = observed[service]
|
|
if (
|
|
actual_item["containerId"] != expected_item["containerId"]
|
|
or actual_item["imageId"] != expected_item["imageId"]
|
|
or actual_item["running"] is not True
|
|
or actual_item["health"] != "healthy"
|
|
):
|
|
die(
|
|
"Device Control Core v3 reconciliation changed preserved service: "
|
|
f"{service}"
|
|
)
|
|
image_id = inspect_optional_local_image(
|
|
DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID,
|
|
"Device Control Core v3 exact pre-apply image",
|
|
)
|
|
if image_id != DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID:
|
|
die("Device Control Core v3 exact pre-apply image is unavailable")
|
|
core = observed["device-control-core"]
|
|
if require_recovered:
|
|
if (
|
|
core["imageId"] != DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID
|
|
or core["running"] is not True
|
|
or core["health"] != "healthy"
|
|
):
|
|
die("Device Control Core v3 exact-image recovery did not converge")
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
return {
|
|
"before": runtime_before,
|
|
"current": current,
|
|
"core": core,
|
|
}
|
|
|
|
|
|
def validate_device_plane_control_core_v3_reconciliation_evidence(payload_dir):
|
|
descriptor = validate_device_plane_control_core_v3_reconciliation_payload(
|
|
payload_dir
|
|
)
|
|
backup_dir = validate_device_plane_control_core_v3_reconciliation_backup()
|
|
failed_artifact = FAILED_DIR / DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Control Core v3 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)
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Control Core v3 failed artifact evidence mismatch")
|
|
records = [
|
|
value
|
|
for value in load_state(FAILED_STATE_FILE)
|
|
if value.get("id") == DEVICE_PLANE_CONTROL_CORE_V3_FAILED_PATCH_ID
|
|
]
|
|
if len(records) != 1:
|
|
die("Device Control Core v3 failed journal evidence count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact") != DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT
|
|
or record.get("backup_id")
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_BACKUP_ID
|
|
or record.get("component") != "device-plane"
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not True
|
|
or record.get("rollback_status") != "failed:DeployError"
|
|
or record.get("status") != "failed"
|
|
or record.get("message")
|
|
!= (
|
|
"container healthcheck grace exhausted for "
|
|
"97513c9a1027ae84f5dad6d0bc4792a67a4b0a1f32665d6d498093f43ee7987b: "
|
|
"unhealthy"
|
|
)
|
|
):
|
|
die("Device Control Core v3 failed journal evidence mismatch")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-control-core-v3-failed-artifact-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, _failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id") != DEVICE_PLANE_CONTROL_CORE_V3_FAILED_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries) != DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_ENTRIES
|
|
):
|
|
die("Device Control Core v3 failed artifact contract mismatch")
|
|
validate_device_plane_control_core_v3_restored_source(
|
|
backup_dir,
|
|
marker_installed=False,
|
|
)
|
|
runtime = validate_device_plane_control_core_v3_reconciliation_runtime(
|
|
backup_dir,
|
|
require_recovered=False,
|
|
)
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"backup": backup_dir,
|
|
"failedArtifact": failed_artifact,
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def validate_device_plane_control_core_incident_audit_evidence(payload_dir):
|
|
descriptor = validate_device_plane_control_core_incident_audit_payload(
|
|
payload_dir
|
|
)
|
|
evidence = validate_device_plane_control_core_double_failure_evidence()
|
|
evidence["mode"] = descriptor["mode"]
|
|
return evidence
|
|
|
|
|
|
def validate_device_plane_control_core_double_failure_evidence():
|
|
first_backup = validate_device_plane_control_core_v3_reconciliation_backup()
|
|
first_failed_artifact = (
|
|
FAILED_DIR / DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT
|
|
)
|
|
try:
|
|
first_failed_stat = first_failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Control Core v3 failed artifact is missing")
|
|
if (
|
|
stat.S_ISLNK(first_failed_stat.st_mode)
|
|
or not stat.S_ISREG(first_failed_stat.st_mode)
|
|
or sha256_file(first_failed_artifact)
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Control Core v3 failed artifact evidence mismatch")
|
|
first_records = [
|
|
value
|
|
for value in load_state(FAILED_STATE_FILE)
|
|
if value.get("id") == DEVICE_PLANE_CONTROL_CORE_V3_FAILED_PATCH_ID
|
|
]
|
|
if len(first_records) != 1:
|
|
die("Device Control Core v3 failed journal evidence count mismatch")
|
|
first_record = first_records[0]
|
|
if (
|
|
first_record.get("artifact") != DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT
|
|
or first_record.get("backup_id")
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_BACKUP_ID
|
|
or first_record.get("component") != "device-plane"
|
|
or first_record.get("sha256")
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT_SHA256
|
|
or first_record.get("started_apply") is not True
|
|
or first_record.get("rollback_status") != "failed:DeployError"
|
|
or first_record.get("status") != "failed"
|
|
):
|
|
die("Device Control Core v3 failed journal evidence mismatch")
|
|
validate_device_plane_control_core_v3_restored_source(
|
|
first_backup,
|
|
marker_installed=False,
|
|
)
|
|
validate_device_plane_control_core_v3_reconciliation_runtime(
|
|
first_backup,
|
|
require_recovered=False,
|
|
)
|
|
backup_dir = (
|
|
BACKUPS_DIR
|
|
/ DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_BACKUP_ID
|
|
)
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Control Core reconciliation failed-apply backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Control Core reconciliation failed-apply backup is unsafe")
|
|
expected_backup = (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_BACKUP_SHA256
|
|
)
|
|
if {child.name for child in backup_dir.iterdir()} != set(expected_backup):
|
|
die("Device Control Core reconciliation backup file set mismatch")
|
|
for name, expected_sha256 in expected_backup.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_sha256
|
|
):
|
|
die(
|
|
"Device Control Core reconciliation backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
existing = read_backup_path_list(backup_dir / "existing-files.txt")
|
|
missing = read_backup_path_list(backup_dir / "missing-files.txt")
|
|
validate_backup_partition(
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_ENTRIES,
|
|
existing,
|
|
missing,
|
|
"Device Control Core incident audit",
|
|
)
|
|
if existing or tuple(missing) != (
|
|
DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_REL,
|
|
):
|
|
die("Device Control Core reconciliation backup partition mismatch")
|
|
|
|
failed_artifact = (
|
|
FAILED_DIR
|
|
/ DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_ARTIFACT
|
|
)
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Control Core reconciliation 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)
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Control Core reconciliation failed artifact mismatch")
|
|
records = [
|
|
value
|
|
for value in load_state(FAILED_STATE_FILE)
|
|
if value.get("id")
|
|
== DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_PATCH_ID
|
|
]
|
|
if len(records) != 1:
|
|
die("Device Control Core reconciliation failed journal count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact")
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_ARTIFACT
|
|
or record.get("backup_id")
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_BACKUP_ID
|
|
or record.get("component") != "device-plane"
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not True
|
|
or record.get("rollback_status") != "failed:DeployError"
|
|
or record.get("status") != "failed"
|
|
or record.get("message")
|
|
!= (
|
|
"container healthcheck grace exhausted for "
|
|
"8b4b6c6fa1073808f0fafc24450735fe7769a2f92c36ae58dec97d3fa8a30dc0: "
|
|
"starting"
|
|
)
|
|
):
|
|
die("Device Control Core reconciliation failed journal mismatch")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-control-core-incident-audit-artifact-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, _failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id")
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries)
|
|
!= DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_ENTRIES
|
|
):
|
|
die("Device Control Core reconciliation failed artifact contract mismatch")
|
|
current = device_plane_runtime_inventory((
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
))
|
|
return {
|
|
"mode": "double-rollback-failed-evidence",
|
|
"firstBackup": first_backup,
|
|
"secondBackup": backup_dir,
|
|
"runtime": current,
|
|
}
|
|
|
|
|
|
def validate_device_plane_control_core_migration_replay_recovery_failure():
|
|
failed_artifact = (
|
|
FAILED_DIR
|
|
/ DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_FAILED_ARTIFACT
|
|
)
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Control Core migration recovery 044 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)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256
|
|
):
|
|
die("Device Control Core migration recovery 044 failed artifact mismatch")
|
|
if any(
|
|
value.get("id")
|
|
== DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
for value in load_state(STATE_FILE)
|
|
):
|
|
die("Device Control Core migration recovery 044 applied journal conflict")
|
|
records = [
|
|
value
|
|
for value in load_state(FAILED_STATE_FILE)
|
|
if value.get("id")
|
|
== DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
]
|
|
if len(records) != 1:
|
|
die("Device Control Core migration recovery 044 failed journal count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact")
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_FAILED_ARTIFACT
|
|
or record.get("backup_id") is not None
|
|
or record.get("component") != "device-plane"
|
|
or record.get("failed_at")
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_FAILED_AT
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not False
|
|
or record.get("rollback_status") != "not-required"
|
|
or record.get("status") != "failed"
|
|
or record.get("message")
|
|
!= "Device Control Core migration recovery database invariant mismatch"
|
|
):
|
|
die("Device Control Core migration recovery 044 failed journal mismatch")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-control-core-migration-recovery-044-failed-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id")
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ENTRIES
|
|
):
|
|
die(
|
|
"Device Control Core migration recovery 044 failed artifact "
|
|
"contract mismatch"
|
|
)
|
|
validate_device_plane_control_core_migration_replay_recovery_payload(
|
|
failed_payload
|
|
)
|
|
return {"artifact": failed_artifact, "record": record}
|
|
|
|
|
|
def device_plane_control_core_migration_replay_database_invariants_match(
|
|
evidence,
|
|
):
|
|
return (
|
|
evidence["invalidCommandKindCount"] == 0
|
|
and evidence["triggeringReceiptCount"] >= 1
|
|
and evidence["constraintValidated"]
|
|
and evidence["constraintCoversFinalKinds"]
|
|
and evidence["constraintPhase"] == "final-016"
|
|
and evidence["hostTelemetryTableAbsent"]
|
|
)
|
|
|
|
|
|
def device_plane_control_core_migration_replay_predecessor_invariants_match(
|
|
evidence,
|
|
):
|
|
return (
|
|
evidence["invalidCommandKindCount"] == 0
|
|
and evidence["triggeringReceiptCount"] >= 1
|
|
and not evidence["constraintValidated"]
|
|
and not evidence["constraintCoversFinalKinds"]
|
|
and evidence["constraintMatchesReplay011Kinds"]
|
|
and evidence["hostTelemetryTableAbsent"]
|
|
)
|
|
|
|
|
|
def device_plane_control_core_migration_replay_checkpoint_invariants_match(
|
|
evidence,
|
|
):
|
|
return (
|
|
evidence["invalidCommandKindCount"] == 0
|
|
and evidence["triggeringReceiptCount"] >= 1
|
|
and evidence["constraintValidationState"] == "false"
|
|
and not evidence["constraintCoversFinalKinds"]
|
|
and evidence["constraintPhase"]
|
|
in DEVICE_PLANE_CONTROL_CORE_REPLAY_CHECKPOINT_PHASES
|
|
and evidence["hostTelemetryTableAbsent"]
|
|
)
|
|
|
|
|
|
def emit_device_plane_control_core_migration_replay_database_evidence(
|
|
evidence,
|
|
):
|
|
def boolean(value):
|
|
return "true" if value else "false"
|
|
|
|
print(
|
|
"device_control_core_invalid_command_kind_count="
|
|
f"{evidence['invalidCommandKindCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_triggering_receipt_count="
|
|
f"{evidence['triggeringReceiptCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_constraint_validated="
|
|
f"{boolean(evidence['constraintValidated'])}"
|
|
)
|
|
print(
|
|
"device_control_core_constraint_covers_final_kinds="
|
|
f"{boolean(evidence['constraintCoversFinalKinds'])}"
|
|
)
|
|
print(
|
|
"device_control_core_host_telemetry_table_absent="
|
|
f"{boolean(evidence['hostTelemetryTableAbsent'])}"
|
|
)
|
|
print(
|
|
"device_control_core_constraint_matches_replay_011_kinds="
|
|
f"{boolean(evidence['constraintMatchesReplay011Kinds'])}"
|
|
)
|
|
print(
|
|
"device_control_core_constraint_phase="
|
|
f"{evidence['constraintPhase']}"
|
|
)
|
|
print(
|
|
"device_control_core_constraint_matches_known_replay_checkpoint="
|
|
f"{boolean(evidence['constraintMatchesKnownReplayCheckpoint'])}"
|
|
)
|
|
print(
|
|
"device_control_core_constraint_matches_final_016_kinds="
|
|
f"{boolean(evidence['constraintMatchesFinalKinds'])}"
|
|
)
|
|
print(
|
|
"device_control_core_recovery_044_ready="
|
|
f"{boolean(evidence['recovery044Ready'])}"
|
|
)
|
|
print(
|
|
"device_control_core_checkpoint_recovery_ready="
|
|
f"{boolean(evidence['checkpointRecoveryReady'])}"
|
|
)
|
|
print(
|
|
"device_control_core_recovery_final_state_ready="
|
|
f"{boolean(evidence['finalStateReady'])}"
|
|
)
|
|
|
|
|
|
def collect_device_plane_control_core_migration_replay_database_evidence(
|
|
enforce_recovery_invariants=True,
|
|
expected_state="final",
|
|
):
|
|
if expected_state not in (
|
|
"final",
|
|
"replay-011-predecessor",
|
|
"replay-checkpoint-predecessor",
|
|
):
|
|
die("Device Control Core migration recovery database state is invalid")
|
|
postgres_ids = device_plane_service_container_ids("device-postgres")
|
|
if len(postgres_ids) != 1:
|
|
die("Device Control Core migration recovery PostgreSQL is missing")
|
|
allowed = ",".join(
|
|
"'" + value.replace("'", "''") + "'"
|
|
for value in DEVICE_PLANE_CONTROL_CORE_FINAL_COMMAND_KINDS
|
|
)
|
|
triggering = ",".join(
|
|
"'" + value.replace("'", "''") + "'"
|
|
for value in DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TRIGGER_KINDS
|
|
)
|
|
query = f"""
|
|
with constraint_state as (
|
|
select convalidated,
|
|
pg_get_constraintdef(c.oid) as definition,
|
|
(select bool_and(pg_get_constraintdef(c.oid) like '%' || kind || '%')
|
|
from unnest(array[{allowed}]::text[]) as kind) as covers_final
|
|
from pg_constraint c
|
|
where c.conrelid = 'public.device_management_command_receipts'::regclass
|
|
and c.conname = 'device_management_command_receipts_command_kind_check'
|
|
),
|
|
constraint_literals as (
|
|
select array_agg((matched.value)[1] order by (matched.value)[1]) as values
|
|
from constraint_state
|
|
cross join lateral regexp_matches(
|
|
constraint_state.definition,
|
|
'''([^'']+)''',
|
|
'g'
|
|
) as matched(value)
|
|
)
|
|
select (
|
|
select count(*)
|
|
from device_management_command_receipts
|
|
where not (command_kind = any(array[{allowed}]::text[]))
|
|
)::text,
|
|
(
|
|
select count(*)
|
|
from device_management_command_receipts
|
|
where command_kind = any(array[{triggering}]::text[])
|
|
)::text,
|
|
coalesce((select convalidated::text from constraint_state), 'missing'),
|
|
coalesce((select covers_final::text from constraint_state), 'false'),
|
|
(to_regclass('public.device_infrastructure_host_telemetry_samples')
|
|
is null)::text,
|
|
coalesce((select array_to_json(values)::text from constraint_literals),
|
|
'[]')
|
|
""".strip()
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"exec",
|
|
postgres_ids[0],
|
|
"psql",
|
|
"-X",
|
|
"-qAt",
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
"-U",
|
|
"device_plane",
|
|
"-d",
|
|
"device_plane",
|
|
"-F",
|
|
"\t",
|
|
"-c",
|
|
query,
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
values = result.stdout.strip().split("\t")
|
|
if (
|
|
result.returncode != 0
|
|
or result.stderr.strip()
|
|
or len(values) != 6
|
|
or not re.fullmatch(r"[0-9]+", values[0])
|
|
or not re.fullmatch(r"[0-9]+", values[1])
|
|
or values[2] not in ("true", "false", "missing")
|
|
or values[3] not in ("true", "false")
|
|
or values[4] not in ("true", "false")
|
|
):
|
|
die("Device Control Core migration recovery database read failed")
|
|
try:
|
|
constraint_literal_kinds = json.loads(values[5])
|
|
except (TypeError, ValueError):
|
|
die("Device Control Core migration recovery constraint read failed")
|
|
if (
|
|
not isinstance(constraint_literal_kinds, list)
|
|
or len(constraint_literal_kinds) > 128
|
|
or any(
|
|
not isinstance(value, str)
|
|
or not re.fullmatch(r"[a-z][a-z0-9._-]{1,127}", value)
|
|
for value in constraint_literal_kinds
|
|
)
|
|
or len(set(constraint_literal_kinds)) != len(constraint_literal_kinds)
|
|
):
|
|
die("Device Control Core migration recovery constraint read failed")
|
|
constraint_literal_kinds = tuple(sorted(constraint_literal_kinds))
|
|
constraint_phase_by_kinds = {
|
|
tuple(sorted(kinds)): phase
|
|
for phase, kinds in (
|
|
*DEVICE_PLANE_CONTROL_CORE_REPLAY_CHECKPOINTS,
|
|
("final-016", DEVICE_PLANE_CONTROL_CORE_FINAL_COMMAND_KINDS),
|
|
)
|
|
}
|
|
if values[2] == "missing":
|
|
constraint_phase = "missing"
|
|
else:
|
|
constraint_phase = constraint_phase_by_kinds.get(
|
|
constraint_literal_kinds,
|
|
"unknown",
|
|
)
|
|
evidence = {
|
|
"invalidCommandKindCount": int(values[0]),
|
|
"triggeringReceiptCount": int(values[1]),
|
|
"constraintValidationState": values[2],
|
|
"constraintValidated": values[2] == "true",
|
|
"constraintCoversFinalKinds": values[3] == "true",
|
|
"hostTelemetryTableAbsent": values[4] == "true",
|
|
"constraintLiteralKinds": constraint_literal_kinds,
|
|
"constraintPhase": constraint_phase,
|
|
"constraintMatchesReplay011Kinds": (
|
|
constraint_phase == "replay-011"
|
|
),
|
|
"constraintMatchesKnownReplayCheckpoint": (
|
|
constraint_phase
|
|
in DEVICE_PLANE_CONTROL_CORE_REPLAY_CHECKPOINT_PHASES
|
|
),
|
|
"constraintMatchesFinalKinds": constraint_phase == "final-016",
|
|
"query": query,
|
|
}
|
|
evidence["finalStateReady"] = (
|
|
device_plane_control_core_migration_replay_database_invariants_match(
|
|
evidence
|
|
)
|
|
)
|
|
evidence["recovery044Ready"] = (
|
|
device_plane_control_core_migration_replay_predecessor_invariants_match(
|
|
evidence
|
|
)
|
|
)
|
|
evidence["checkpointRecoveryReady"] = (
|
|
device_plane_control_core_migration_replay_checkpoint_invariants_match(
|
|
evidence
|
|
)
|
|
)
|
|
expected_ready = {
|
|
"final": evidence["finalStateReady"],
|
|
"replay-011-predecessor": evidence["recovery044Ready"],
|
|
"replay-checkpoint-predecessor": (
|
|
evidence["checkpointRecoveryReady"]
|
|
),
|
|
}[expected_state]
|
|
if enforce_recovery_invariants and not expected_ready:
|
|
emit_device_plane_control_core_migration_replay_database_evidence(
|
|
evidence
|
|
)
|
|
die("Device Control Core migration recovery database invariant mismatch")
|
|
return evidence
|
|
|
|
|
|
def collect_device_plane_control_core_host_telemetry_database_evidence():
|
|
postgres_ids = device_plane_service_container_ids("device-postgres")
|
|
if len(postgres_ids) != 1:
|
|
die("Device Control Core host telemetry PostgreSQL is missing")
|
|
query = """
|
|
select (
|
|
to_regclass(
|
|
'public.device_infrastructure_host_telemetry_samples'
|
|
) is not null
|
|
)::text,
|
|
(
|
|
select count(*)
|
|
from information_schema.columns
|
|
where table_schema = 'public'
|
|
and table_name =
|
|
'device_infrastructure_host_telemetry_samples'
|
|
)::text,
|
|
(
|
|
select count(*)
|
|
from pg_indexes
|
|
where schemaname = 'public'
|
|
and indexname in (
|
|
'device_host_telemetry_project_host_time_idx',
|
|
'device_host_telemetry_retention_idx'
|
|
)
|
|
)::text,
|
|
coalesce((
|
|
select bool_and(convalidated)::text
|
|
from pg_constraint
|
|
where conrelid =
|
|
'public.device_infrastructure_host_telemetry_samples'::regclass
|
|
), 'false'),
|
|
(
|
|
select count(*)
|
|
from device_infrastructure_host_telemetry_samples
|
|
)::text,
|
|
(
|
|
select count(*)
|
|
from device_infrastructure_host_telemetry_samples
|
|
where observed_at < now() - interval '7 days'
|
|
)::text
|
|
""".strip()
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"exec",
|
|
postgres_ids[0],
|
|
"psql",
|
|
"-X",
|
|
"-qAt",
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
"-U",
|
|
"device_plane",
|
|
"-d",
|
|
"device_plane",
|
|
"-F",
|
|
"\t",
|
|
"-c",
|
|
query,
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
values = result.stdout.strip().split("\t")
|
|
if (
|
|
result.returncode != 0
|
|
or result.stderr.strip()
|
|
or len(values) != 6
|
|
or values[0] not in ("true", "false")
|
|
or not all(re.fullmatch(r"[0-9]+", value) for value in values[1:3])
|
|
or values[3] not in ("true", "false")
|
|
or not all(re.fullmatch(r"[0-9]+", value) for value in values[4:6])
|
|
):
|
|
die("Device Control Core host telemetry database read failed")
|
|
evidence = {
|
|
"tablePresent": values[0] == "true",
|
|
"columnCount": int(values[1]),
|
|
"indexCount": int(values[2]),
|
|
"constraintsValidated": values[3] == "true",
|
|
"sampleCount": int(values[4]),
|
|
"expiredSampleCount": int(values[5]),
|
|
"query": query,
|
|
}
|
|
if (
|
|
not evidence["tablePresent"]
|
|
or evidence["columnCount"] != 19
|
|
or evidence["indexCount"] != 2
|
|
or not evidence["constraintsValidated"]
|
|
or evidence["expiredSampleCount"] != 0
|
|
):
|
|
die("Device Control Core host telemetry database invariant mismatch")
|
|
return evidence
|
|
|
|
|
|
def accept_device_plane_control_core_release_v4(
|
|
payload_dir,
|
|
runtime_before,
|
|
):
|
|
descriptor = validate_device_plane_control_core_release_payload(
|
|
payload_dir,
|
|
expected_release_id=DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_PATCH_ID,
|
|
)
|
|
if (
|
|
descriptor.get("schemaVersion")
|
|
!= "nodedc.device-plane.device-control-core-release.v4"
|
|
):
|
|
die("Device Control Core release v4 acceptance descriptor mismatch")
|
|
expected_source = collect_exact_files(
|
|
payload_dir,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES,
|
|
"Device Control Core release v4 candidate source",
|
|
)
|
|
actual_source = collect_exact_files(
|
|
DEVICE_PLANE_ROOT,
|
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES,
|
|
"installed Device Control Core release v4 source",
|
|
)
|
|
if actual_source != expected_source:
|
|
die("installed Device Control Core release v4 source mismatch")
|
|
installed_descriptor = read_strict_json(
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL,
|
|
"installed Device Control Core release v4 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_descriptor != descriptor:
|
|
die("installed Device Control Core release v4 descriptor mismatch")
|
|
installed_recovery = read_strict_json(
|
|
(
|
|
DEVICE_PLANE_ROOT
|
|
/ DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL
|
|
),
|
|
"preserved Device Control Core recovery 046 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
installed_recovery
|
|
!= expected_device_plane_control_core_migration_replay_checkpoint_recovery_descriptor()
|
|
):
|
|
die("Device Control Core release v4 lost recovery 046 evidence")
|
|
legacy_v3 = DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_V3_REL
|
|
if legacy_v3.exists() or legacy_v3.is_symlink():
|
|
die("failed Device Control Core release v3 descriptor appeared")
|
|
|
|
expected_services = {
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
}
|
|
if set(device_plane_inventory_service_names(runtime_before)) != expected_services:
|
|
die("Device Control Core release v4 predecessor inventory mismatch")
|
|
current = device_plane_runtime_inventory(tuple(sorted(expected_services)))
|
|
if set(device_plane_inventory_service_names(current)) != expected_services:
|
|
die("Device Control Core release v4 runtime is incomplete")
|
|
before = {item["service"]: item for item in runtime_before["services"]}
|
|
observed = {item["service"]: item for item in current["services"]}
|
|
for service in expected_services - {"device-control-core"}:
|
|
item = observed[service]
|
|
if (
|
|
item["containerId"] != before[service]["containerId"]
|
|
or item["imageId"] != before[service]["imageId"]
|
|
or item["status"] != "running"
|
|
or item["running"] is not True
|
|
or item["health"] != "healthy"
|
|
or item["restartCount"] != before[service]["restartCount"]
|
|
):
|
|
die(
|
|
"Device Control Core release v4 changed preserved service: "
|
|
f"{service}"
|
|
)
|
|
core = observed["device-control-core"]
|
|
local_image_id = inspect_local_image(
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
"Device Control Core release v4 local image",
|
|
)
|
|
if (
|
|
core["containerId"] == before["device-control-core"]["containerId"]
|
|
or core["imageId"] == before["device-control-core"]["imageId"]
|
|
or core["imageId"] != local_image_id
|
|
or core["status"] != "running"
|
|
or core["running"] is not True
|
|
or core["health"] != "healthy"
|
|
or core["restartCount"] != 0
|
|
):
|
|
die("Device Control Core release v4 did not converge")
|
|
|
|
database = (
|
|
collect_device_plane_control_core_migration_replay_database_evidence(
|
|
enforce_recovery_invariants=False,
|
|
)
|
|
)
|
|
if (
|
|
database["invalidCommandKindCount"] != 0
|
|
or database["triggeringReceiptCount"] < 1
|
|
or not database["constraintValidated"]
|
|
or not database["constraintCoversFinalKinds"]
|
|
or database["constraintPhase"] != "final-016"
|
|
or database["hostTelemetryTableAbsent"]
|
|
):
|
|
emit_device_plane_control_core_migration_replay_database_evidence(
|
|
database
|
|
)
|
|
die("Device Control Core release v4 database invariant mismatch")
|
|
telemetry = (
|
|
collect_device_plane_control_core_host_telemetry_database_evidence()
|
|
)
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
assert_loopback_tcp_port_open(9921)
|
|
return {
|
|
"descriptor": descriptor,
|
|
"runtime": current,
|
|
"core": core,
|
|
"database": database,
|
|
"telemetry": telemetry,
|
|
}
|
|
|
|
|
|
def validate_device_plane_control_core_migration_replay_audit_evidence(
|
|
payload_dir,
|
|
):
|
|
descriptor = (
|
|
validate_device_plane_control_core_migration_replay_audit_payload(
|
|
payload_dir
|
|
)
|
|
)
|
|
failure_evidence = validate_device_plane_control_core_double_failure_evidence()
|
|
recovery_artifact = (
|
|
INBOX
|
|
/ DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT
|
|
)
|
|
try:
|
|
recovery_stat = recovery_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Control Core migration recovery staged artifact is missing")
|
|
if (
|
|
stat.S_ISLNK(recovery_stat.st_mode)
|
|
or not stat.S_ISREG(recovery_stat.st_mode)
|
|
or sha256_file(recovery_artifact)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256
|
|
):
|
|
die("Device Control Core migration recovery staged artifact mismatch")
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-control-core-migration-replay-audit-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
recovery_manifest, recovery_entries, recovery_payload = load_artifact(
|
|
recovery_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
recovery_manifest.get("id")
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
or recovery_manifest.get("component") != "device-plane"
|
|
or recovery_manifest.get("type") != "app-overlay"
|
|
or tuple(recovery_entries)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ENTRIES
|
|
):
|
|
die("Device Control Core migration recovery artifact contract mismatch")
|
|
validate_device_plane_control_core_migration_replay_recovery_payload(
|
|
recovery_payload
|
|
)
|
|
if any(
|
|
value.get("id")
|
|
== DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
for value in (
|
|
*load_state(STATE_FILE),
|
|
*load_state(FAILED_STATE_FILE),
|
|
)
|
|
):
|
|
die("Device Control Core migration recovery has terminal journal state")
|
|
|
|
root = component_root("device-plane")
|
|
live_migration = root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
|
|
if (
|
|
live_migration.is_symlink()
|
|
or not live_migration.is_file()
|
|
or sha256_file(live_migration)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256
|
|
):
|
|
die("Device Control Core migration replay audit source mismatch")
|
|
installed_descriptor = (
|
|
root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL
|
|
)
|
|
if installed_descriptor.exists() or installed_descriptor.is_symlink():
|
|
die("Device Control Core migration recovery is already installed")
|
|
|
|
runtime = (
|
|
validate_device_plane_control_core_migration_replay_preserved_runtime(
|
|
failure_evidence
|
|
)
|
|
)
|
|
core = runtime["core"]
|
|
if (
|
|
core["imageId"] != DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID
|
|
or core["status"] not in ("running", "restarting", "exited")
|
|
or core["health"] not in ("starting", "unhealthy", None)
|
|
):
|
|
die("Device Control Core migration replay audit runtime mismatch")
|
|
database = (
|
|
collect_device_plane_control_core_migration_replay_database_evidence(
|
|
enforce_recovery_invariants=False,
|
|
)
|
|
)
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"descriptor": descriptor,
|
|
"firstBackup": failure_evidence["firstBackup"],
|
|
"secondBackup": failure_evidence["secondBackup"],
|
|
"runtime": runtime["current"],
|
|
"core": core,
|
|
"database": database,
|
|
"recoveryArtifact": recovery_artifact,
|
|
}
|
|
|
|
|
|
def validate_device_plane_control_core_migration_replay_preserved_runtime(
|
|
evidence,
|
|
):
|
|
expected_services = {
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
}
|
|
current = device_plane_runtime_inventory(tuple(sorted(expected_services)))
|
|
if set(device_plane_inventory_service_names(current)) != expected_services:
|
|
die("Device Control Core migration recovery runtime is incomplete")
|
|
second_before = read_strict_json(
|
|
evidence["secondBackup"] / "runtime-before.json",
|
|
"Device Control Core second failed-apply runtime inventory",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
if set(device_plane_inventory_service_names(second_before)) != expected_services:
|
|
die("Device Control Core second failed-apply runtime evidence mismatch")
|
|
before = {item["service"]: item for item in second_before["services"]}
|
|
observed = {item["service"]: item for item in current["services"]}
|
|
for service in expected_services - {"device-control-core"}:
|
|
if (
|
|
observed[service]["containerId"] != before[service]["containerId"]
|
|
or observed[service]["imageId"] != before[service]["imageId"]
|
|
or observed[service]["status"] != "running"
|
|
or observed[service]["running"] is not True
|
|
or observed[service]["health"] != "healthy"
|
|
):
|
|
die(
|
|
"Device Control Core migration recovery changed preserved "
|
|
f"service: {service}"
|
|
)
|
|
return {"current": current, "core": observed["device-control-core"]}
|
|
|
|
|
|
def validate_device_plane_control_core_migration_replay_recovery_evidence(
|
|
payload_dir,
|
|
):
|
|
descriptor = (
|
|
validate_device_plane_control_core_migration_replay_recovery_payload(
|
|
payload_dir
|
|
)
|
|
)
|
|
evidence = validate_device_plane_control_core_double_failure_evidence()
|
|
root = component_root("device-plane")
|
|
live_migration = root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
|
|
if (
|
|
live_migration.is_symlink()
|
|
or not live_migration.is_file()
|
|
or sha256_file(live_migration)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256
|
|
):
|
|
die("Device Control Core migration 014 predecessor mismatch")
|
|
installed_descriptor = (
|
|
root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL
|
|
)
|
|
if installed_descriptor.exists() or installed_descriptor.is_symlink():
|
|
die("Device Control Core migration replay recovery already installed")
|
|
runtime = (
|
|
validate_device_plane_control_core_migration_replay_preserved_runtime(
|
|
evidence
|
|
)
|
|
)
|
|
core = runtime["core"]
|
|
if (
|
|
core["imageId"] != DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID
|
|
or core["status"] not in ("running", "restarting", "exited")
|
|
or core["health"] not in ("starting", "unhealthy", None)
|
|
):
|
|
die("Device Control Core migration recovery predecessor mismatch")
|
|
image_id = inspect_optional_local_image(
|
|
DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID,
|
|
"Device Control Core migration recovery exact predecessor image",
|
|
)
|
|
if image_id != DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID:
|
|
die("Device Control Core migration recovery predecessor image is missing")
|
|
database = (
|
|
collect_device_plane_control_core_migration_replay_database_evidence(
|
|
expected_state="replay-011-predecessor",
|
|
)
|
|
)
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"descriptor": descriptor,
|
|
"firstBackup": evidence["firstBackup"],
|
|
"secondBackup": evidence["secondBackup"],
|
|
"runtime": runtime["current"],
|
|
"core": core,
|
|
"database": database,
|
|
}
|
|
|
|
|
|
def validate_device_plane_control_core_migration_replay_checkpoint_recovery_evidence(
|
|
payload_dir,
|
|
):
|
|
descriptor = (
|
|
validate_device_plane_control_core_migration_replay_checkpoint_recovery_payload(
|
|
payload_dir
|
|
)
|
|
)
|
|
evidence = validate_device_plane_control_core_double_failure_evidence()
|
|
failed_recovery = (
|
|
validate_device_plane_control_core_migration_replay_recovery_failure()
|
|
)
|
|
root = component_root("device-plane")
|
|
live_migration = root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
|
|
if (
|
|
live_migration.is_symlink()
|
|
or not live_migration.is_file()
|
|
or sha256_file(live_migration)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256
|
|
):
|
|
die("Device Control Core migration 014 predecessor mismatch")
|
|
for descriptor_rel in (
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL,
|
|
DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL,
|
|
):
|
|
installed_descriptor = root / descriptor_rel
|
|
if installed_descriptor.exists() or installed_descriptor.is_symlink():
|
|
die(
|
|
"Device Control Core migration replay checkpoint recovery "
|
|
"already installed"
|
|
)
|
|
runtime = (
|
|
validate_device_plane_control_core_migration_replay_preserved_runtime(
|
|
evidence
|
|
)
|
|
)
|
|
core = runtime["core"]
|
|
if (
|
|
core["imageId"] != DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID
|
|
or core["status"] not in ("running", "restarting", "exited")
|
|
or core["health"] not in ("starting", "unhealthy", None)
|
|
):
|
|
die(
|
|
"Device Control Core migration replay checkpoint predecessor "
|
|
"mismatch"
|
|
)
|
|
image_id = inspect_optional_local_image(
|
|
DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID,
|
|
"Device Control Core migration replay checkpoint exact predecessor image",
|
|
)
|
|
if image_id != DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID:
|
|
die(
|
|
"Device Control Core migration replay checkpoint predecessor "
|
|
"image is missing"
|
|
)
|
|
database = (
|
|
collect_device_plane_control_core_migration_replay_database_evidence(
|
|
expected_state="replay-checkpoint-predecessor",
|
|
)
|
|
)
|
|
return {
|
|
"mode": descriptor["mode"],
|
|
"descriptor": descriptor,
|
|
"firstBackup": evidence["firstBackup"],
|
|
"secondBackup": evidence["secondBackup"],
|
|
"failedRecovery": failed_recovery,
|
|
"runtime": runtime["current"],
|
|
"core": core,
|
|
"database": database,
|
|
}
|
|
|
|
|
|
def accept_device_plane_control_core_migration_replay_recovery():
|
|
root = component_root("device-plane")
|
|
migration = root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
|
|
if (
|
|
migration.is_symlink()
|
|
or not migration.is_file()
|
|
or sha256_file(migration)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256
|
|
):
|
|
die("installed Device Control Core migration 014 repair mismatch")
|
|
descriptor = read_strict_json(
|
|
root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL,
|
|
"installed Device Control Core migration replay recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_control_core_migration_replay_recovery_descriptor()
|
|
):
|
|
die("installed Device Control Core migration recovery mismatch")
|
|
evidence = {
|
|
"secondBackup": (
|
|
BACKUPS_DIR
|
|
/ DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_BACKUP_ID
|
|
)
|
|
}
|
|
runtime = (
|
|
validate_device_plane_control_core_migration_replay_preserved_runtime(
|
|
evidence
|
|
)
|
|
)
|
|
core = runtime["core"]
|
|
if (
|
|
core["imageId"] == DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID
|
|
or core["status"] != "running"
|
|
or core["running"] is not True
|
|
or core["health"] != "healthy"
|
|
):
|
|
die("Device Control Core migration recovery did not converge")
|
|
database = (
|
|
collect_device_plane_control_core_migration_replay_database_evidence(
|
|
expected_state="final",
|
|
)
|
|
)
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
return {"runtime": runtime["current"], "database": database}
|
|
|
|
|
|
def accept_device_plane_control_core_migration_replay_checkpoint_recovery():
|
|
root = component_root("device-plane")
|
|
migration = root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
|
|
if (
|
|
migration.is_symlink()
|
|
or not migration.is_file()
|
|
or sha256_file(migration)
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256
|
|
):
|
|
die("installed Device Control Core migration 014 repair mismatch")
|
|
descriptor = read_strict_json(
|
|
root
|
|
/ DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL,
|
|
"installed Device Control Core replay checkpoint recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_control_core_migration_replay_checkpoint_recovery_descriptor()
|
|
):
|
|
die(
|
|
"installed Device Control Core replay checkpoint recovery mismatch"
|
|
)
|
|
legacy_descriptor = (
|
|
root / DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_REL
|
|
)
|
|
if legacy_descriptor.exists() or legacy_descriptor.is_symlink():
|
|
die("legacy Device Control Core migration recovery descriptor appeared")
|
|
evidence = {
|
|
"secondBackup": (
|
|
BACKUPS_DIR
|
|
/ DEVICE_PLANE_CONTROL_CORE_V3_RECONCILIATION_FAILED_BACKUP_ID
|
|
)
|
|
}
|
|
runtime = (
|
|
validate_device_plane_control_core_migration_replay_preserved_runtime(
|
|
evidence
|
|
)
|
|
)
|
|
core = runtime["core"]
|
|
if (
|
|
core["imageId"] == DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID
|
|
or core["status"] != "running"
|
|
or core["running"] is not True
|
|
or core["health"] != "healthy"
|
|
):
|
|
die(
|
|
"Device Control Core migration replay checkpoint recovery did "
|
|
"not converge"
|
|
)
|
|
database = (
|
|
collect_device_plane_control_core_migration_replay_database_evidence(
|
|
expected_state="final",
|
|
)
|
|
)
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
return {"runtime": runtime["current"], "database": database}
|
|
|
|
|
|
def emit_bounded_device_control_core_failure_logs():
|
|
core_ids = device_plane_service_container_ids("device-control-core")
|
|
if len(core_ids) != 1:
|
|
return
|
|
result = subprocess.run(
|
|
[str(DOCKER), "logs", "--tail", "160", core_ids[0]],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if result.returncode != 0:
|
|
return
|
|
selected = []
|
|
for raw_line in f"{result.stdout}\n{result.stderr}".splitlines():
|
|
line = re.sub(r"\s+", " ", raw_line.strip())
|
|
if not line or not re.search(
|
|
r"(?:error|exception|failed|constraint|migration|postgres|relation)",
|
|
line,
|
|
re.IGNORECASE,
|
|
):
|
|
continue
|
|
if re.search(
|
|
r"(?:authorization|bearer|password|private[_ -]?key|token=|secret=)",
|
|
line,
|
|
re.IGNORECASE,
|
|
):
|
|
line = "[sensitive log line redacted]"
|
|
if line not in selected:
|
|
selected.append(line[:480])
|
|
for index, line in enumerate(selected[-16:], start=1):
|
|
print(
|
|
f"device_control_core_failure_log_{index:02d}={line}",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
|
|
def collect_device_plane_control_core_incident_audit():
|
|
core_ids = device_plane_service_container_ids("device-control-core")
|
|
postgres_ids = device_plane_service_container_ids("device-postgres")
|
|
if len(core_ids) != 1 or len(postgres_ids) != 1:
|
|
die("Device Control Core incident audit container set mismatch")
|
|
core_id = core_ids[0]
|
|
logs = subprocess.run(
|
|
[str(DOCKER), "logs", "--tail", "240", core_id],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if logs.returncode != 0:
|
|
die("Device Control Core incident audit log read failed")
|
|
bounded_logs = f"{logs.stdout}\n{logs.stderr}"
|
|
if len(bounded_logs.encode("utf-8")) > 256 * 1024:
|
|
die("Device Control Core incident audit logs exceeded bound")
|
|
error_lines = []
|
|
for raw_line in bounded_logs.splitlines():
|
|
line = re.sub(r"\s+", " ", raw_line.strip())
|
|
if not line or not re.search(
|
|
r"(?:error|exception|failed|timeout|econn|enoent|eacces|"
|
|
r"postgres|relation|constraint|migration|lock|device_[a-z0-9_]+)",
|
|
line,
|
|
re.IGNORECASE,
|
|
):
|
|
continue
|
|
if re.search(
|
|
r"(?:authorization|bearer|password|private[_ -]?key|token=|secret=)",
|
|
line,
|
|
re.IGNORECASE,
|
|
):
|
|
line = "[sensitive log line redacted]"
|
|
line = line[:480]
|
|
if line not in error_lines:
|
|
error_lines.append(line)
|
|
error_lines = error_lines[-16:]
|
|
|
|
activity_query = """
|
|
select 'schema' as kind,
|
|
case when to_regclass('public.device_infrastructure_host_telemetry_samples')
|
|
is null then 'host-telemetry-table-absent'
|
|
else 'host-telemetry-table-present' end as detail
|
|
union all
|
|
select 'activity' as kind,
|
|
concat_ws(',', pid::text, state,
|
|
coalesce(wait_event_type, 'none'), coalesce(wait_event, 'none'),
|
|
case
|
|
when query ~* 'device_infrastructure_host_telemetry_samples'
|
|
then 'host-telemetry-ddl'
|
|
when query ~* '(create|alter|drop)[[:space:]]+(table|index|constraint)'
|
|
then 'schema-ddl'
|
|
when state = 'idle in transaction' then 'idle-transaction'
|
|
else 'application-query'
|
|
end,
|
|
greatest(0, floor(extract(epoch from (clock_timestamp() - query_start))))::bigint
|
|
) as detail
|
|
from pg_stat_activity
|
|
where datname = 'device_plane' and pid <> pg_backend_pid()
|
|
order by 1, 2
|
|
""".strip()
|
|
activity = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"exec",
|
|
postgres_ids[0],
|
|
"psql",
|
|
"-X",
|
|
"-qAt",
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
"-U",
|
|
"device_plane",
|
|
"-d",
|
|
"device_plane",
|
|
"-F",
|
|
"\t",
|
|
"-c",
|
|
activity_query,
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if activity.returncode != 0 or activity.stderr.strip():
|
|
die("Device Control Core incident audit database read failed")
|
|
database_evidence = []
|
|
for line in activity.stdout.splitlines():
|
|
normalized = line.strip()
|
|
if not re.fullmatch(r"(?:schema|activity)\t[A-Za-z0-9_, -]{1,240}", normalized):
|
|
die("Device Control Core incident audit database output invalid")
|
|
database_evidence.append(normalized)
|
|
if not database_evidence or not any(
|
|
item.startswith("schema\t") for item in database_evidence
|
|
):
|
|
die("Device Control Core incident audit database evidence missing")
|
|
return {
|
|
"coreContainerId": core_id,
|
|
"logSha256": hashlib.sha256(bounded_logs.encode("utf-8")).hexdigest(),
|
|
"logErrors": error_lines,
|
|
"database": database_evidence,
|
|
}
|
|
|
|
|
|
def device_plane_service_container_ids(service):
|
|
if service not in (
|
|
*DEVICE_PLANE_RUNTIME_SERVICES,
|
|
"device-manager",
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,
|
|
):
|
|
die(f"Device Plane runtime service is not registered: {service}")
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"container",
|
|
"ls",
|
|
"-a",
|
|
"--filter",
|
|
"label=com.docker.compose.project=nodedc-device-plane",
|
|
"--filter",
|
|
f"label=com.docker.compose.service={service}",
|
|
"--format",
|
|
"{{.ID}}",
|
|
],
|
|
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 any(
|
|
not re.fullmatch(r"[a-f0-9]{12,64}", container_id)
|
|
for container_id in container_ids
|
|
)
|
|
or len(container_ids) > 1
|
|
):
|
|
die(f"Device Plane runtime inventory failed: {service}")
|
|
return tuple(container_ids)
|
|
|
|
|
|
def inspect_device_plane_container(container_id):
|
|
containers = docker_json(
|
|
["container", "inspect", container_id],
|
|
"Device Plane container inspect",
|
|
)
|
|
if (
|
|
not isinstance(containers, list)
|
|
or len(containers) != 1
|
|
or not isinstance(containers[0], dict)
|
|
):
|
|
die("Device Plane container inspect shape mismatch")
|
|
return containers[0]
|
|
|
|
|
|
def device_plane_runtime_inventory(services):
|
|
inventory = []
|
|
for service in services:
|
|
container_ids = device_plane_service_container_ids(service)
|
|
if not container_ids:
|
|
continue
|
|
container = inspect_device_plane_container(container_ids[0])
|
|
state = container.get("State") or {}
|
|
health = (state.get("Health") or {}).get("Status")
|
|
inventory.append({
|
|
"service": service,
|
|
"containerId": container.get("Id"),
|
|
"imageId": container.get("Image"),
|
|
"status": state.get("Status"),
|
|
"running": state.get("Running") is True,
|
|
"health": health,
|
|
"restartCount": int(container.get("RestartCount") or 0),
|
|
})
|
|
return {
|
|
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
|
|
"composeProject": "nodedc-device-plane",
|
|
"services": inventory,
|
|
}
|
|
|
|
|
|
def device_plane_inventory_service_names(inventory):
|
|
if (
|
|
not isinstance(inventory, dict)
|
|
or inventory.get("schemaVersion")
|
|
!= "nodedc.device-plane.runtime-inventory.v1"
|
|
or inventory.get("composeProject") != "nodedc-device-plane"
|
|
or not isinstance(inventory.get("services"), list)
|
|
):
|
|
die("Device Plane runtime inventory shape mismatch")
|
|
names = []
|
|
for item in inventory["services"]:
|
|
if (
|
|
not isinstance(item, dict)
|
|
or set(item) != {
|
|
"service",
|
|
"containerId",
|
|
"imageId",
|
|
"status",
|
|
"running",
|
|
"health",
|
|
"restartCount",
|
|
}
|
|
or item.get("service") not in (
|
|
*DEVICE_PLANE_RUNTIME_SERVICES,
|
|
"device-manager",
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,
|
|
)
|
|
or not isinstance(item.get("containerId"), str)
|
|
or not re.fullmatch(r"[a-f0-9]{64}", item["containerId"])
|
|
or not isinstance(item.get("imageId"), str)
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", item["imageId"])
|
|
or not isinstance(item.get("status"), str)
|
|
or not isinstance(item.get("running"), bool)
|
|
or item.get("health") not in (
|
|
None,
|
|
"starting",
|
|
"healthy",
|
|
"unhealthy",
|
|
)
|
|
or not isinstance(item.get("restartCount"), int)
|
|
or item["restartCount"] < 0
|
|
):
|
|
die("Device Plane runtime inventory service mismatch")
|
|
service = item["service"]
|
|
if service in names:
|
|
die("Device Plane runtime inventory contains duplicates")
|
|
names.append(service)
|
|
return tuple(names)
|
|
|
|
|
|
def validate_device_plane_control_core_selected_predecessor_runtime():
|
|
inventory = device_plane_runtime_inventory(("device-control-core",))
|
|
names = device_plane_inventory_service_names(inventory)
|
|
if names != ("device-control-core",):
|
|
die("Device Control Core selected predecessor runtime is missing")
|
|
# This is the selected repair target, not a preserved dependency. Its
|
|
# observed Docker state is evidence for rollback only; health and state
|
|
# acceptance belongs to the new generation after recreate.
|
|
return inventory["services"][0]
|
|
|
|
|
|
def accept_device_plane_control_core_rollback_runtime(runtime_before):
|
|
expected_services = (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
)
|
|
names = device_plane_inventory_service_names(runtime_before)
|
|
if set(names) != set(expected_services):
|
|
die("Device Control Core rollback predecessor inventory mismatch")
|
|
before = {
|
|
item["service"]: item
|
|
for item in runtime_before["services"]
|
|
}
|
|
selected_before = before["device-control-core"]
|
|
predecessor_was_healthy = (
|
|
selected_before["status"] == "running"
|
|
and selected_before["running"] is True
|
|
and selected_before["health"] == "healthy"
|
|
)
|
|
|
|
for service in expected_services[1:]:
|
|
healthcheck_compose_service_with_grace("device-plane", service)
|
|
|
|
preserved = device_plane_runtime_inventory(expected_services[1:])
|
|
if set(device_plane_inventory_service_names(preserved)) != set(
|
|
expected_services[1:]
|
|
):
|
|
die("Device Control Core rollback preserved runtime is incomplete")
|
|
for item in preserved["services"]:
|
|
expected = before[item["service"]]
|
|
if (
|
|
item["containerId"] != expected["containerId"]
|
|
or item["imageId"] != expected["imageId"]
|
|
or item["status"] != "running"
|
|
or item["running"] is not True
|
|
or item["health"] != "healthy"
|
|
):
|
|
die(
|
|
"Device Control Core rollback changed preserved service: "
|
|
f"{item['service']}"
|
|
)
|
|
|
|
container_id = compose_service_container_id(
|
|
"device-plane",
|
|
"device-control-core",
|
|
)
|
|
if container_id == selected_before["containerId"]:
|
|
die("Device Control Core rollback generation was not recreated")
|
|
last_status = "unknown"
|
|
for attempt in range(1, 61):
|
|
container = inspect_device_plane_container(container_id)
|
|
state = container.get("State") or {}
|
|
health = (state.get("Health") or {}).get("Status")
|
|
last_status = health or state.get("Status") or "unknown"
|
|
restored_healthy = (
|
|
state.get("Status") == "running"
|
|
and state.get("Running") is True
|
|
and health == "healthy"
|
|
)
|
|
restored_repair_boundary = (
|
|
state.get("Status") in (
|
|
"created",
|
|
"running",
|
|
"paused",
|
|
"restarting",
|
|
"exited",
|
|
)
|
|
and isinstance(state.get("Running"), bool)
|
|
and health in (None, "starting", "healthy", "unhealthy")
|
|
)
|
|
if restored_healthy or (
|
|
not predecessor_was_healthy
|
|
and restored_repair_boundary
|
|
):
|
|
return {
|
|
"containerId": container_id,
|
|
"health": health,
|
|
"status": state.get("Status"),
|
|
"predecessorHealth": selected_before["health"],
|
|
"predecessorStatus": selected_before["status"],
|
|
}
|
|
if attempt < 60:
|
|
time.sleep(5)
|
|
die(
|
|
"Device Control Core rollback state did not converge: "
|
|
f"{last_status}"
|
|
)
|
|
|
|
|
|
def validate_device_plane_foundation_recovery_evidence(payload_dir):
|
|
backup_dir = BACKUPS_DIR / DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_ID
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane foundation recovery backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Plane foundation recovery backup is unsafe")
|
|
backup_names = {child.name for child in backup_dir.iterdir()}
|
|
if backup_names != set(DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_SHA256):
|
|
die("Device Plane foundation recovery backup file set mismatch")
|
|
for name, expected in (
|
|
DEVICE_PLANE_FOUNDATION_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(
|
|
"Device Plane foundation recovery backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
|
|
failed_artifact = FAILED_DIR / DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane foundation 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)
|
|
!= DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Plane foundation 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("Device Plane foundation failed journal is unreadable")
|
|
if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISREG(
|
|
state_stat.st_mode
|
|
):
|
|
die("Device Plane foundation failed journal is unsafe")
|
|
records = []
|
|
for line in state_lines:
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
die("Device Plane foundation failed journal contains invalid JSON")
|
|
if (
|
|
isinstance(value, dict)
|
|
and value.get("id") == DEVICE_PLANE_FOUNDATION_FAILED_PATCH_ID
|
|
):
|
|
records.append(value)
|
|
if len(records) != 1:
|
|
die("Device Plane foundation failed journal evidence count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact") != DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT
|
|
or record.get("backup_id")
|
|
!= DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_ID
|
|
or record.get("component") != "device-plane"
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not True
|
|
or record.get("rollback_status") != "failed:DeployError"
|
|
or record.get("status") != "failed"
|
|
or record.get("message")
|
|
!= (
|
|
"healthcheck failed for http://127.0.0.1:18120/healthz: "
|
|
"<urlopen error [Errno 111] Connection refused>"
|
|
)
|
|
):
|
|
die("Device Plane foundation failed journal evidence mismatch")
|
|
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-plane-failed-foundation-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id")
|
|
!= DEVICE_PLANE_FOUNDATION_FAILED_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries) != DEVICE_PLANE_FOUNDATION_ENTRIES
|
|
):
|
|
die("Device Plane foundation failed artifact contract mismatch")
|
|
failed_source = collect_exact_files(
|
|
failed_payload,
|
|
DEVICE_PLANE_FOUNDATION_ENTRIES,
|
|
"Device Plane failed foundation source",
|
|
)
|
|
candidate_source = collect_exact_files(
|
|
payload_dir,
|
|
DEVICE_PLANE_FOUNDATION_ENTRIES,
|
|
"Device Plane recovery candidate source",
|
|
)
|
|
if candidate_source != failed_source:
|
|
die(
|
|
"Device Plane recovery candidate does not match failed "
|
|
"foundation source"
|
|
)
|
|
|
|
root = component_root("device-plane")
|
|
compose_path = root / "docker-compose.device-plane.yml"
|
|
if (
|
|
compose_path.is_symlink()
|
|
or not compose_path.is_file()
|
|
or sha256_file(compose_path)
|
|
!= DEVICE_PLANE_FOUNDATION_PREDECESSOR_COMPOSE_SHA256
|
|
):
|
|
die("Device Plane foundation partial Compose predecessor mismatch")
|
|
for rel in DEVICE_PLANE_FOUNDATION_ENTRIES:
|
|
if rel == "docker-compose.device-plane.yml":
|
|
continue
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Device Plane foundation partial predecessor retained source: "
|
|
f"{rel}"
|
|
)
|
|
recovery_descriptor = root / DEVICE_PLANE_FOUNDATION_RECOVERY_REL
|
|
if recovery_descriptor.exists() or recovery_descriptor.is_symlink():
|
|
die("Device Plane foundation recovery descriptor already exists")
|
|
bootstrap_descriptor = (
|
|
root / DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL
|
|
)
|
|
if (
|
|
bootstrap_descriptor.is_symlink()
|
|
or not bootstrap_descriptor.is_file()
|
|
or sha256_file(bootstrap_descriptor)
|
|
!= DEVICE_PLANE_POSTGRES_BOOTSTRAP_DESCRIPTOR_SHA256
|
|
):
|
|
die("Device Plane PostgreSQL bootstrap descriptor drift detected")
|
|
|
|
runtime = validate_device_plane_foundation_runtime()
|
|
return {
|
|
"mode": "failed-foundation-live-runtime-adoption",
|
|
"backup": backup_dir,
|
|
"failedArtifact": failed_artifact,
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def inspect_device_plane_network_optional(name):
|
|
result = subprocess.run(
|
|
[str(DOCKER), "network", "inspect", name],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 1:
|
|
return None
|
|
if result.returncode != 0:
|
|
die(f"Device Plane network inspect failed: {name}")
|
|
try:
|
|
value = json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
die(f"Device Plane network inspect returned invalid JSON: {name}")
|
|
if (
|
|
not isinstance(value, list)
|
|
or len(value) != 1
|
|
or not isinstance(value[0], dict)
|
|
):
|
|
die(f"Device Plane network inspect shape mismatch: {name}")
|
|
return value[0]
|
|
|
|
|
|
def validate_device_plane_network_contract(
|
|
name,
|
|
*,
|
|
internal,
|
|
expected_container_ids,
|
|
):
|
|
network = inspect_device_plane_network_optional(name)
|
|
if network is None:
|
|
die(f"Device Plane network is missing: {name}")
|
|
options = network.get("Options") or {}
|
|
if (
|
|
network.get("Name") != name
|
|
or network.get("Driver") != "bridge"
|
|
or network.get("Internal") is not internal
|
|
or set((network.get("Containers") or {}).keys())
|
|
!= set(expected_container_ids)
|
|
):
|
|
die(f"Device Plane network boundary mismatch: {name}")
|
|
if (
|
|
name == DEVICE_PLANE_CONTROL_NETWORK
|
|
and options.get(
|
|
"com.docker.network.bridge.enable_ip_masquerade"
|
|
)
|
|
!= "false"
|
|
):
|
|
die("Device Plane control network masquerade boundary mismatch")
|
|
return network
|
|
|
|
|
|
def validate_device_plane_foundation_network_publication_evidence(
|
|
payload_dir,
|
|
):
|
|
validate_device_plane_foundation_network_publication_payload(payload_dir)
|
|
|
|
backup_dir = (
|
|
BACKUPS_DIR
|
|
/ DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_BACKUP_ID
|
|
)
|
|
try:
|
|
backup_stat = backup_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane failed recovery backup is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(
|
|
backup_stat.st_mode
|
|
):
|
|
die("Device Plane failed recovery backup is unsafe")
|
|
backup_names = {child.name for child in backup_dir.iterdir()}
|
|
expected_backup = (
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_BACKUP_SHA256
|
|
)
|
|
if backup_names != set(expected_backup):
|
|
die("Device Plane failed recovery backup file set mismatch")
|
|
for name, expected_sha256 in expected_backup.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_sha256
|
|
):
|
|
die(
|
|
"Device Plane failed recovery backup drift detected: "
|
|
f"{name}"
|
|
)
|
|
|
|
failed_artifact = (
|
|
FAILED_DIR / DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT
|
|
)
|
|
try:
|
|
failed_stat = failed_artifact.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane failed recovery 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)
|
|
!= DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT_SHA256
|
|
):
|
|
die("Device Plane failed recovery 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("Device Plane failed recovery journal is unreadable")
|
|
if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISREG(
|
|
state_stat.st_mode
|
|
):
|
|
die("Device Plane failed recovery journal is unsafe")
|
|
records = []
|
|
for line in state_lines:
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
die("Device Plane failed recovery journal contains invalid JSON")
|
|
if (
|
|
isinstance(value, dict)
|
|
and value.get("id")
|
|
== DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_PATCH_ID
|
|
):
|
|
records.append(value)
|
|
if len(records) != 1:
|
|
die("Device Plane failed recovery journal evidence count mismatch")
|
|
record = records[0]
|
|
if (
|
|
record.get("artifact")
|
|
!= DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT
|
|
or record.get("backup_id")
|
|
!= DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_BACKUP_ID
|
|
or record.get("component") != "device-plane"
|
|
or record.get("sha256")
|
|
!= DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT_SHA256
|
|
or record.get("started_apply") is not True
|
|
or record.get("rollback_status")
|
|
!= (
|
|
"ok:device-plane-overlay:"
|
|
"source-restored-runtime-unchanged:9"
|
|
)
|
|
or record.get("status") != "failed"
|
|
or record.get("message")
|
|
!= (
|
|
"healthcheck failed for http://127.0.0.1:18120/healthz: "
|
|
"<urlopen error [Errno 111] Connection refused>"
|
|
)
|
|
):
|
|
die("Device Plane failed recovery journal evidence mismatch")
|
|
|
|
comparable_entries = tuple(
|
|
rel
|
|
for rel in DEVICE_PLANE_FOUNDATION_ENTRIES
|
|
if rel != "docker-compose.device-plane.yml"
|
|
)
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-plane-failed-recovery-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id")
|
|
!= DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_PATCH_ID
|
|
or failed_manifest.get("component") != "device-plane"
|
|
or failed_manifest.get("type") != "app-overlay"
|
|
or tuple(failed_entries)
|
|
!= DEVICE_PLANE_FOUNDATION_RECOVERY_ENTRIES
|
|
):
|
|
die("Device Plane failed recovery artifact contract mismatch")
|
|
failed_source = collect_exact_files(
|
|
failed_payload,
|
|
comparable_entries,
|
|
"Device Plane failed recovery source",
|
|
)
|
|
candidate_source = collect_exact_files(
|
|
payload_dir,
|
|
comparable_entries,
|
|
"Device Plane network-publication candidate source",
|
|
)
|
|
if candidate_source != failed_source:
|
|
die(
|
|
"Device Plane network-publication candidate source drift "
|
|
"detected"
|
|
)
|
|
|
|
root = component_root("device-plane")
|
|
compose_path = root / "docker-compose.device-plane.yml"
|
|
if (
|
|
compose_path.is_symlink()
|
|
or not compose_path.is_file()
|
|
or sha256_file(compose_path)
|
|
!= DEVICE_PLANE_FOUNDATION_PREDECESSOR_COMPOSE_SHA256
|
|
):
|
|
die("Device Plane network-publication source predecessor mismatch")
|
|
for rel in DEVICE_PLANE_FOUNDATION_ENTRIES:
|
|
if rel == "docker-compose.device-plane.yml":
|
|
continue
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Device Plane network-publication predecessor retained "
|
|
f"source: {rel}"
|
|
)
|
|
for descriptor_rel in (
|
|
DEVICE_PLANE_FOUNDATION_RECOVERY_REL,
|
|
DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_REL,
|
|
):
|
|
descriptor_path = root / descriptor_rel
|
|
if descriptor_path.exists() or descriptor_path.is_symlink():
|
|
die(
|
|
"Device Plane network-publication descriptor predecessor "
|
|
"mismatch"
|
|
)
|
|
bootstrap_descriptor = root / DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL
|
|
if (
|
|
bootstrap_descriptor.is_symlink()
|
|
or not bootstrap_descriptor.is_file()
|
|
or sha256_file(bootstrap_descriptor)
|
|
!= DEVICE_PLANE_POSTGRES_BOOTSTRAP_DESCRIPTOR_SHA256
|
|
):
|
|
die("Device Plane PostgreSQL bootstrap descriptor drift detected")
|
|
|
|
runtime = validate_device_plane_foundation_runtime()
|
|
for service, expected_id in (
|
|
DEVICE_PLANE_FOUNDATION_PREDECESSOR_CONTAINER_IDS.items()
|
|
):
|
|
if runtime[service]["containerId"] != expected_id:
|
|
die(
|
|
"Device Plane network-publication runtime predecessor "
|
|
f"generation mismatch: {service}"
|
|
)
|
|
expected_network_ids = {
|
|
item["containerId"]
|
|
for item in runtime.values()
|
|
}
|
|
validate_device_plane_network_contract(
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
internal=True,
|
|
expected_container_ids=expected_network_ids,
|
|
)
|
|
if inspect_device_plane_network_optional(DEVICE_PLANE_CONTROL_NETWORK):
|
|
die("Device Plane control network already exists")
|
|
|
|
return {
|
|
"mode": "failed-foundation-network-publication-correction",
|
|
"backup": backup_dir,
|
|
"failedArtifact": failed_artifact,
|
|
"runtime": runtime,
|
|
}
|
|
|
|
|
|
def validate_device_plane_postgres_bootstrap_payload(payload_dir):
|
|
descriptor = read_strict_json(
|
|
payload_dir / DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
|
|
"Device Plane PostgreSQL bootstrap descriptor",
|
|
max_bytes=8 * 1024,
|
|
)
|
|
expected = {
|
|
"schemaVersion": "nodedc.device-plane.postgres-bootstrap.v1",
|
|
"service": "device-postgres",
|
|
"volume": DEVICE_PLANE_POSTGRES_VOLUME,
|
|
"mode": "create-if-absent",
|
|
"ordinaryApplicationSelection": "forbidden",
|
|
"rollbackVolumePolicy": "preserve",
|
|
}
|
|
if descriptor != expected:
|
|
die("Device Plane PostgreSQL bootstrap descriptor mismatch")
|
|
return descriptor
|
|
|
|
|
|
def preflight_device_plane_postgres_bootstrap():
|
|
container_result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"container",
|
|
"ls",
|
|
"-a",
|
|
"--filter",
|
|
"label=com.docker.compose.project=nodedc-device-plane",
|
|
"--filter",
|
|
"label=com.docker.compose.service=device-postgres",
|
|
"--format",
|
|
"{{.ID}}",
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if container_result.returncode != 0:
|
|
die("Device Plane PostgreSQL container preflight failed")
|
|
container_ids = [
|
|
line.strip()
|
|
for line in container_result.stdout.splitlines()
|
|
if line.strip()
|
|
]
|
|
if container_ids:
|
|
die("Device Plane PostgreSQL container already exists")
|
|
|
|
volume_result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"volume",
|
|
"inspect",
|
|
DEVICE_PLANE_POSTGRES_VOLUME,
|
|
],
|
|
check=False,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
if volume_result.returncode == 0:
|
|
die("Device Plane PostgreSQL volume already exists")
|
|
if volume_result.returncode != 1:
|
|
die("Device Plane PostgreSQL volume preflight failed")
|
|
return "absent"
|
|
|
|
|
|
def device_plane_postgres_plan_selection(postgres_preflight):
|
|
if postgres_preflight is None:
|
|
return "preserved-prerequisite:not-selected"
|
|
if postgres_preflight != "absent":
|
|
die("Device Plane PostgreSQL plan preflight state is invalid")
|
|
return "bootstrap-selected:create-if-absent"
|
|
|
|
|
|
def validate_device_plane_foundation_runtime(network_publication=False):
|
|
validate_device_plane_runtime_secret_metadata()
|
|
expected = {
|
|
"device-control-core": {
|
|
"image": DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
"imageId": DEVICE_PLANE_FOUNDATION_RECOVERY_IMAGE_IDS[
|
|
"device-control-core"
|
|
],
|
|
"user": "1000:1000",
|
|
"ports": {
|
|
"18120/tcp": [{
|
|
"HostIp": "127.0.0.1",
|
|
"HostPort": "18120",
|
|
}],
|
|
},
|
|
"actualPorts": (
|
|
{
|
|
"18120/tcp": [{
|
|
"HostIp": "127.0.0.1",
|
|
"HostPort": "18120",
|
|
}],
|
|
}
|
|
if network_publication
|
|
else {}
|
|
),
|
|
"networks": (
|
|
{
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
DEVICE_PLANE_CONTROL_NETWORK,
|
|
}
|
|
if network_publication
|
|
else {DEVICE_PLANE_PRIVATE_NETWORK}
|
|
),
|
|
},
|
|
"device-gateway": {
|
|
"image": DEVICE_PLANE_GATEWAY_IMAGE,
|
|
"imageId": DEVICE_PLANE_FOUNDATION_RECOVERY_IMAGE_IDS[
|
|
"device-gateway"
|
|
],
|
|
"user": "1000:1000",
|
|
"ports": {
|
|
"18121/tcp": [{
|
|
"HostIp": "127.0.0.1",
|
|
"HostPort": "18121",
|
|
}],
|
|
},
|
|
"actualPorts": (
|
|
{
|
|
"18121/tcp": [{
|
|
"HostIp": "127.0.0.1",
|
|
"HostPort": "18121",
|
|
}],
|
|
}
|
|
if network_publication
|
|
else {}
|
|
),
|
|
"networks": (
|
|
{
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
DEVICE_PLANE_CONTROL_NETWORK,
|
|
}
|
|
if network_publication
|
|
else {DEVICE_PLANE_PRIVATE_NETWORK}
|
|
),
|
|
},
|
|
"device-postgres": {
|
|
"image": "postgres:16-alpine",
|
|
"imageId": DEVICE_PLANE_FOUNDATION_RECOVERY_IMAGE_IDS[
|
|
"device-postgres"
|
|
],
|
|
"user": "",
|
|
"ports": {},
|
|
"actualPorts": {},
|
|
"networks": {DEVICE_PLANE_PRIVATE_NETWORK},
|
|
},
|
|
}
|
|
accepted = {}
|
|
for service, contract in expected.items():
|
|
container_ids = device_plane_service_container_ids(service)
|
|
if len(container_ids) != 1:
|
|
die(
|
|
"Device Plane foundation runtime service count mismatch: "
|
|
f"{service}"
|
|
)
|
|
container = inspect_device_plane_container(container_ids[0])
|
|
state = container.get("State") or {}
|
|
config = container.get("Config") or {}
|
|
host_config = container.get("HostConfig") or {}
|
|
labels = config.get("Labels") or {}
|
|
networks = (container.get("NetworkSettings") or {}).get(
|
|
"Networks"
|
|
) or {}
|
|
actual_ports = (container.get("NetworkSettings") or {}).get(
|
|
"Ports"
|
|
) or {}
|
|
if (
|
|
state.get("Status") != "running"
|
|
or state.get("Running") is not True
|
|
or state.get("Restarting") is True
|
|
or state.get("ExitCode") != 0
|
|
or state.get("Error") not in ("", None)
|
|
or (state.get("Health") or {}).get("Status") != "healthy"
|
|
or int(container.get("RestartCount") or 0) != 0
|
|
):
|
|
die(
|
|
"Device Plane foundation runtime state mismatch: "
|
|
f"{service}"
|
|
)
|
|
if (
|
|
container.get("Image") != contract["imageId"]
|
|
or config.get("Image") != contract["image"]
|
|
or config.get("User", "") != contract["user"]
|
|
or (host_config.get("PortBindings") or {}) != contract["ports"]
|
|
or (host_config.get("RestartPolicy") or {}).get("Name")
|
|
!= "unless-stopped"
|
|
or labels.get("com.docker.compose.project")
|
|
!= "nodedc-device-plane"
|
|
or labels.get("com.docker.compose.service") != service
|
|
or set(networks) != contract["networks"]
|
|
or actual_ports != contract["actualPorts"]
|
|
):
|
|
die(
|
|
"Device Plane foundation runtime boundary mismatch: "
|
|
f"{service}"
|
|
)
|
|
accepted[service] = {
|
|
"containerId": container.get("Id"),
|
|
"imageId": container.get("Image"),
|
|
"health": "healthy",
|
|
"restartCount": 0,
|
|
}
|
|
|
|
if network_publication:
|
|
for service in ("device-control-core", "device-gateway"):
|
|
if (
|
|
accepted[service]["containerId"]
|
|
== DEVICE_PLANE_FOUNDATION_PREDECESSOR_CONTAINER_IDS[service]
|
|
):
|
|
die(
|
|
"Device Plane stateless generation was not recreated: "
|
|
f"{service}"
|
|
)
|
|
if (
|
|
accepted["device-postgres"]["containerId"]
|
|
!= DEVICE_PLANE_FOUNDATION_PREDECESSOR_CONTAINER_IDS[
|
|
"device-postgres"
|
|
]
|
|
):
|
|
die("Device Plane PostgreSQL generation changed")
|
|
|
|
core = inspect_device_plane_container(
|
|
device_plane_service_container_ids("device-control-core")[0]
|
|
)
|
|
gateway = inspect_device_plane_container(
|
|
device_plane_service_container_ids("device-gateway")[0]
|
|
)
|
|
postgres = inspect_device_plane_container(
|
|
device_plane_service_container_ids("device-postgres")[0]
|
|
)
|
|
for service, container in (
|
|
("device-control-core", core),
|
|
("device-gateway", gateway),
|
|
):
|
|
host_config = container.get("HostConfig") or {}
|
|
if (
|
|
host_config.get("ReadonlyRootfs") is not True
|
|
or set(host_config.get("CapDrop") or ()) != {"ALL"}
|
|
or "no-new-privileges:true"
|
|
not in set(host_config.get("SecurityOpt") or ())
|
|
):
|
|
die(
|
|
"Device Plane stateless hardening mismatch: "
|
|
f"{service}"
|
|
)
|
|
|
|
core_environment = container_environment(
|
|
core,
|
|
"Device Plane Control Core",
|
|
)
|
|
core_required_environment = {
|
|
"HOST": "0.0.0.0",
|
|
"PORT": "18120",
|
|
"DEVICE_DATABASE_HOST": "device-postgres",
|
|
"DEVICE_DATABASE_PORT": "5432",
|
|
"DEVICE_DATABASE_NAME": "device_plane",
|
|
"DEVICE_DATABASE_USER": "device_plane",
|
|
"DEVICE_DATABASE_PASSWORD_FILE": (
|
|
"/run/nodedc-secrets/postgres-password"
|
|
),
|
|
"DEVICE_DATABASE_POOL_SIZE": "10",
|
|
"DEVICE_DISCOVERY_INGEST_ENABLED": "false",
|
|
}
|
|
if any(
|
|
core_environment.get(key) != value
|
|
for key, value in core_required_environment.items()
|
|
):
|
|
die("Device Plane Control Core environment mismatch")
|
|
for forbidden in (
|
|
"DEVICE_DATABASE_URL",
|
|
"DEVICE_DATABASE_PASSWORD",
|
|
"DEVICE_GATEWAY_CORE_TOKEN",
|
|
"DEVICE_IDENTIFIER_PEPPER",
|
|
):
|
|
if forbidden in core_environment:
|
|
die("Device Plane Control Core plaintext secret boundary mismatch")
|
|
|
|
gateway_environment = container_environment(
|
|
gateway,
|
|
"Device Plane Gateway",
|
|
)
|
|
gateway_required_environment = {
|
|
"DEVICE_GATEWAY_HEALTH_HOST": "0.0.0.0",
|
|
"DEVICE_GATEWAY_HEALTH_PORT": "18121",
|
|
"DEVICE_GATEWAY_LISTEN_ENABLED": "false",
|
|
"DEVICE_GATEWAY_TCP_HOST": "127.0.0.1",
|
|
"DEVICE_GATEWAY_TCP_PORT": "9921",
|
|
"DEVICE_GATEWAY_MAX_SESSIONS": "100",
|
|
"DEVICE_GATEWAY_SESSION_TIMEOUT_MS": "10000",
|
|
}
|
|
if any(
|
|
gateway_environment.get(key) != value
|
|
for key, value in gateway_required_environment.items()
|
|
):
|
|
die("Device Plane Gateway environment mismatch")
|
|
if any(
|
|
key in gateway_environment
|
|
for key in (
|
|
"DEVICE_GATEWAY_CORE_TOKEN",
|
|
"DEVICE_GATEWAY_COMMAND_TOKEN",
|
|
)
|
|
):
|
|
die("Device Plane Gateway plaintext secret boundary mismatch")
|
|
|
|
core_mounts = core.get("Mounts") or []
|
|
if len(core_mounts) != 1:
|
|
die("Device Plane Control Core mount count mismatch")
|
|
core_secret = core_mounts[0]
|
|
if (
|
|
core_secret.get("Type") != "bind"
|
|
or core_secret.get("Source")
|
|
!= str(DEVICE_PLANE_POSTGRES_PASSWORD_FILE)
|
|
or core_secret.get("Destination")
|
|
!= "/run/nodedc-secrets/postgres-password"
|
|
or core_secret.get("RW") is not False
|
|
):
|
|
die("Device Plane Control Core secret mount mismatch")
|
|
if gateway.get("Mounts") not in (None, []):
|
|
die("Device Plane Gateway unexpected mount")
|
|
|
|
postgres_mounts = postgres.get("Mounts") or []
|
|
postgres_secret = [
|
|
mount
|
|
for mount in postgres_mounts
|
|
if mount.get("Destination")
|
|
== "/run/nodedc-secrets/postgres-password"
|
|
]
|
|
postgres_volume = [
|
|
mount
|
|
for mount in postgres_mounts
|
|
if mount.get("Destination") == "/var/lib/postgresql/data"
|
|
]
|
|
if (
|
|
len(postgres_mounts) != 2
|
|
or len(postgres_secret) != 1
|
|
or postgres_secret[0].get("Type") != "bind"
|
|
or postgres_secret[0].get("Source")
|
|
!= str(DEVICE_PLANE_POSTGRES_PASSWORD_FILE)
|
|
or postgres_secret[0].get("RW") is not False
|
|
or len(postgres_volume) != 1
|
|
or postgres_volume[0].get("Type") != "volume"
|
|
or postgres_volume[0].get("Name")
|
|
!= DEVICE_PLANE_POSTGRES_VOLUME
|
|
or postgres_volume[0].get("RW") is not True
|
|
):
|
|
die("Device Plane PostgreSQL preserved mount mismatch")
|
|
if network_publication:
|
|
validate_device_plane_network_contract(
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
internal=True,
|
|
expected_container_ids={
|
|
accepted[service]["containerId"]
|
|
for service in DEVICE_PLANE_RUNTIME_SERVICES
|
|
},
|
|
)
|
|
validate_device_plane_network_contract(
|
|
DEVICE_PLANE_CONTROL_NETWORK,
|
|
internal=False,
|
|
expected_container_ids={
|
|
accepted["device-control-core"]["containerId"],
|
|
accepted["device-gateway"]["containerId"],
|
|
},
|
|
)
|
|
return accepted
|
|
|
|
|
|
def validate_device_plane_b2_discovery_ingress_runtime(
|
|
runtime_before,
|
|
preserved_stateless_services=(),
|
|
):
|
|
validate_device_plane_runtime_secret_metadata()
|
|
preserved_stateless = set(preserved_stateless_services)
|
|
if not preserved_stateless.issubset({
|
|
"device-control-core",
|
|
"device-gateway",
|
|
}):
|
|
die("Device Plane B2 preserved stateless service set mismatch")
|
|
before_names = device_plane_inventory_service_names(runtime_before)
|
|
if set(before_names) != set(DEVICE_PLANE_RUNTIME_SERVICES):
|
|
die("Device Plane B2 ingress predecessor inventory mismatch")
|
|
before = {
|
|
item["service"]: item
|
|
for item in runtime_before["services"]
|
|
}
|
|
contracts = {
|
|
"device-control-core": {
|
|
"image": DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
"user": "1000:1000",
|
|
"ports": {
|
|
"18120/tcp": [{
|
|
"HostIp": "127.0.0.1",
|
|
"HostPort": "18120",
|
|
}],
|
|
},
|
|
"networks": {
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
DEVICE_PLANE_CONTROL_NETWORK,
|
|
},
|
|
},
|
|
"device-gateway": {
|
|
"image": DEVICE_PLANE_GATEWAY_IMAGE,
|
|
"user": "1000:1000",
|
|
"ports": {
|
|
"18121/tcp": [{
|
|
"HostIp": "127.0.0.1",
|
|
"HostPort": "18121",
|
|
}],
|
|
"9921/tcp": [{
|
|
"HostIp": "127.0.0.1",
|
|
"HostPort": "9921",
|
|
}],
|
|
},
|
|
"networks": {
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
DEVICE_PLANE_CONTROL_NETWORK,
|
|
},
|
|
},
|
|
"device-postgres": {
|
|
"image": "postgres:16-alpine",
|
|
"user": "",
|
|
"ports": {},
|
|
"networks": {DEVICE_PLANE_PRIVATE_NETWORK},
|
|
},
|
|
}
|
|
accepted = {}
|
|
containers = {}
|
|
for service, contract in contracts.items():
|
|
container_ids = device_plane_service_container_ids(service)
|
|
if len(container_ids) != 1:
|
|
die(f"Device Plane B2 ingress service count mismatch: {service}")
|
|
container = inspect_device_plane_container(container_ids[0])
|
|
containers[service] = container
|
|
state = container.get("State") or {}
|
|
config = container.get("Config") or {}
|
|
host_config = container.get("HostConfig") or {}
|
|
labels = config.get("Labels") or {}
|
|
networks = (container.get("NetworkSettings") or {}).get(
|
|
"Networks"
|
|
) or {}
|
|
actual_ports = (container.get("NetworkSettings") or {}).get(
|
|
"Ports"
|
|
) or {}
|
|
if (
|
|
state.get("Status") != "running"
|
|
or state.get("Running") is not True
|
|
or state.get("Restarting") is True
|
|
or state.get("ExitCode") != 0
|
|
or state.get("Error") not in ("", None)
|
|
or (state.get("Health") or {}).get("Status") != "healthy"
|
|
or int(container.get("RestartCount") or 0) != 0
|
|
or config.get("Image") != contract["image"]
|
|
or config.get("User", "") != contract["user"]
|
|
or (host_config.get("PortBindings") or {}) != contract["ports"]
|
|
or actual_ports != contract["ports"]
|
|
or set(networks) != contract["networks"]
|
|
or (host_config.get("RestartPolicy") or {}).get("Name")
|
|
!= "unless-stopped"
|
|
or labels.get("com.docker.compose.project")
|
|
!= "nodedc-device-plane"
|
|
or labels.get("com.docker.compose.service") != service
|
|
):
|
|
die(f"Device Plane B2 ingress runtime mismatch: {service}")
|
|
accepted[service] = {
|
|
"containerId": container.get("Id"),
|
|
"imageId": container.get("Image"),
|
|
}
|
|
|
|
if (
|
|
accepted["device-postgres"]["containerId"]
|
|
!= before["device-postgres"]["containerId"]
|
|
or accepted["device-postgres"]["imageId"]
|
|
!= before["device-postgres"]["imageId"]
|
|
):
|
|
die("Device Plane B2 ingress changed PostgreSQL generation")
|
|
for service in ("device-control-core", "device-gateway"):
|
|
same_container = (
|
|
accepted[service]["containerId"]
|
|
== before[service]["containerId"]
|
|
)
|
|
same_image = (
|
|
accepted[service]["imageId"] == before[service]["imageId"]
|
|
)
|
|
if service in preserved_stateless:
|
|
if not (same_container and same_image):
|
|
die(
|
|
"Device Plane B2 stateless generation was not preserved: "
|
|
f"{service}"
|
|
)
|
|
elif same_container or same_image:
|
|
die(
|
|
"Device Plane B2 ingress stateless generation not replaced: "
|
|
f"{service}"
|
|
)
|
|
host_config = containers[service].get("HostConfig") or {}
|
|
if (
|
|
host_config.get("ReadonlyRootfs") is not True
|
|
or set(host_config.get("CapDrop") or ()) != {"ALL"}
|
|
or "no-new-privileges:true"
|
|
not in set(host_config.get("SecurityOpt") or ())
|
|
):
|
|
die(
|
|
"Device Plane B2 ingress hardening mismatch: "
|
|
f"{service}"
|
|
)
|
|
|
|
core_environment = container_environment(
|
|
containers["device-control-core"],
|
|
"Device Plane B2 Control Core",
|
|
)
|
|
core_required = {
|
|
"DEVICE_DISCOVERY_INGEST_ENABLED": "true",
|
|
"DEVICE_GATEWAY_CORE_TOKEN_FILE":
|
|
"/run/nodedc-secrets/gateway-core-token",
|
|
"DEVICE_IDENTIFIER_PEPPER_FILE":
|
|
"/run/nodedc-secrets/identifier-pepper",
|
|
}
|
|
if any(
|
|
core_environment.get(key) != value
|
|
for key, value in core_required.items()
|
|
):
|
|
die("Device Plane B2 Control Core environment mismatch")
|
|
|
|
gateway_environment = container_environment(
|
|
containers["device-gateway"],
|
|
"Device Plane B2 Gateway",
|
|
)
|
|
gateway_required = {
|
|
"DEVICE_GATEWAY_LISTEN_ENABLED": "true",
|
|
"DEVICE_GATEWAY_PUBLIC_INGRESS_ENABLED": "false",
|
|
"DEVICE_GATEWAY_TCP_HOST": "127.0.0.1",
|
|
"DEVICE_GATEWAY_TCP_PORT": "9921",
|
|
"DEVICE_GATEWAY_CORE_URL": "http://device-control-core:18120",
|
|
"DEVICE_GATEWAY_CORE_TOKEN_FILE":
|
|
"/run/nodedc-secrets/gateway-core-token",
|
|
"DEVICE_GATEWAY_CORE_TIMEOUT_MS": "5000",
|
|
"DEVICE_GATEWAY_MAX_BUFFERED_BYTES": "65536",
|
|
"DEVICE_GATEWAY_MAX_SESSIONS": "100",
|
|
"DEVICE_GATEWAY_MAX_SESSIONS_PER_ADDRESS": "10",
|
|
"DEVICE_GATEWAY_MAX_CONNECTIONS_PER_MINUTE_PER_ADDRESS": "30",
|
|
"DEVICE_GATEWAY_SESSION_TIMEOUT_MS": "10000",
|
|
}
|
|
if any(
|
|
gateway_environment.get(key) != value
|
|
for key, value in gateway_required.items()
|
|
):
|
|
die("Device Plane B2 Gateway environment mismatch")
|
|
for environment in (core_environment, gateway_environment):
|
|
for forbidden in (
|
|
"DEVICE_GATEWAY_CORE_TOKEN",
|
|
"DEVICE_IDENTIFIER_PEPPER",
|
|
"DEVICE_GATEWAY_COMMAND_TOKEN",
|
|
):
|
|
if forbidden in environment:
|
|
die("Device Plane B2 plaintext secret boundary mismatch")
|
|
|
|
core_mounts = {
|
|
mount.get("Destination"): mount
|
|
for mount in containers["device-control-core"].get("Mounts") or []
|
|
}
|
|
expected_core_mounts = {
|
|
"/run/nodedc-secrets/postgres-password":
|
|
DEVICE_PLANE_POSTGRES_PASSWORD_FILE,
|
|
"/run/nodedc-secrets/gateway-core-token":
|
|
DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE,
|
|
"/run/nodedc-secrets/identifier-pepper":
|
|
DEVICE_PLANE_IDENTIFIER_PEPPER_FILE,
|
|
}
|
|
if set(core_mounts) != set(expected_core_mounts):
|
|
die("Device Plane B2 Control Core mount set mismatch")
|
|
for destination, source in expected_core_mounts.items():
|
|
mount = core_mounts[destination]
|
|
if (
|
|
mount.get("Type") != "bind"
|
|
or mount.get("Source") != str(source)
|
|
or mount.get("RW") is not False
|
|
):
|
|
die("Device Plane B2 Control Core secret mount mismatch")
|
|
|
|
gateway_mounts = (
|
|
containers["device-gateway"].get("Mounts") or []
|
|
)
|
|
if (
|
|
len(gateway_mounts) != 1
|
|
or gateway_mounts[0].get("Type") != "bind"
|
|
or gateway_mounts[0].get("Source")
|
|
!= str(DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE)
|
|
or gateway_mounts[0].get("Destination")
|
|
!= "/run/nodedc-secrets/gateway-core-token"
|
|
or gateway_mounts[0].get("RW") is not False
|
|
):
|
|
die("Device Plane B2 Gateway secret mount mismatch")
|
|
|
|
postgres_mounts = containers["device-postgres"].get("Mounts") or []
|
|
postgres_secret = [
|
|
mount
|
|
for mount in postgres_mounts
|
|
if mount.get("Destination")
|
|
== "/run/nodedc-secrets/postgres-password"
|
|
]
|
|
postgres_volume = [
|
|
mount
|
|
for mount in postgres_mounts
|
|
if mount.get("Destination") == "/var/lib/postgresql/data"
|
|
]
|
|
if (
|
|
len(postgres_mounts) != 2
|
|
or len(postgres_secret) != 1
|
|
or postgres_secret[0].get("Type") != "bind"
|
|
or postgres_secret[0].get("Source")
|
|
!= str(DEVICE_PLANE_POSTGRES_PASSWORD_FILE)
|
|
or postgres_secret[0].get("RW") is not False
|
|
or len(postgres_volume) != 1
|
|
or postgres_volume[0].get("Type") != "volume"
|
|
or postgres_volume[0].get("Name")
|
|
!= DEVICE_PLANE_POSTGRES_VOLUME
|
|
or postgres_volume[0].get("RW") is not True
|
|
):
|
|
die("Device Plane B2 PostgreSQL preserved mount mismatch")
|
|
|
|
expected_private_ids = {
|
|
accepted[service]["containerId"]
|
|
for service in DEVICE_PLANE_RUNTIME_SERVICES
|
|
}
|
|
validate_device_plane_network_contract(
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
internal=True,
|
|
expected_container_ids=expected_private_ids,
|
|
)
|
|
validate_device_plane_network_contract(
|
|
DEVICE_PLANE_CONTROL_NETWORK,
|
|
internal=False,
|
|
expected_container_ids={
|
|
accepted["device-control-core"]["containerId"],
|
|
accepted["device-gateway"]["containerId"],
|
|
},
|
|
)
|
|
assert_loopback_tcp_port_open(9921)
|
|
return accepted
|
|
|
|
|
|
def validate_device_plane_preserved_runtime_unchanged(runtime_before, label):
|
|
before_names = device_plane_inventory_service_names(runtime_before)
|
|
if set(before_names) != set(DEVICE_PLANE_RUNTIME_SERVICES):
|
|
die(f"{label} predecessor inventory mismatch")
|
|
before = {
|
|
item["service"]: item
|
|
for item in runtime_before["services"]
|
|
}
|
|
current = device_plane_runtime_inventory(DEVICE_PLANE_RUNTIME_SERVICES)
|
|
current_names = device_plane_inventory_service_names(current)
|
|
if set(current_names) != set(DEVICE_PLANE_RUNTIME_SERVICES):
|
|
die(f"{label} preserved runtime is incomplete")
|
|
for item in current["services"]:
|
|
predecessor = before[item["service"]]
|
|
if (
|
|
item["containerId"] != predecessor["containerId"]
|
|
or item["imageId"] != predecessor["imageId"]
|
|
or item["status"] != "running"
|
|
or item["running"] is not True
|
|
or item["health"] != "healthy"
|
|
or item["restartCount"] != 0
|
|
):
|
|
die(f"{label} changed preserved service: {item['service']}")
|
|
return current
|
|
|
|
|
|
def validate_device_plane_backhaul_target_runtime(
|
|
runtime_before,
|
|
expected_enrollment=None,
|
|
):
|
|
current = validate_device_plane_preserved_runtime_unchanged(
|
|
runtime_before,
|
|
"Device Plane backhaul",
|
|
)
|
|
root = component_root("device-plane")
|
|
installed_descriptor = read_strict_json(
|
|
root / DEVICE_PLANE_BACKHAUL_TARGET_REL,
|
|
"installed Device Plane backhaul target descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if installed_descriptor != expected_device_plane_backhaul_target_descriptor():
|
|
die("installed Device Plane backhaul target descriptor mismatch")
|
|
installed_compose = root / DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL
|
|
if (
|
|
installed_compose.is_symlink()
|
|
or not installed_compose.is_file()
|
|
or sha256_file(installed_compose)
|
|
!= DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_SHA256
|
|
):
|
|
die("installed Device Plane backhaul target Compose mismatch")
|
|
failed_descriptor = root / DEVICE_PLANE_BACKHAUL_FAILED_TARGET_REL
|
|
if failed_descriptor.exists() or failed_descriptor.is_symlink():
|
|
die("failed Device Plane backhaul descriptor remains installed")
|
|
|
|
container_ids = device_plane_service_container_ids(
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE
|
|
)
|
|
if len(container_ids) != 1:
|
|
die("Device Plane backhaul target service count mismatch")
|
|
container = inspect_device_plane_container(container_ids[0])
|
|
state = container.get("State") or {}
|
|
config = container.get("Config") or {}
|
|
host_config = container.get("HostConfig") or {}
|
|
labels = config.get("Labels") or {}
|
|
mounts = container.get("Mounts") or []
|
|
expected_mounts = {
|
|
"/run/nodedc-secrets/ssh_host_ed25519_key": (
|
|
DEVICE_PLANE_BACKHAUL_HOST_KEY_FILE
|
|
),
|
|
"/run/nodedc-secrets/authorized_keys": (
|
|
DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE
|
|
),
|
|
}
|
|
actual_mounts = {
|
|
mount.get("Destination"): mount
|
|
for mount in mounts
|
|
}
|
|
if (
|
|
state.get("Status") != "running"
|
|
or state.get("Running") is not True
|
|
or state.get("Restarting") is True
|
|
or state.get("ExitCode") != 0
|
|
or state.get("Error") not in ("", None)
|
|
or (state.get("Health") or {}).get("Status") != "healthy"
|
|
or int(container.get("RestartCount") or 0) != 0
|
|
or config.get("Image") != DEVICE_PLANE_BACKHAUL_TARGET_IMAGE
|
|
or config.get("User", "") != ""
|
|
or host_config.get("NetworkMode") != "host"
|
|
or (host_config.get("PortBindings") or {}) != {}
|
|
or ((container.get("NetworkSettings") or {}).get("Ports") or {})
|
|
!= {}
|
|
or host_config.get("ReadonlyRootfs") is not True
|
|
or set(host_config.get("CapDrop") or ()) != {"ALL"}
|
|
or set(host_config.get("CapAdd") or ())
|
|
!= {"CHOWN", "DAC_OVERRIDE", "SETGID", "SETUID", "SYS_CHROOT"}
|
|
or "no-new-privileges:true"
|
|
not in set(host_config.get("SecurityOpt") or ())
|
|
or (host_config.get("RestartPolicy") or {}).get("Name")
|
|
!= "unless-stopped"
|
|
or labels.get("com.docker.compose.project")
|
|
!= "nodedc-device-plane"
|
|
or labels.get("com.docker.compose.service")
|
|
!= DEVICE_PLANE_BACKHAUL_TARGET_SERVICE
|
|
or set(actual_mounts) != set(expected_mounts)
|
|
):
|
|
die("Device Plane backhaul target runtime mismatch")
|
|
for destination, source in expected_mounts.items():
|
|
mount = actual_mounts[destination]
|
|
if (
|
|
mount.get("Type") != "bind"
|
|
or mount.get("Source") != str(source)
|
|
or mount.get("RW") is not False
|
|
):
|
|
die("Device Plane backhaul target mount mismatch")
|
|
environment = container_environment(
|
|
container,
|
|
"Device Plane backhaul target",
|
|
)
|
|
for name in environment:
|
|
if any(token in name.upper() for token in ("KEY", "PASS", "TOKEN")):
|
|
die("Device Plane backhaul target plaintext environment rejected")
|
|
|
|
effective = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"exec",
|
|
container_ids[0],
|
|
"/usr/sbin/sshd",
|
|
"-T",
|
|
"-f",
|
|
"/etc/ssh/sshd_config",
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout.lower()
|
|
for required in (
|
|
"port 2222",
|
|
"listenaddress 127.0.0.1:2222",
|
|
"passwordauthentication no",
|
|
"kbdinteractiveauthentication no",
|
|
"allowtcpforwarding local",
|
|
"permitopen 127.0.0.1:9921",
|
|
"gatewayports no",
|
|
"permittty no",
|
|
"allowagentforwarding no",
|
|
"x11forwarding no",
|
|
"permittunnel no",
|
|
"forcecommand /bin/false",
|
|
):
|
|
if required not in effective:
|
|
die(
|
|
"Device Plane backhaul effective sshd contract mismatch: "
|
|
f"{required}"
|
|
)
|
|
|
|
enrollment = (
|
|
expected_enrollment
|
|
if expected_enrollment is not None
|
|
else read_device_plane_backhaul_enrollment_public_key()
|
|
)
|
|
authorized = (
|
|
'restrict,port-forwarding,permitopen="127.0.0.1:9921" '
|
|
f"{enrollment['line']}\n"
|
|
)
|
|
for path, mode, max_size in (
|
|
(DEVICE_PLANE_BACKHAUL_HOST_KEY_FILE, 0o400, 2048),
|
|
(DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE, 0o444, 2048),
|
|
(DEVICE_PLANE_BACKHAUL_HOST_PUBLIC_KEY_FILE, 0o444, 1024),
|
|
):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane backhaul runtime trust file is missing")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_uid != 0
|
|
or stat.S_IMODE(path_stat.st_mode) != mode
|
|
or path_stat.st_size > max_size
|
|
):
|
|
die("Device Plane backhaul runtime trust boundary mismatch")
|
|
if DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE.read_text(
|
|
encoding="ascii"
|
|
) != authorized:
|
|
die("Device Plane backhaul authorized key mismatch")
|
|
|
|
try:
|
|
connection = socket.create_connection(
|
|
(
|
|
DEVICE_PLANE_BACKHAUL_LOOPBACK_ADDRESS,
|
|
DEVICE_PLANE_BACKHAUL_LISTEN_PORT,
|
|
),
|
|
timeout=5,
|
|
)
|
|
connection.settimeout(5)
|
|
banner = connection.recv(256)
|
|
connection.close()
|
|
except OSError as exc:
|
|
die(f"Device Plane backhaul SSH listener is unavailable: {exc}")
|
|
if not banner.startswith(b"SSH-2.0-OpenSSH_"):
|
|
die("Device Plane backhaul SSH banner mismatch")
|
|
validate_device_plane_tailscale_runtime(require_target=True)
|
|
assert_loopback_tcp_port_open(9921)
|
|
return {
|
|
"preserved": current,
|
|
"targetContainerId": container.get("Id"),
|
|
}
|
|
|
|
|
|
def validate_device_plane_runtime_secret_metadata(include_management=False):
|
|
try:
|
|
directory_stat = DEVICE_PLANE_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane runtime secret directory is missing")
|
|
if (
|
|
stat.S_ISLNK(directory_stat.st_mode)
|
|
or not stat.S_ISDIR(directory_stat.st_mode)
|
|
or directory_stat.st_uid != 0
|
|
or directory_stat.st_gid != MAP_GATEWAY_RUNTIME_GID
|
|
or stat.S_IMODE(directory_stat.st_mode) != 0o710
|
|
):
|
|
die("Device Plane runtime secret directory boundary mismatch")
|
|
required_secrets = [
|
|
(DEVICE_PLANE_POSTGRES_PASSWORD_FILE, "PostgreSQL"),
|
|
(DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE, "Gateway to Core"),
|
|
(DEVICE_PLANE_IDENTIFIER_PEPPER_FILE, "identifier pepper"),
|
|
]
|
|
if include_management:
|
|
required_secrets.append((
|
|
DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE,
|
|
"management to Core",
|
|
))
|
|
for path, label in required_secrets:
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Device Plane {label} runtime secret is missing")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_uid != 0
|
|
or path_stat.st_gid != MAP_GATEWAY_RUNTIME_GID
|
|
or stat.S_IMODE(path_stat.st_mode) != 0o640
|
|
or path_stat.st_size > 512
|
|
):
|
|
die(f"Device Plane {label} runtime secret boundary mismatch")
|
|
try:
|
|
value = path.read_text(encoding="ascii").strip()
|
|
except (OSError, UnicodeDecodeError):
|
|
die(f"Device Plane {label} runtime secret is unreadable")
|
|
if not MAP_GATEWAY_SECRET_RE.fullmatch(value):
|
|
die(f"Device Plane {label} runtime secret format mismatch")
|
|
return "exact"
|
|
|
|
|
|
def validate_device_manager_control_plane_runtime(
|
|
*,
|
|
require_edge_channel=None,
|
|
core_network_mode=None,
|
|
require_persistent_data=None,
|
|
):
|
|
core_ids = device_plane_service_container_ids("device-control-core")
|
|
manager_ids = device_plane_service_container_ids("device-manager")
|
|
if len(core_ids) != 1 or len(manager_ids) != 1:
|
|
die("Device Manager control-plane runtime topology mismatch")
|
|
core = inspect_device_plane_container(core_ids[0])
|
|
manager = inspect_device_plane_container(manager_ids[0])
|
|
core_environment = container_environment(core, "Device Control Core")
|
|
manager_environment = container_environment(manager, "Device Manager")
|
|
if require_edge_channel is None:
|
|
edge_descriptor = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL
|
|
)
|
|
require_edge_channel = edge_descriptor.is_file() and not edge_descriptor.is_symlink()
|
|
if require_persistent_data is None:
|
|
data_descriptors = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V13_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V12_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V11_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V10_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V9_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V8_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V7_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
|
|
)
|
|
require_persistent_data = any(
|
|
descriptor.is_file() and not descriptor.is_symlink()
|
|
for descriptor in data_descriptors
|
|
)
|
|
expected_core_environment = {
|
|
"DEVICE_MANAGEMENT_API_ENABLED": "true",
|
|
"DEVICE_MANAGEMENT_CORE_TOKEN_FILE": (
|
|
"/run/nodedc-secrets/management-core-token"
|
|
),
|
|
}
|
|
if require_edge_channel:
|
|
expected_core_environment.update({
|
|
"DEVICE_EDGE_CHANNEL_ENABLED": "true",
|
|
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE": (
|
|
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem"
|
|
),
|
|
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE": (
|
|
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem"
|
|
),
|
|
"DEVICE_EDGE_CHANNEL_TRUST_ROOT": (
|
|
"/run/nodedc-secrets/device-edge-channel/peers"
|
|
),
|
|
"DEVICE_EDGE_CHANNEL_MAX_EDGES": "32",
|
|
"DEVICE_EDGE_CHANNEL_RECONCILE_INTERVAL_MS": "15000",
|
|
})
|
|
if any(
|
|
core_environment.get(key) != value
|
|
for key, value in expected_core_environment.items()
|
|
):
|
|
die("Device Control Core management runtime mismatch")
|
|
if not require_edge_channel and any(
|
|
key in core_environment
|
|
for key in (
|
|
"DEVICE_EDGE_CHANNEL_ENABLED",
|
|
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE",
|
|
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE",
|
|
"DEVICE_EDGE_CHANNEL_TRUST_ROOT",
|
|
"DEVICE_EDGE_CHANNEL_MAX_EDGES",
|
|
"DEVICE_EDGE_CHANNEL_RECONCILE_INTERVAL_MS",
|
|
)
|
|
):
|
|
die("Device Manager runtime unexpectedly owns Edge channel state")
|
|
expected_manager_environment = {
|
|
"NODE_ENV": "production",
|
|
"HOST": "0.0.0.0",
|
|
"PORT": "18122",
|
|
"NODEDC_DEVICE_MANAGER_AUTH_REQUIRED": "true",
|
|
"NODEDC_DEVICE_MANAGER_COOKIE_SECURE": "true",
|
|
"NODEDC_DEVICE_MANAGER_LOCAL_PREVIEW": "false",
|
|
"NODEDC_DEVICE_MANAGER_SERVICE_SLUG": "device-core",
|
|
"NODEDC_LAUNCHER_BASE_URL": "https://hub.nodedc.ru",
|
|
"NODEDC_LAUNCHER_INTERNAL_URL": "http://launcher:5173",
|
|
"NODEDC_LAUNCHER_INTERNAL_TOKEN_FILE": (
|
|
"/run/nodedc-secrets/device-core-internal-token"
|
|
),
|
|
"NODEDC_DEVICE_CORE_INTERNAL_URL": (
|
|
"http://device-control-core:18120"
|
|
),
|
|
"NODEDC_DEVICE_CORE_TOKEN_FILE": (
|
|
"/run/nodedc-secrets/management-core-token"
|
|
),
|
|
}
|
|
if require_persistent_data:
|
|
expected_manager_environment.update({
|
|
"NODEDC_DEVICE_MANAGER_PRESENTATION_PATH": (
|
|
DEVICE_PLANE_MANAGER_PRESENTATION_PATH
|
|
),
|
|
"NODEDC_DEVICE_MANAGER_MEDIA_ROOT": (
|
|
DEVICE_PLANE_MANAGER_MEDIA_ROOT
|
|
),
|
|
})
|
|
elif any(
|
|
key in manager_environment
|
|
for key in (
|
|
"NODEDC_DEVICE_MANAGER_PRESENTATION_PATH",
|
|
"NODEDC_DEVICE_MANAGER_MEDIA_ROOT",
|
|
)
|
|
):
|
|
die("Device Manager runtime unexpectedly owns persistent data")
|
|
if any(
|
|
manager_environment.get(key) != value
|
|
for key, value in expected_manager_environment.items()
|
|
):
|
|
die("Device Manager environment mismatch")
|
|
for environment in (core_environment, manager_environment):
|
|
for forbidden in (
|
|
"NODEDC_INTERNAL_ACCESS_TOKEN",
|
|
"NODEDC_PLATFORM_SERVICE_TOKEN",
|
|
"DEVICE_MANAGEMENT_CORE_TOKEN",
|
|
):
|
|
if forbidden in environment:
|
|
die("Device Manager plaintext secret boundary mismatch")
|
|
core_mounts = {
|
|
mount.get("Destination"): mount
|
|
for mount in core.get("Mounts") or []
|
|
}
|
|
core_management_mount = core_mounts.get(
|
|
"/run/nodedc-secrets/management-core-token"
|
|
)
|
|
if (
|
|
core_management_mount is None
|
|
or core_management_mount.get("Type") != "bind"
|
|
or core_management_mount.get("Source")
|
|
!= str(DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE)
|
|
or core_management_mount.get("RW") is not False
|
|
):
|
|
die("Device Control Core management secret mount mismatch")
|
|
expected_core_channel_mounts = {
|
|
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem": (
|
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE
|
|
),
|
|
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem": (
|
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE
|
|
),
|
|
"/run/nodedc-secrets/device-edge-channel/peers": (
|
|
DEVICE_PLANE_EDGE_CHANNEL_PEER_TRUST_DIR
|
|
),
|
|
}
|
|
if require_edge_channel:
|
|
for destination, source in expected_core_channel_mounts.items():
|
|
mount = core_mounts.get(destination)
|
|
if (
|
|
mount is None
|
|
or mount.get("Type") != "bind"
|
|
or mount.get("Source") != str(source)
|
|
or mount.get("RW") is not False
|
|
):
|
|
die("Device Control Core Edge channel mount mismatch")
|
|
elif any(destination in core_mounts for destination in expected_core_channel_mounts):
|
|
die("Device Manager runtime unexpectedly mounts Edge channel state")
|
|
core_networks = set(
|
|
((core.get("NetworkSettings") or {}).get("Networks") or {}).keys()
|
|
)
|
|
if core_network_mode not in (None, "private-egress"):
|
|
die("Device Control Core Edge channel network mode is invalid")
|
|
if core_network_mode == "private-egress" and not require_edge_channel:
|
|
die("Device Control Core private egress requires Edge channel")
|
|
if core_network_mode == "private-egress":
|
|
expected_core_networks = {
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
DEVICE_PLANE_EGRESS_NETWORK,
|
|
}
|
|
else:
|
|
expected_core_networks = {
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
DEVICE_PLANE_CONTROL_NETWORK,
|
|
}
|
|
if require_edge_channel:
|
|
expected_core_networks.add(DEVICE_PLANE_EGRESS_NETWORK)
|
|
if core_networks != expected_core_networks:
|
|
die("Device Control Core Edge channel network boundary mismatch")
|
|
manager_mounts = {
|
|
mount.get("Destination"): mount
|
|
for mount in manager.get("Mounts") or []
|
|
}
|
|
expected_manager_mounts = {
|
|
"/run/nodedc-secrets/device-core-internal-token": (
|
|
PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE
|
|
),
|
|
"/run/nodedc-secrets/management-core-token": (
|
|
DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE
|
|
),
|
|
}
|
|
if require_persistent_data:
|
|
expected_manager_mounts[DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR] = (
|
|
DEVICE_PLANE_MANAGER_DATA_DIR
|
|
)
|
|
if set(manager_mounts) != set(expected_manager_mounts):
|
|
die("Device Manager mount set mismatch")
|
|
for destination, source in expected_manager_mounts.items():
|
|
mount = manager_mounts[destination]
|
|
persistent_mount = destination == DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR
|
|
if (
|
|
mount.get("Type") != "bind"
|
|
or mount.get("Source") != str(source)
|
|
or mount.get("RW") is not persistent_mount
|
|
):
|
|
die("Device Manager mount boundary mismatch")
|
|
if require_persistent_data:
|
|
validate_device_plane_manager_persistent_data_metadata()
|
|
ports = (manager.get("NetworkSettings") or {}).get("Ports") or {}
|
|
if any(bindings for bindings in ports.values()):
|
|
die("Device Manager host port publication is forbidden")
|
|
networks = set(
|
|
((manager.get("NetworkSettings") or {}).get("Networks") or {}).keys()
|
|
)
|
|
if networks != {
|
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
|
"nodedc-platform_edge",
|
|
}:
|
|
die("Device Manager network boundary mismatch")
|
|
validate_device_plane_runtime_secret_metadata(include_management=True)
|
|
if require_edge_channel:
|
|
if inspect_device_edge_channel_core_identity_state() != "valid-reuse-at-apply":
|
|
die("Device Edge channel Core identity is not active")
|
|
validate_device_edge_channel_public_export()
|
|
ensure_platform_runtime_secret(
|
|
PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"Device Core Hub handoff",
|
|
)
|
|
return "exact"
|
|
|
|
|
|
def validate_device_plane_foundation_installed_source():
|
|
failed_artifact = FAILED_DIR / DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-plane-installed-foundation-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id")
|
|
!= DEVICE_PLANE_FOUNDATION_FAILED_PATCH_ID
|
|
or tuple(failed_entries) != DEVICE_PLANE_FOUNDATION_ENTRIES
|
|
):
|
|
die("Device Plane failed foundation source contract mismatch")
|
|
expected = collect_exact_files(
|
|
failed_payload,
|
|
DEVICE_PLANE_FOUNDATION_ENTRIES,
|
|
"Device Plane failed foundation source",
|
|
)
|
|
actual = collect_exact_files(
|
|
component_root("device-plane"),
|
|
DEVICE_PLANE_FOUNDATION_ENTRIES,
|
|
"Device Plane installed foundation source",
|
|
)
|
|
if actual != expected:
|
|
die("Device Plane installed foundation source mismatch")
|
|
descriptor = read_strict_json(
|
|
component_root("device-plane")
|
|
/ DEVICE_PLANE_FOUNDATION_RECOVERY_REL,
|
|
"installed Device Plane foundation recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_device_plane_foundation_recovery_descriptor():
|
|
die("installed Device Plane foundation recovery descriptor mismatch")
|
|
return actual
|
|
|
|
|
|
def validate_device_plane_foundation_network_publication_installed_source():
|
|
failed_artifact = (
|
|
FAILED_DIR / DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT
|
|
)
|
|
comparable_entries = tuple(
|
|
rel
|
|
for rel in DEVICE_PLANE_FOUNDATION_ENTRIES
|
|
if rel != "docker-compose.device-plane.yml"
|
|
)
|
|
with tempfile.TemporaryDirectory(
|
|
prefix="device-plane-installed-network-publication-",
|
|
dir=TMP_DIR,
|
|
) as directory:
|
|
failed_manifest, failed_entries, failed_payload = load_artifact(
|
|
failed_artifact,
|
|
Path(directory),
|
|
)
|
|
if (
|
|
failed_manifest.get("id")
|
|
!= DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_PATCH_ID
|
|
or tuple(failed_entries)
|
|
!= DEVICE_PLANE_FOUNDATION_RECOVERY_ENTRIES
|
|
):
|
|
die("Device Plane failed recovery source contract mismatch")
|
|
expected = collect_exact_files(
|
|
failed_payload,
|
|
comparable_entries,
|
|
"Device Plane failed recovery source",
|
|
)
|
|
actual = collect_exact_files(
|
|
component_root("device-plane"),
|
|
comparable_entries,
|
|
"Device Plane installed network-publication source",
|
|
)
|
|
if actual != expected:
|
|
die("Device Plane installed network-publication source mismatch")
|
|
compose = (
|
|
component_root("device-plane")
|
|
/ "docker-compose.device-plane.yml"
|
|
)
|
|
if (
|
|
compose.is_symlink()
|
|
or not compose.is_file()
|
|
or sha256_file(compose)
|
|
!= DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_COMPOSE_SHA256
|
|
):
|
|
die("Device Plane installed network-publication Compose mismatch")
|
|
descriptor = read_strict_json(
|
|
component_root("device-plane")
|
|
/ DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_REL,
|
|
"installed Device Plane network-publication descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
descriptor
|
|
!= expected_device_plane_foundation_network_publication_descriptor()
|
|
):
|
|
die(
|
|
"installed Device Plane network-publication descriptor mismatch"
|
|
)
|
|
recovery_descriptor = (
|
|
component_root("device-plane")
|
|
/ DEVICE_PLANE_FOUNDATION_RECOVERY_REL
|
|
)
|
|
if recovery_descriptor.exists() or recovery_descriptor.is_symlink():
|
|
die("terminal Device Plane recovery descriptor is installed")
|
|
return actual
|
|
|
|
|
|
def component_compose_root(component):
|
|
return COMPONENTS[component].get("compose_root", component_root(component))
|
|
|
|
|
|
def component_compose_project(component):
|
|
return COMPONENTS[component].get("compose_project")
|
|
|
|
|
|
def component_artifact_only(component):
|
|
return bool(COMPONENTS[component].get("artifact_only"))
|
|
|
|
|
|
def touches_engine_credential_sink(component, entries):
|
|
if component != "engine" or entries is None:
|
|
return False
|
|
return any(
|
|
rel == "nodedc-source/server/credentialSink"
|
|
or rel.startswith("nodedc-source/server/credentialSink/")
|
|
or rel == "nodedc-source/server/routes/engineCredentialSink.js"
|
|
for rel in entries
|
|
)
|
|
|
|
|
|
# Frozen compatibility predicate retained because the last successful Engine
|
|
# credential runtime canon includes it in run_compose(). The corresponding
|
|
# Platform service is not an allowed artifact path and has no deploy branch.
|
|
def touches_engine_credential_provisioner(component, entries):
|
|
return False
|
|
|
|
|
|
def touches_external_data_plane_files(entries):
|
|
runtime_contract_files = {
|
|
"platform/packages/external-provider-contract/package.json",
|
|
"platform/packages/external-provider-contract/src/contract-version.mjs",
|
|
"platform/packages/external-provider-contract/src/data-plane.mjs",
|
|
"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/sensitive-field-policy.mjs",
|
|
}
|
|
return any(
|
|
rel == "platform/services/external-data-plane"
|
|
or rel.startswith("platform/services/external-data-plane/")
|
|
or rel in runtime_contract_files
|
|
or rel == "platform/docker-compose.external-data-plane.yml"
|
|
for rel in entries
|
|
)
|
|
|
|
|
|
def touches_engine_data_product_publish_grant(entries):
|
|
if entries is None:
|
|
return False
|
|
return any(
|
|
rel == "nodedc-source/server/dataProductPublishGrant"
|
|
or rel.startswith("nodedc-source/server/dataProductPublishGrant/")
|
|
for rel in entries
|
|
)
|
|
|
|
|
|
def is_engine_data_product_publish_grant_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_composite_provider_v4_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_provider_rotating_slot_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_provider_authority_diagnostics_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_depttrans_zone_authority_v1_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_provider_target_host_policy_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_execution_profile_decoder_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_MCP_EXECUTION_PROFILE_DECODER_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_telemetry_catalog_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_MCP_TELEMETRY_CATALOG_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_execution_plan_materialization_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_execution_plan_telemetry_runtime_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_execution_plan_module_ownership_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_normalized_identity_search_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_l1_credential_reuse_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_MCP_L1_CREDENTIAL_REUSE_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_l1_credential_provenance_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_execution_plan_sandbox_runtime_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_gelios_items_envelope_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_registered_execution_profiles_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_mcp_gelios_units_items_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries)
|
|
== ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES
|
|
)
|
|
|
|
|
|
def is_engine_agent_full_grant_migration_slice(component, entries):
|
|
return (
|
|
component == "engine"
|
|
and entries is not None
|
|
and tuple(entries) == ENGINE_AGENT_FULL_GRANT_MIGRATION_ARTIFACT_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):
|
|
files = {}
|
|
for rel in entries:
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"{label} file 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 validate_installed_engine_data_product_publish_grant_foundation(root):
|
|
# The original credential-sink baseline remains immutable. The two route
|
|
# files below became Publish-grant-owned when the initial transition was
|
|
# applied, so their current contract is validated semantically instead of
|
|
# comparing them to their pre-transition bytes.
|
|
publish_owned_predecessors = {
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/routes/n8n.js",
|
|
}
|
|
for rel, expected_sha256 in ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256.items():
|
|
if rel in publish_owned_predecessors:
|
|
continue
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"installed Engine data product publish grant foundation is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"installed Engine data product publish grant foundation is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"installed Engine data product publish grant foundation drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
|
|
grant_dir = root / "nodedc-source/server/dataProductPublishGrant"
|
|
try:
|
|
grant_stat = grant_dir.lstat()
|
|
except FileNotFoundError:
|
|
die("installed Engine data product publish grant source is missing")
|
|
if stat.S_ISLNK(grant_stat.st_mode) or not stat.S_ISDIR(grant_stat.st_mode):
|
|
die("installed Engine data product publish grant source is unsafe")
|
|
installed_names = []
|
|
for child in grant_dir.iterdir():
|
|
child_stat = child.lstat()
|
|
if stat.S_ISLNK(child_stat.st_mode) or not stat.S_ISREG(child_stat.st_mode):
|
|
die(f"installed Engine data product publish grant source is unsafe: {child.name}")
|
|
installed_names.append(child.name)
|
|
if tuple(sorted(installed_names)) != ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_FILES:
|
|
die("installed Engine data product publish grant source set mismatch")
|
|
|
|
required_files = (
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/engineAgents/store.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/routes/n8n.js",
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL,
|
|
)
|
|
for rel in required_files:
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"installed Engine data product publish grant file is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"installed Engine data product publish grant file is unsafe: {rel}")
|
|
|
|
try:
|
|
override_text = (root / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL).read_text(
|
|
encoding="utf-8"
|
|
)
|
|
gateway = (root / "nodedc-source/server/routes/engineAgentGateway.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
n8n_route = (root / "nodedc-source/server/routes/n8n.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
except (OSError, UnicodeDecodeError):
|
|
die("installed Engine data product publish grant source is unreadable")
|
|
if override_text != expected_engine_data_product_publish_grant_override():
|
|
die("installed Engine data product publish grant override drift detected")
|
|
if "from '../credentialSink/" in n8n_route:
|
|
die("installed Engine data product publish grant depends on the legacy sink")
|
|
for adapter in (
|
|
"engineCredentialSinkN8nAdapter",
|
|
"engineDataProductPublishGrantN8nAdapter",
|
|
):
|
|
if adapter not in n8n_route:
|
|
die(f"installed Engine data product publish grant adapter is missing: {adapter}")
|
|
for tool in (
|
|
"engine_plan_data_product_publish_grant",
|
|
"engine_apply_data_product_publish_grant",
|
|
"engine_accept_data_product_publish_grant",
|
|
"engine_rollback_data_product_publish_grant",
|
|
):
|
|
if tool not in gateway:
|
|
die(f"installed Engine data product publish grant tool is missing: {tool}")
|
|
|
|
return sha256_file(root / "docker-compose.yml")
|
|
|
|
|
|
def preflight_engine_data_product_publish_grant_predecessor(payload_dir=None):
|
|
root = component_root("engine")
|
|
installed_override = root / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL
|
|
if installed_override.exists() or installed_override.is_symlink():
|
|
compose_sha256 = validate_installed_engine_data_product_publish_grant_foundation(root)
|
|
if payload_dir is None:
|
|
die("Engine data product publish grant update candidate is required")
|
|
installed_files = collect_engine_data_product_publish_grant_files(
|
|
root,
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES,
|
|
"installed Engine data product publish grant",
|
|
)
|
|
candidate_files = collect_engine_data_product_publish_grant_files(
|
|
payload_dir,
|
|
ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES,
|
|
"candidate Engine data product publish grant",
|
|
)
|
|
if set(installed_files) != set(candidate_files):
|
|
die("Engine data product publish grant update file set mismatch")
|
|
changed_paths = tuple(sorted(
|
|
rel
|
|
for rel, candidate_sha256 in candidate_files.items()
|
|
if installed_files[rel] != candidate_sha256
|
|
))
|
|
if not changed_paths:
|
|
die("Engine data product publish grant update contains no source change")
|
|
forbidden_changes = tuple(
|
|
rel
|
|
for rel in changed_paths
|
|
if (
|
|
not rel.startswith(ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_PREFIX)
|
|
and rel != ENGINE_PROVIDER_SECURITY_CATALOG_REL
|
|
)
|
|
)
|
|
if forbidden_changes:
|
|
die(
|
|
"Engine data product publish grant update crosses its source boundary: "
|
|
f"path={forbidden_changes[0]}"
|
|
)
|
|
if ENGINE_PROVIDER_SECURITY_CATALOG_REL in changed_paths:
|
|
installed_catalog_sha256 = installed_files[ENGINE_PROVIDER_SECURITY_CATALOG_REL]
|
|
candidate_catalog_sha256 = candidate_files[ENGINE_PROVIDER_SECURITY_CATALOG_REL]
|
|
if (
|
|
installed_catalog_sha256
|
|
!= ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_PREDECESSOR_SHA256
|
|
or candidate_catalog_sha256
|
|
!= ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256
|
|
):
|
|
die("Engine composite provider catalog transition mismatch")
|
|
if changed_paths != ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CHANGED_PATHS:
|
|
die("Engine composite provider update exact changed path set mismatch")
|
|
mode = "installed-composite-provider-update"
|
|
else:
|
|
mode = "installed-source-update"
|
|
return {
|
|
"mode": mode,
|
|
"compose_sha256": compose_sha256,
|
|
"changed_paths": changed_paths,
|
|
}
|
|
|
|
actual = {}
|
|
for rel, expected_sha256 in ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine data product publish grant predecessor file is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine data product publish grant predecessor file is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine data product publish grant predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
return {
|
|
"mode": "credential-sink-initial-transition",
|
|
"compose_sha256": actual["docker-compose.yml"],
|
|
"changed_paths": (),
|
|
}
|
|
|
|
|
|
def preflight_engine_composite_provider_v4_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine composite provider v4 predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine composite provider v4 predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine composite provider v4 predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine composite provider v4 requires the active immutable backend")
|
|
return {
|
|
"mode": "exact-composite-provider-v3-to-v4",
|
|
"predecessor_sha256": actual,
|
|
"target_sha256": dict(ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_provider_rotating_slot_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in ENGINE_PROVIDER_ROTATING_SLOT_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine provider rotating slot predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine provider rotating slot predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine provider rotating slot predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine provider rotating slot requires the active immutable backend")
|
|
return {
|
|
"mode": "exact-gelios-v4-credential-slot-alignment",
|
|
"predecessor_sha256": actual,
|
|
"target_sha256": dict(ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_provider_authority_diagnostics_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine provider authority diagnostics predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine provider authority diagnostics predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine provider authority diagnostics predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine provider authority diagnostics requires the active immutable backend")
|
|
return {
|
|
"mode": "private-node-name-reconciliation",
|
|
"predecessor_sha256": actual,
|
|
"target_sha256": dict(ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_depttrans_zone_authority_v1_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine Depttrans zone authority v1 predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine Depttrans zone authority v1 predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine Depttrans zone authority v1 predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
for rel in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(f"Engine Depttrans zone authority v1 new path already exists: {rel}")
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine Depttrans zone authority v1 requires the active immutable backend")
|
|
return {
|
|
"mode": "exact-platform-service-authority-v1",
|
|
"predecessor_sha256": actual,
|
|
"target_sha256": dict(ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def print_engine_depttrans_zone_authority_v1_plan(preflight):
|
|
print("engine_provider_authority_transition=platform-service-v1")
|
|
print("engine_provider_package=moscow-department-of-transport.pmd-slow-zones.v1")
|
|
print("engine_provider_id=moscow-department-of-transport")
|
|
print("engine_provider_authority_boundary=platform-service")
|
|
print("engine_provider_platform_service=nodedc-map-gateway")
|
|
print("engine_provider_platform_network=engine")
|
|
print("engine_data_product=map.zones.current.v2")
|
|
print("provider_credential_values=preserved")
|
|
predecessor_sha256 = preflight["predecessor_sha256"]
|
|
target_sha256 = preflight["target_sha256"]
|
|
for changed_path in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES:
|
|
print(f"engine_depttrans_zone_authority_changed_path={changed_path}")
|
|
if changed_path in predecessor_sha256:
|
|
print(
|
|
f"engine_depttrans_zone_authority_predecessor_sha256[{changed_path}]="
|
|
f"{predecessor_sha256[changed_path]}"
|
|
)
|
|
elif changed_path in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_NEW_PATHS:
|
|
print(
|
|
f"engine_depttrans_zone_authority_predecessor_state[{changed_path}]="
|
|
"absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine Depttrans zone authority v1 plan has no predecessor state: "
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path not in target_sha256:
|
|
die(
|
|
"Engine Depttrans zone authority v1 plan has no target sha256: "
|
|
f"{changed_path}"
|
|
)
|
|
print(
|
|
f"engine_depttrans_zone_authority_target_sha256[{changed_path}]="
|
|
f"{target_sha256[changed_path]}"
|
|
)
|
|
print(f"backend_current_barrier={preflight['backend_mode']}")
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("mcp_nginx=untouched")
|
|
|
|
|
|
def preflight_engine_provider_target_host_policy_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in ENGINE_PROVIDER_TARGET_HOST_POLICY_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine provider target host policy predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine provider target host policy predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine provider target host policy predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine provider target host policy requires the active immutable backend")
|
|
return {
|
|
"mode": "exact-provider-literal-target-host",
|
|
"predecessor_sha256": actual,
|
|
"target_sha256": dict(ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_execution_profile_decoder_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP execution profile decoder predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine MCP execution profile decoder predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP execution profile decoder predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
for rel in ENGINE_MCP_EXECUTION_PROFILE_DECODER_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(f"Engine MCP execution profile decoder new path already exists: {rel}")
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine MCP execution profile decoder requires the active immutable backend")
|
|
return {
|
|
"mode": "exact-flatted-numeric-string-preservation",
|
|
"predecessor_sha256": actual,
|
|
"target_sha256": dict(ENGINE_MCP_EXECUTION_PROFILE_DECODER_TARGET_SHA256),
|
|
"new_paths": tuple(ENGINE_MCP_EXECUTION_PROFILE_DECODER_NEW_PATHS),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_telemetry_catalog_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in ENGINE_MCP_TELEMETRY_CATALOG_PREDECESSOR_SHA256.items():
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP telemetry catalog predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine MCP telemetry catalog predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP telemetry catalog predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
foundation = {}
|
|
for rel, expected_sha256 in ENGINE_MCP_TELEMETRY_CATALOG_FOUNDATION_SHA256.items():
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP telemetry catalog foundation is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine MCP telemetry catalog foundation drift detected: {rel}")
|
|
foundation[rel] = expected_sha256
|
|
for rel in ENGINE_MCP_TELEMETRY_CATALOG_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(f"Engine MCP telemetry catalog new path already exists: {rel}")
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine MCP telemetry catalog requires the active immutable backend")
|
|
return {
|
|
"mode": "execution-profile-decoder-v1-to-telemetry-catalog-v1",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256),
|
|
"new_paths": tuple(ENGINE_MCP_TELEMETRY_CATALOG_NEW_PATHS),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_execution_plan_materialization_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan materialization predecessor is "
|
|
f"missing: {rel}"
|
|
)
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(
|
|
"Engine MCP execution plan materialization predecessor is "
|
|
f"unsafe: {rel}"
|
|
)
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP execution plan materialization predecessor drift "
|
|
f"detected: path={rel} expected={expected_sha256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan materialization foundation is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP execution plan materialization foundation drift "
|
|
f"detected: {rel}"
|
|
)
|
|
foundation[rel] = expected_sha256
|
|
for rel in ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP execution plan materialization new path already "
|
|
f"exists: {rel}"
|
|
)
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution plan materialization requires the active "
|
|
"immutable backend"
|
|
)
|
|
return {
|
|
"mode": "telemetry-catalog-v1-to-execution-plan-materialization-v1",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_NEW_PATHS),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_execution_plan_telemetry_runtime_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime predecessor is "
|
|
f"missing: {rel}"
|
|
)
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime predecessor is "
|
|
f"unsafe: {rel}"
|
|
)
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime predecessor drift "
|
|
f"detected: path={rel} expected={expected_sha256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime foundation is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime foundation drift "
|
|
f"detected: {rel}"
|
|
)
|
|
foundation[rel] = expected_sha256
|
|
for rel in ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime new path already "
|
|
f"exists: {rel}"
|
|
)
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime requires the active "
|
|
"immutable backend"
|
|
)
|
|
return {
|
|
"mode": "execution-plan-materialization-v1-to-telemetry-runtime-v2",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_NEW_PATHS
|
|
),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_execution_plan_module_ownership_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan module ownership predecessor is "
|
|
f"missing: {rel}"
|
|
)
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(
|
|
"Engine MCP execution plan module ownership predecessor is "
|
|
f"unsafe: {rel}"
|
|
)
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP execution plan module ownership predecessor drift "
|
|
f"detected: path={rel} expected={expected_sha256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan module ownership foundation is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP execution plan module ownership foundation drift "
|
|
f"detected: {rel}"
|
|
)
|
|
foundation[rel] = expected_sha256
|
|
for rel in ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP execution plan module ownership new path already "
|
|
f"exists: {rel}"
|
|
)
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution plan module ownership requires the active "
|
|
"immutable backend"
|
|
)
|
|
return {
|
|
"mode":
|
|
"execution-plan-telemetry-runtime-v2-to-module-ownership-v3",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_NEW_PATHS
|
|
),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_normalized_identity_search_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP normalized identity search predecessor is "
|
|
f"missing: {rel}"
|
|
)
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(
|
|
"Engine MCP normalized identity search predecessor is "
|
|
f"unsafe: {rel}"
|
|
)
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP normalized identity search predecessor drift "
|
|
f"detected: path={rel} expected={expected_sha256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP normalized identity search foundation is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP normalized identity search foundation drift "
|
|
f"detected: {rel}"
|
|
)
|
|
foundation[rel] = expected_sha256
|
|
|
|
for rel in ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP normalized identity search new path already "
|
|
f"exists: {rel}"
|
|
)
|
|
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP normalized identity search requires the active "
|
|
"immutable backend"
|
|
)
|
|
return {
|
|
"mode": "classified-aspects-v1-to-normalized-identity-search-v1",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_NEW_PATHS),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_l1_credential_reuse_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP L1 credential reuse predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine MCP L1 credential reuse predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP L1 credential reuse predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP L1 credential reuse foundation is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine MCP L1 credential reuse foundation drift detected: {rel}")
|
|
foundation[rel] = expected_sha256
|
|
|
|
for rel in ENGINE_MCP_L1_CREDENTIAL_REUSE_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP L1 credential reuse new path already exists: "
|
|
f"{rel}"
|
|
)
|
|
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP L1 credential reuse requires the active immutable "
|
|
"backend"
|
|
)
|
|
return {
|
|
"mode": "normalized-identity-search-v1-to-l1-credential-reuse-v1",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(ENGINE_MCP_L1_CREDENTIAL_REUSE_TARGET_SHA256),
|
|
"new_paths": tuple(ENGINE_MCP_L1_CREDENTIAL_REUSE_NEW_PATHS),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_l1_credential_provenance_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP L1 credential provenance predecessor is missing: "
|
|
f"{rel}"
|
|
)
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(
|
|
"Engine MCP L1 credential provenance predecessor is unsafe: "
|
|
f"{rel}"
|
|
)
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP L1 credential provenance predecessor drift "
|
|
f"detected: path={rel} expected={expected_sha256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP L1 credential provenance foundation is missing: "
|
|
f"{rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP L1 credential provenance foundation drift "
|
|
f"detected: {rel}"
|
|
)
|
|
foundation[rel] = expected_sha256
|
|
|
|
for rel in ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP L1 credential provenance new path already exists: "
|
|
f"{rel}"
|
|
)
|
|
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP L1 credential provenance requires the active "
|
|
"immutable backend"
|
|
)
|
|
return {
|
|
"mode": "l1-credential-reuse-v1-to-provenance-v2",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_NEW_PATHS),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_execution_plan_sandbox_runtime_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime predecessor is "
|
|
f"missing: {rel}"
|
|
)
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime predecessor is "
|
|
f"unsafe: {rel}"
|
|
)
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime predecessor drift "
|
|
f"detected: path={rel} expected={expected_sha256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime foundation is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime foundation drift "
|
|
f"detected: {rel}"
|
|
)
|
|
foundation[rel] = expected_sha256
|
|
|
|
for rel in ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime new path already "
|
|
f"exists: {rel}"
|
|
)
|
|
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime requires the active "
|
|
"immutable backend"
|
|
)
|
|
return {
|
|
"mode": "l1-credential-provenance-v2-to-sandbox-runtime-v4",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_NEW_PATHS
|
|
),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_gelios_items_envelope_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP Gelios items envelope predecessor is missing: {rel}")
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine MCP Gelios items envelope predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP Gelios items envelope predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Engine MCP Gelios items envelope foundation is missing: {rel}")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine MCP Gelios items envelope foundation drift: {rel}")
|
|
foundation[rel] = expected_sha256
|
|
|
|
for rel in ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP Gelios items envelope new path already exists: "
|
|
f"{rel}"
|
|
)
|
|
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP Gelios items envelope requires the active immutable "
|
|
"backend"
|
|
)
|
|
return {
|
|
"mode": "gelios-provider-v11-to-v12-items-envelope",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_NEW_PATHS),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_registered_execution_profiles_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP registered execution profiles predecessor is "
|
|
f"missing: {rel}"
|
|
)
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(
|
|
"Engine MCP registered execution profiles predecessor is "
|
|
f"unsafe: {rel}"
|
|
)
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP registered execution profiles predecessor drift "
|
|
f"detected: path={rel} expected={expected_sha256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP registered execution profiles foundation is "
|
|
f"missing: {rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(
|
|
"Engine MCP registered execution profiles foundation drift: "
|
|
f"{rel}"
|
|
)
|
|
foundation[rel] = expected_sha256
|
|
|
|
for rel in (
|
|
*ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS,
|
|
*ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FAILED_PATHS,
|
|
):
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP registered execution profiles new path already "
|
|
f"exists: {rel}"
|
|
)
|
|
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP registered execution profiles requires the active "
|
|
"immutable backend"
|
|
)
|
|
return {
|
|
"mode":
|
|
"gelios-items-envelope-v12-to-registered-profiles-v2-attested",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS
|
|
),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_mcp_gelios_units_items_predecessor():
|
|
root = component_root("engine")
|
|
actual = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP Gelios units items predecessor is missing: "
|
|
f"{rel}"
|
|
)
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode):
|
|
die(f"Engine MCP Gelios units items predecessor is unsafe: {rel}")
|
|
actual_sha256 = sha256_file(path)
|
|
if actual_sha256 != expected_sha256:
|
|
die(
|
|
"Engine MCP Gelios units items predecessor drift detected: "
|
|
f"path={rel} expected={expected_sha256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
actual[rel] = actual_sha256
|
|
|
|
foundation = {}
|
|
for rel, expected_sha256 in (
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256.items()
|
|
):
|
|
path = root / rel
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(
|
|
"Engine MCP Gelios units items foundation is missing: "
|
|
f"{rel}"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or sha256_file(path) != expected_sha256
|
|
):
|
|
die(f"Engine MCP Gelios units items foundation drift: {rel}")
|
|
foundation[rel] = expected_sha256
|
|
|
|
for rel in ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS:
|
|
path = root / rel
|
|
if path.exists() or path.is_symlink():
|
|
die(
|
|
"Engine MCP Gelios units items new path already exists: "
|
|
f"{rel}"
|
|
)
|
|
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP Gelios units items requires the active immutable "
|
|
"backend"
|
|
)
|
|
return {
|
|
"mode":
|
|
"registered-profiles-v2-to-gelios-v12.0.1-units-items-envelope",
|
|
"predecessor_sha256": actual,
|
|
"foundation_sha256": foundation,
|
|
"target_sha256": dict(
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256
|
|
),
|
|
"new_paths": tuple(ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS),
|
|
"backend_mode": backend["mode"],
|
|
}
|
|
|
|
|
|
def preflight_engine_agent_full_grant_migration_predecessor():
|
|
root = component_root("engine")
|
|
store_path = root / ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL
|
|
try:
|
|
store_stat = store_path.lstat()
|
|
except FileNotFoundError:
|
|
die("Engine agent full grant migration predecessor is missing")
|
|
if stat.S_ISLNK(store_stat.st_mode) or not stat.S_ISREG(store_stat.st_mode):
|
|
die("Engine agent full grant migration predecessor is unsafe")
|
|
actual_sha256 = sha256_file(store_path)
|
|
if actual_sha256 != ENGINE_AGENT_FULL_GRANT_MIGRATION_PREDECESSOR_SHA256:
|
|
die(
|
|
"Engine agent full grant migration predecessor drift detected: "
|
|
f"expected={ENGINE_AGENT_FULL_GRANT_MIGRATION_PREDECESSOR_SHA256} "
|
|
f"actual={actual_sha256}"
|
|
)
|
|
|
|
publish_override = root / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL
|
|
try:
|
|
publish_stat = publish_override.lstat()
|
|
publish_text = publish_override.read_text(encoding="utf-8")
|
|
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
die("Engine agent full grant migration requires the installed Publish overlay")
|
|
if (
|
|
stat.S_ISLNK(publish_stat.st_mode)
|
|
or not stat.S_ISREG(publish_stat.st_mode)
|
|
or publish_text != expected_engine_data_product_publish_grant_override()
|
|
):
|
|
die("Engine agent full grant migration Publish overlay mismatch")
|
|
return actual_sha256
|
|
|
|
|
|
def is_platform_provider_catalog_only(entries):
|
|
prefix = "platform/packages/external-provider-contract/providers/"
|
|
return bool(entries) and all(rel.startswith(prefix) for rel in entries)
|
|
|
|
|
|
def component_services(component, entries=None):
|
|
if is_device_plane_control_core_release_slice(component, entries):
|
|
return ("device-control-core",)
|
|
|
|
if is_device_plane_control_core_v3_reconciliation_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ("device-control-core",)
|
|
|
|
if is_device_plane_control_core_incident_audit_slice(component, entries):
|
|
return ()
|
|
|
|
if is_device_plane_control_core_migration_replay_audit_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ()
|
|
|
|
if is_device_plane_control_core_migration_replay_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ("device-control-core",)
|
|
|
|
if is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ("device-control-core",)
|
|
|
|
if is_device_plane_manager_only_release_slice(component, entries):
|
|
return ("device-manager",)
|
|
|
|
if is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
|
|
return ("device-control-core",)
|
|
|
|
if is_device_plane_backhaul_vps_enrollment_slice(component, entries):
|
|
return (DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,)
|
|
|
|
if is_device_plane_backhaul_target_slice(component, entries):
|
|
return (DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,)
|
|
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
# Reconciliation writes only the reviewed marker after proving the
|
|
# failed archive, journal, backup, restored source and live runtime.
|
|
return ()
|
|
|
|
if is_device_plane_manager_reconciliation_slice(component, entries):
|
|
# Reconciliation publishes one marker after proving the failed
|
|
# evidence and the already-restored baseline. Runtime is read-only.
|
|
return ()
|
|
|
|
if is_device_plane_manager_v2_reconciliation_slice(component, entries):
|
|
# The failed v2 apply already restored the prior source and later
|
|
# converged to the healthy baseline. Publish evidence only.
|
|
return ()
|
|
|
|
if is_device_plane_postgres_bootstrap_slice(component, entries):
|
|
# This exact one-time transition is the only Device Plane artifact that
|
|
# may select durable state. Its preflight requires both container and
|
|
# named volume to be absent, so --force-recreate cannot touch an
|
|
# installed database.
|
|
return ("device-postgres",)
|
|
|
|
if is_device_plane_foundation_recovery_slice(component, entries):
|
|
# The failed apply already built and started the exact reviewed images.
|
|
# Recovery publishes their matching source and accepts the current
|
|
# runtime without build/recreate/restart.
|
|
return ()
|
|
|
|
if is_device_plane_foundation_network_publication_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
# Recreate only the two stateless services from their exact existing
|
|
# image IDs. PostgreSQL is a preserved prerequisite, never selected.
|
|
return ("device-control-core", "device-gateway")
|
|
|
|
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 (
|
|
is_engine_mcp_control_plane_slice(component, entries)
|
|
or is_engine_mcp_ontology_sdk_slice(component, entries)
|
|
or is_engine_mcp_autonomy_provider_v5_slice(component, entries)
|
|
or is_engine_composite_provider_v4_slice(component, entries)
|
|
or is_engine_provider_rotating_slot_slice(component, entries)
|
|
or is_engine_provider_authority_diagnostics_slice(component, entries)
|
|
or is_engine_depttrans_zone_authority_v1_slice(component, entries)
|
|
or is_engine_provider_target_host_policy_slice(component, entries)
|
|
or is_engine_mcp_execution_profile_decoder_slice(component, entries)
|
|
or is_engine_mcp_telemetry_catalog_slice(component, entries)
|
|
or is_engine_mcp_execution_plan_materialization_slice(component, entries)
|
|
or is_engine_mcp_execution_plan_telemetry_runtime_slice(component, entries)
|
|
or is_engine_mcp_execution_plan_module_ownership_slice(component, entries)
|
|
or is_engine_mcp_normalized_identity_search_slice(component, entries)
|
|
or is_engine_mcp_l1_credential_reuse_slice(component, entries)
|
|
or is_engine_mcp_l1_credential_provenance_slice(component, entries)
|
|
or is_engine_mcp_execution_plan_sandbox_runtime_slice(component, entries)
|
|
or is_engine_mcp_gelios_items_envelope_slice(component, entries)
|
|
or is_engine_mcp_registered_execution_profiles_slice(component, entries)
|
|
or is_engine_mcp_gelios_units_items_slice(component, entries)
|
|
or is_engine_provider_security_catalog_slice(component, entries)
|
|
):
|
|
# This slice updates only the existing Engine backend control plane.
|
|
# Node intelligence keeps the same immutable sidecar image and n8n/L1
|
|
# retain their current generations.
|
|
return ("nodedc-backend",)
|
|
|
|
if is_engine_node_intelligence_transition(component, entries):
|
|
if tuple(entries) == ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES:
|
|
return (ENGINE_NODE_INTELLIGENCE_SERVICE, "nodedc-backend")
|
|
return ("nodedc-backend",)
|
|
|
|
if is_engine_n8n_transition(component, entries):
|
|
# The actual Engine topology has one n8n process and no separately
|
|
# deployed worker/webhook services. The release barrier therefore
|
|
# force-recreates exactly this service and never touches its database.
|
|
return ("n8n",)
|
|
|
|
if touches_engine_credential_sink(component, entries):
|
|
# The sink is served by the existing Engine backend. It has no frontend
|
|
# or n8n process code, so preserve those generations during this slice.
|
|
return ("nodedc-backend",)
|
|
if is_engine_data_product_publish_grant_slice(component, entries):
|
|
# Publish-grant source and its fixed runtime overlay are backend-owned.
|
|
# n8n, the UI and every database retain their existing generations.
|
|
return ("nodedc-backend",)
|
|
if is_engine_agent_full_grant_migration_slice(component, entries):
|
|
# This one-time compatibility migration changes only Engine Agent
|
|
# authorization normalization inside the already-active backend.
|
|
return ("nodedc-backend",)
|
|
|
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
|
return ("device-control-core", "device-manager")
|
|
|
|
if component == "device-plane" and entries is not None:
|
|
selected = []
|
|
|
|
def add(*services):
|
|
for service in services:
|
|
if service not in selected:
|
|
selected.append(service)
|
|
|
|
touches_common = any(
|
|
rel in (
|
|
".dockerignore",
|
|
"package.json",
|
|
"package-lock.json",
|
|
"docker-compose.device-plane.yml",
|
|
"packages/device-protocol-contract",
|
|
"packages/arusnavi-b2-adapter",
|
|
)
|
|
or rel.startswith((
|
|
"packages/device-protocol-contract/",
|
|
"packages/arusnavi-b2-adapter/",
|
|
))
|
|
for rel in entries
|
|
)
|
|
touches_core = any(
|
|
rel == "services/device-control-core"
|
|
or rel.startswith("services/device-control-core/")
|
|
for rel in entries
|
|
)
|
|
touches_gateway = any(
|
|
rel == "services/device-gateway"
|
|
or rel.startswith("services/device-gateway/")
|
|
for rel in entries
|
|
)
|
|
if touches_common or touches_core:
|
|
add("device-control-core")
|
|
if touches_common or touches_gateway:
|
|
add("device-gateway")
|
|
return tuple(selected)
|
|
|
|
if component == "dc-cms" and entries is not None:
|
|
selected = []
|
|
|
|
def add(*services):
|
|
for service in services:
|
|
if service not in selected:
|
|
selected.append(service)
|
|
|
|
touches_compose = any(rel == "infra/docker-compose.yml" for rel in entries)
|
|
touches_proxy = any(rel == "infra/reverse-proxy" or rel.startswith("infra/reverse-proxy/") for rel in entries)
|
|
touches_authentik = any(rel == "infra/authentik" or rel.startswith("infra/authentik/") for rel in entries)
|
|
touches_app = any(
|
|
rel in ("Dockerfile", "package.json", "package-lock.json", "README.md")
|
|
or rel.startswith(("admin/", "projects/", "server/"))
|
|
for rel in entries
|
|
)
|
|
|
|
if touches_compose:
|
|
return COMPONENTS[component]["services"]
|
|
if touches_authentik:
|
|
add("postgresql-authentik", "authentik-server", "authentik-worker", "authentik-bootstrap", "cms-app", "reverse-proxy")
|
|
if touches_app:
|
|
add("cms-app")
|
|
if touches_proxy:
|
|
add("reverse-proxy")
|
|
if selected:
|
|
return tuple(selected)
|
|
|
|
if component == "tasker" and entries is not None:
|
|
selected = ["api", "worker", "beat-worker", "web"]
|
|
touches_compose = any(
|
|
rel in ("plane-app/docker-compose.yaml", "plane-app/docker-compose.synology.override.yml")
|
|
for rel in entries
|
|
)
|
|
touches_proxy = any(rel == "plane-src/apps/proxy/Caddyfile.ce" for rel in entries)
|
|
if touches_compose or touches_proxy:
|
|
selected.append("proxy")
|
|
return tuple(selected)
|
|
|
|
if is_platform_device_core_hub_trust_slice(component, entries):
|
|
return ("launcher",)
|
|
|
|
if is_platform_device_manager_public_route_slice(component, entries):
|
|
return ("reverse-proxy",)
|
|
|
|
if component == "platform" and entries is not None:
|
|
if is_platform_provider_catalog_only(entries):
|
|
return ()
|
|
selected = []
|
|
|
|
def add(*services):
|
|
for service in services:
|
|
if service not in selected:
|
|
selected.append(service)
|
|
|
|
touches_compose = any(rel == "platform/docker-compose.platform-http.yml" for rel in entries)
|
|
touches_caddy = any(rel == "platform/Caddyfile.http" for rel in entries)
|
|
touches_notification = any(rel == "platform/notification-core" or rel.startswith("platform/notification-core/") for rel in entries)
|
|
touches_ai_workspace = any(rel == "platform/ai-workspace-hub" or rel.startswith("platform/ai-workspace-hub/") for rel in entries)
|
|
touches_ai_workspace_assistant = any(rel == "platform/ai-workspace-assistant" or rel.startswith("platform/ai-workspace-assistant/") for rel in entries)
|
|
touches_ontology = any(rel == "platform/ontology-core" or rel.startswith("platform/ontology-core/") for rel in entries)
|
|
touches_gelios = any(rel == "platform/gelios-gateway" or rel.startswith("platform/gelios-gateway/") for rel in entries)
|
|
touches_map_gateway = any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries)
|
|
touches_external_data_plane = touches_external_data_plane_files(entries)
|
|
touches_authentik = any(rel == "authentik/custom-templates" or rel.startswith("authentik/custom-templates/") for rel in entries)
|
|
map_gateway_only_compose = touches_compose and touches_map_gateway and not any((touches_notification, touches_ai_workspace, touches_ai_workspace_assistant, touches_ontology, touches_gelios, touches_external_data_plane, touches_authentik))
|
|
restart_all_for_compose = touches_compose and not map_gateway_only_compose
|
|
|
|
if touches_notification or restart_all_for_compose:
|
|
add("notification-postgres", "notification-core", "launcher", "reverse-proxy")
|
|
if touches_ai_workspace or restart_all_for_compose:
|
|
add("ai-workspace-hub", "reverse-proxy")
|
|
if touches_ai_workspace_assistant or restart_all_for_compose:
|
|
add("ai-workspace-postgres", "ai-workspace-assistant")
|
|
if touches_ontology or restart_all_for_compose:
|
|
add("ontology-core", "ai-workspace-hub")
|
|
if touches_gelios or restart_all_for_compose:
|
|
add("gelios-postgres", "gelios-gateway")
|
|
if touches_map_gateway:
|
|
add("map-gateway")
|
|
if touches_external_data_plane:
|
|
# The Timescale/PostgreSQL service is durable infrastructure. Both
|
|
# contract and managed EDP changes recreate only the EDP
|
|
# application process.
|
|
add("external-data-plane")
|
|
if touches_authentik:
|
|
add("authentik-server", "authentik-worker", "reverse-proxy")
|
|
if touches_caddy:
|
|
add("reverse-proxy")
|
|
|
|
if selected:
|
|
return tuple(selected)
|
|
|
|
return COMPONENTS[component]["services"]
|
|
|
|
|
|
def component_compose_no_deps(component, entries=None):
|
|
if component == "dc-cms" and entries is not None:
|
|
touches_compose = any(rel == "infra/docker-compose.yml" for rel in entries)
|
|
touches_proxy = any(rel == "infra/reverse-proxy" or rel.startswith("infra/reverse-proxy/") for rel in entries)
|
|
touches_authentik = any(rel == "infra/authentik" or rel.startswith("infra/authentik/") for rel in entries)
|
|
return not (touches_compose or touches_proxy or touches_authentik)
|
|
|
|
return bool(COMPONENTS[component].get("compose_no_deps"))
|
|
|
|
|
|
def engine_backend_immutable_runtime_is_current():
|
|
metadata = validate_engine_backend_activation_marker()
|
|
compose_project = component_compose_root("engine").name
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER), "container", "ls", "-a",
|
|
"--filter", f"label=com.docker.compose.project={compose_project}",
|
|
"--filter", "label=com.docker.compose.service=nodedc-backend",
|
|
"--format", "{{.ID}}",
|
|
],
|
|
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("Engine backend immutable runtime topology is unproven")
|
|
containers = docker_json(
|
|
["container", "inspect", container_ids[0]],
|
|
"Engine backend immutable runtime container inspect",
|
|
)
|
|
container = containers[0] if isinstance(containers, list) and len(containers) == 1 else None
|
|
image = inspect_engine_backend_derived_image(metadata)
|
|
return (
|
|
isinstance(container, dict)
|
|
and container.get("Image") == image.get("Id")
|
|
and (container.get("Config") or {}).get("Image") == ENGINE_CREDENTIAL_BACKEND_IMAGE
|
|
)
|
|
|
|
|
|
def component_compose_files(
|
|
component,
|
|
allow_prepared_engine_backend=False,
|
|
expected_node_intelligence_gateway_sha256=None,
|
|
):
|
|
if component == "engine":
|
|
root = component_root(component)
|
|
files = [root / "docker-compose.yml"]
|
|
descriptor = current_engine_n8n_transition_descriptor()
|
|
if descriptor and descriptor["action"] == "activate":
|
|
override = root / ENGINE_N8N_COMPOSE_OVERRIDE_REL
|
|
if override.is_symlink() or not override.is_file():
|
|
die("active Engine n8n Compose override is missing or unsafe")
|
|
try:
|
|
override_text = override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("active Engine n8n Compose override cannot be read")
|
|
if override_text != expected_engine_n8n_compose_override(descriptor):
|
|
die("installed Engine n8n Compose override drift detected")
|
|
files.append(override)
|
|
if (ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.exists()
|
|
or ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.is_symlink()):
|
|
validate_engine_backend_activation_marker()
|
|
if (allow_prepared_engine_backend
|
|
or engine_backend_immutable_runtime_is_current()):
|
|
files.append(ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE)
|
|
publish_grant_override = root / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL
|
|
if publish_grant_override.exists() or publish_grant_override.is_symlink():
|
|
if publish_grant_override.is_symlink() or not publish_grant_override.is_file():
|
|
die("installed Engine data product publish grant override is unsafe")
|
|
try:
|
|
publish_grant_override_text = publish_grant_override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("installed Engine data product publish grant override cannot be read")
|
|
if publish_grant_override_text != expected_engine_data_product_publish_grant_override():
|
|
die("installed Engine data product publish grant override drift detected")
|
|
# Canonical order is immutable: base, active L2 extension, credential
|
|
# sink, then the additive Publish grant overlay.
|
|
files.append(publish_grant_override)
|
|
read_grant_override = root / ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL
|
|
if read_grant_override.exists() or read_grant_override.is_symlink():
|
|
if read_grant_override.is_symlink() or not read_grant_override.is_file():
|
|
die("installed Engine data product read grant override is unsafe")
|
|
try:
|
|
read_grant_override_text = read_grant_override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("installed Engine data product read grant override cannot be read")
|
|
if read_grant_override_text != expected_engine_data_product_read_grant_override():
|
|
die("installed Engine data product read grant override drift detected")
|
|
# Read authority is additive and ordered after the writer/private-key overlay.
|
|
files.append(read_grant_override)
|
|
node_intelligence_descriptor = current_engine_node_intelligence_descriptor()
|
|
if (
|
|
node_intelligence_descriptor is not None
|
|
and node_intelligence_descriptor["action"] == "activate"
|
|
):
|
|
if expected_node_intelligence_gateway_sha256 is None:
|
|
validate_installed_engine_node_intelligence_source(
|
|
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)
|
|
return tuple(files)
|
|
files = COMPONENTS[component].get("compose_files", ())
|
|
if component == "device-plane":
|
|
manager_overlay = DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_COMPOSE_REL
|
|
if manager_overlay.exists() or manager_overlay.is_symlink():
|
|
expected_manager_compose_sha256 = (
|
|
installed_device_plane_manager_compose_sha256()
|
|
)
|
|
if (
|
|
manager_overlay.is_symlink()
|
|
or not manager_overlay.is_file()
|
|
or sha256_file(manager_overlay)
|
|
!= expected_manager_compose_sha256
|
|
):
|
|
die("installed Device Manager Compose drift detected")
|
|
files = (*files, manager_overlay)
|
|
edge_descriptor = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL
|
|
)
|
|
edge_upgrade_descriptor = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL
|
|
)
|
|
edge_upgrade_v2_descriptor = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL
|
|
)
|
|
edge_upgrade_v4_descriptor = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL
|
|
)
|
|
edge_override = (
|
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL
|
|
)
|
|
edge_descriptor_exists = (
|
|
edge_descriptor.exists() or edge_descriptor.is_symlink()
|
|
)
|
|
edge_upgrade_descriptor_exists = (
|
|
edge_upgrade_descriptor.exists()
|
|
or edge_upgrade_descriptor.is_symlink()
|
|
)
|
|
edge_upgrade_v2_descriptor_exists = (
|
|
edge_upgrade_v2_descriptor.exists()
|
|
or edge_upgrade_v2_descriptor.is_symlink()
|
|
)
|
|
edge_upgrade_v4_descriptor_exists = (
|
|
edge_upgrade_v4_descriptor.exists()
|
|
or edge_upgrade_v4_descriptor.is_symlink()
|
|
)
|
|
edge_override_exists = edge_override.exists() or edge_override.is_symlink()
|
|
if (
|
|
edge_override_exists
|
|
!= (
|
|
edge_descriptor_exists
|
|
or edge_upgrade_descriptor_exists
|
|
or edge_upgrade_v2_descriptor_exists
|
|
or edge_upgrade_v4_descriptor_exists
|
|
)
|
|
or edge_upgrade_descriptor_exists
|
|
and not edge_descriptor_exists
|
|
or edge_upgrade_v2_descriptor_exists
|
|
and not (
|
|
edge_descriptor_exists and edge_upgrade_descriptor_exists
|
|
)
|
|
or edge_upgrade_v4_descriptor_exists
|
|
and not (
|
|
edge_descriptor_exists
|
|
and edge_upgrade_descriptor_exists
|
|
and edge_upgrade_v2_descriptor_exists
|
|
)
|
|
):
|
|
die("installed Device Edge Core channel source is incomplete")
|
|
if edge_descriptor_exists:
|
|
if (
|
|
edge_descriptor.is_symlink()
|
|
or not edge_descriptor.is_file()
|
|
or edge_override.is_symlink()
|
|
or not edge_override.is_file()
|
|
or sha256_file(edge_override)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_SHA256
|
|
):
|
|
die("installed Device Edge Core channel source drift detected")
|
|
descriptor = read_strict_json(
|
|
edge_descriptor,
|
|
"installed Device Edge Core channel descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
transition_id = descriptor.get("transitionId")
|
|
if (
|
|
not isinstance(transition_id, str)
|
|
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", transition_id)
|
|
or descriptor != (
|
|
expected_device_plane_edge_core_channel_bootstrap_descriptor(
|
|
transition_id
|
|
)
|
|
)
|
|
):
|
|
die("installed Device Edge Core channel descriptor drift detected")
|
|
upgrade_id = None
|
|
if edge_upgrade_descriptor_exists:
|
|
if (
|
|
transition_id
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID
|
|
or edge_upgrade_descriptor.is_symlink()
|
|
or not edge_upgrade_descriptor.is_file()
|
|
):
|
|
die("installed Device Edge Core channel upgrade predecessor drift detected")
|
|
upgrade = read_strict_json(
|
|
edge_upgrade_descriptor,
|
|
"installed Device Edge Core channel upgrade descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
upgrade_id = upgrade.get("transitionId")
|
|
if (
|
|
not isinstance(upgrade_id, str)
|
|
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", upgrade_id)
|
|
or upgrade
|
|
!= expected_device_plane_edge_core_channel_upgrade_descriptor(
|
|
upgrade_id
|
|
)
|
|
):
|
|
die("installed Device Edge Core channel upgrade descriptor drift detected")
|
|
if edge_upgrade_v2_descriptor_exists:
|
|
if (
|
|
upgrade_id
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_PATCH_ID
|
|
or edge_upgrade_v2_descriptor.is_symlink()
|
|
or not edge_upgrade_v2_descriptor.is_file()
|
|
):
|
|
die("installed Device Edge Core channel upgrade v2 predecessor drift detected")
|
|
upgrade_v2 = read_strict_json(
|
|
edge_upgrade_v2_descriptor,
|
|
"installed Device Edge Core channel upgrade v2 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
upgrade_v2_id = upgrade_v2.get("transitionId")
|
|
if (
|
|
not isinstance(upgrade_v2_id, str)
|
|
or not re.fullmatch(
|
|
r"[A-Za-z0-9._-]{1,96}", upgrade_v2_id
|
|
)
|
|
or upgrade_v2
|
|
!= expected_device_plane_edge_core_channel_upgrade_v2_descriptor(
|
|
upgrade_v2_id
|
|
)
|
|
):
|
|
die("installed Device Edge Core channel upgrade v2 descriptor drift detected")
|
|
if edge_upgrade_v4_descriptor_exists:
|
|
if (
|
|
upgrade_v2_id
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_PATCH_ID
|
|
or edge_upgrade_v4_descriptor.is_symlink()
|
|
or not edge_upgrade_v4_descriptor.is_file()
|
|
or sha256_file(
|
|
DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml"
|
|
)
|
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_BASE_COMPOSE_SHA256
|
|
):
|
|
die("installed Device Edge Core channel upgrade v4 predecessor drift detected")
|
|
upgrade_v4 = read_strict_json(
|
|
edge_upgrade_v4_descriptor,
|
|
"installed Device Edge Core channel upgrade v4 descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
upgrade_v4_id = upgrade_v4.get("transitionId")
|
|
if (
|
|
not isinstance(upgrade_v4_id, str)
|
|
or not re.fullmatch(
|
|
r"[A-Za-z0-9._-]{1,96}", upgrade_v4_id
|
|
)
|
|
or upgrade_v4
|
|
!= expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
|
upgrade_v4_id
|
|
)
|
|
):
|
|
die("installed Device Edge Core channel upgrade v4 descriptor drift detected")
|
|
files = (*files, edge_override)
|
|
overlay = DEVICE_PLANE_ROOT / DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL
|
|
if overlay.exists() or overlay.is_symlink():
|
|
if (
|
|
overlay.is_symlink()
|
|
or not overlay.is_file()
|
|
or sha256_file(overlay)
|
|
!= DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_SHA256
|
|
):
|
|
die("installed Device Plane backhaul Compose drift detected")
|
|
files = (*files, overlay)
|
|
if component == "platform":
|
|
# The EDP overlay is optional until its first dedicated artifact lands.
|
|
# Once installed (or after a candidate has been copied), it participates
|
|
# in every authoritative Compose operation.
|
|
files = tuple(
|
|
path
|
|
for path in files
|
|
if path.name != Path(PLATFORM_EXTERNAL_DATA_PLANE_COMPOSE_REL).name or path.is_file()
|
|
)
|
|
return files
|
|
|
|
|
|
def component_compose_env_file(component):
|
|
return COMPONENTS[component].get("compose_env_file")
|
|
|
|
|
|
def component_build_root(component):
|
|
return COMPONENTS[component].get("build_root")
|
|
|
|
|
|
def component_build_args(component, entries=None):
|
|
if component == "platform" and entries is not None:
|
|
touches_notification = any(rel == "platform/notification-core" or rel.startswith("platform/notification-core/") for rel in entries)
|
|
return COMPONENTS[component].get("build") if touches_notification else None
|
|
|
|
if component == "bim-viewer" and entries is not None:
|
|
touches_converter = any(rel == "converter" or rel.startswith("converter/") for rel in entries)
|
|
return COMPONENTS[component].get("build") if touches_converter else None
|
|
|
|
return COMPONENTS[component].get("build")
|
|
|
|
|
|
def component_builds(component, entries=None):
|
|
if is_platform_device_core_hub_trust_slice(component, entries):
|
|
return ()
|
|
|
|
if is_platform_device_manager_public_route_slice(component, entries):
|
|
return ()
|
|
|
|
if (
|
|
is_device_plane_control_core_release_slice(component, entries)
|
|
or is_device_plane_edge_core_channel_bootstrap_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
):
|
|
return ((
|
|
DEVICE_PLANE_ROOT,
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"--network=host",
|
|
"-f",
|
|
"services/device-control-core/Dockerfile",
|
|
"-t",
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
".",
|
|
),
|
|
),)
|
|
|
|
if is_device_plane_control_core_v3_reconciliation_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ()
|
|
|
|
if is_device_plane_control_core_incident_audit_slice(component, entries):
|
|
return ()
|
|
|
|
if is_device_plane_control_core_migration_replay_audit_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ()
|
|
|
|
if is_device_plane_control_core_migration_replay_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ((
|
|
DEVICE_PLANE_ROOT,
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"--network=host",
|
|
"-f",
|
|
"services/device-control-core/Dockerfile",
|
|
"-t",
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
".",
|
|
),
|
|
),)
|
|
|
|
if is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ((
|
|
DEVICE_PLANE_ROOT,
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"--network=host",
|
|
"-f",
|
|
"services/device-control-core/Dockerfile",
|
|
"-t",
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
".",
|
|
),
|
|
),)
|
|
|
|
if is_device_plane_manager_only_release_slice(component, entries):
|
|
return ((
|
|
DEVICE_PLANE_ROOT / "services/device-manager",
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"-t",
|
|
DEVICE_PLANE_MANAGER_IMAGE,
|
|
".",
|
|
),
|
|
),)
|
|
|
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
|
return (
|
|
(
|
|
DEVICE_PLANE_ROOT,
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"--network=host",
|
|
"-f",
|
|
"services/device-control-core/Dockerfile",
|
|
"-t",
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
".",
|
|
),
|
|
),
|
|
(
|
|
DEVICE_PLANE_ROOT / "services/device-manager",
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"-t",
|
|
DEVICE_PLANE_MANAGER_IMAGE,
|
|
".",
|
|
),
|
|
),
|
|
)
|
|
|
|
if is_device_plane_manager_reconciliation_slice(component, entries):
|
|
return ()
|
|
|
|
if is_device_plane_manager_v2_reconciliation_slice(component, entries):
|
|
return ()
|
|
|
|
if is_device_plane_backhaul_vps_enrollment_slice(component, entries):
|
|
return ()
|
|
|
|
if is_device_plane_backhaul_target_slice(component, entries):
|
|
return ((
|
|
DEVICE_PLANE_ROOT,
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"--network=host",
|
|
"-f",
|
|
"services/device-backhaul-target/Dockerfile",
|
|
"-t",
|
|
DEVICE_PLANE_BACKHAUL_TARGET_IMAGE,
|
|
".",
|
|
),
|
|
),)
|
|
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ()
|
|
|
|
if is_device_plane_postgres_bootstrap_slice(component, entries):
|
|
return ()
|
|
|
|
if is_device_plane_foundation_recovery_slice(component, entries):
|
|
return ()
|
|
|
|
if is_device_plane_foundation_network_publication_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ()
|
|
|
|
if component == "device-plane" and entries is not None:
|
|
selected_services = component_services(component, entries)
|
|
builds = []
|
|
if "device-control-core" in selected_services:
|
|
builds.append((
|
|
DEVICE_PLANE_ROOT,
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"--network=host",
|
|
"-f",
|
|
"services/device-control-core/Dockerfile",
|
|
"-t",
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
".",
|
|
),
|
|
))
|
|
if "device-gateway" in selected_services:
|
|
builds.append((
|
|
DEVICE_PLANE_ROOT,
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"--network=host",
|
|
"-f",
|
|
"services/device-gateway/Dockerfile",
|
|
"-t",
|
|
DEVICE_PLANE_GATEWAY_IMAGE,
|
|
".",
|
|
),
|
|
))
|
|
if "device-manager" in selected_services:
|
|
builds.append((
|
|
DEVICE_PLANE_ROOT / "services/device-manager",
|
|
(
|
|
"build",
|
|
"--no-cache",
|
|
"-t",
|
|
DEVICE_PLANE_MANAGER_IMAGE,
|
|
".",
|
|
),
|
|
))
|
|
return tuple(builds)
|
|
|
|
if component == "platform" and entries is not None:
|
|
touches_compose = any(rel == "platform/docker-compose.platform-http.yml" for rel in entries)
|
|
touches_notification = any(rel == "platform/notification-core" or rel.startswith("platform/notification-core/") for rel in entries)
|
|
touches_ai_workspace = any(rel == "platform/ai-workspace-hub" or rel.startswith("platform/ai-workspace-hub/") for rel in entries)
|
|
touches_ai_workspace_assistant = any(rel == "platform/ai-workspace-assistant" or rel.startswith("platform/ai-workspace-assistant/") for rel in entries)
|
|
touches_ontology = any(rel == "platform/ontology-core" or rel.startswith("platform/ontology-core/") for rel in entries)
|
|
touches_gelios = any(rel == "platform/gelios-gateway" or rel.startswith("platform/gelios-gateway/") for rel in entries)
|
|
touches_map_gateway = any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries)
|
|
touches_external_data_plane = touches_external_data_plane_files(entries)
|
|
map_gateway_only_compose = touches_compose and touches_map_gateway and not any((touches_notification, touches_ai_workspace, touches_ai_workspace_assistant, touches_ontology, touches_gelios, touches_external_data_plane))
|
|
rebuild_all_for_compose = touches_compose and not map_gateway_only_compose
|
|
builds = []
|
|
if touches_notification or rebuild_all_for_compose:
|
|
builds.append((
|
|
Path("/volume1/docker/nodedc-platform/platform/notification-core"),
|
|
("build", "--no-cache", "-t", "nodedc/notification-core:local", "."),
|
|
))
|
|
if touches_ai_workspace or rebuild_all_for_compose:
|
|
builds.append((
|
|
Path("/volume1/docker/nodedc-platform/platform/ai-workspace-hub"),
|
|
("build", "--no-cache", "-t", "nodedc/ai-workspace-hub:local", "."),
|
|
))
|
|
if touches_ai_workspace_assistant or rebuild_all_for_compose:
|
|
builds.append((
|
|
Path("/volume1/docker/nodedc-platform/platform/ai-workspace-assistant"),
|
|
("build", "--no-cache", "-t", "nodedc/ai-workspace-assistant:local", "."),
|
|
))
|
|
if touches_ontology or rebuild_all_for_compose:
|
|
builds.append((
|
|
Path("/volume1/docker/nodedc-platform/platform/ontology-core"),
|
|
("build", "--no-cache", "-t", "nodedc/ontology-core:local", "."),
|
|
))
|
|
if touches_gelios or rebuild_all_for_compose:
|
|
builds.append((
|
|
Path("/volume1/docker/nodedc-platform/platform/gelios-gateway"),
|
|
("build", "--no-cache", "-t", "nodedc/gelios-gateway:local", "."),
|
|
))
|
|
if touches_map_gateway:
|
|
builds.append((
|
|
Path("/volume1/docker/nodedc-platform/platform/services/map-gateway"),
|
|
("build", "--no-cache", "-t", "nodedc/map-gateway:local", "."),
|
|
))
|
|
if touches_external_data_plane:
|
|
builds.append((
|
|
Path("/volume1/docker/nodedc-platform/platform"),
|
|
("build", "--no-cache", "-f", "services/external-data-plane/Dockerfile", "-t", "nodedc/external-data-plane:local", "."),
|
|
))
|
|
return tuple(builds)
|
|
|
|
if component == "tasker":
|
|
plane_src = Path("/volume1/docker/nodedc-platform/tasker/plane-src")
|
|
return (
|
|
(
|
|
plane_src / "apps/api",
|
|
(
|
|
"build",
|
|
"--network=host",
|
|
"-t",
|
|
"nodedc/plane-backend:local",
|
|
"-f",
|
|
"Dockerfile.api",
|
|
".",
|
|
),
|
|
),
|
|
(
|
|
plane_src,
|
|
(
|
|
"build",
|
|
"--network=host",
|
|
"--build-arg",
|
|
"VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru",
|
|
"--build-arg",
|
|
"VITE_NODEDC_OIDC_LOGIN_ENABLED=1",
|
|
"-t",
|
|
"nodedc/plane-frontend:ru",
|
|
"-f",
|
|
"apps/web/Dockerfile.web.nas-legacy",
|
|
".",
|
|
),
|
|
),
|
|
)
|
|
|
|
build_root = component_build_root(component)
|
|
build_args = component_build_args(component, entries)
|
|
if build_root and build_args:
|
|
return ((build_root, build_args),)
|
|
return ()
|
|
|
|
|
|
def docker_json(args, label):
|
|
result = subprocess.run(
|
|
[str(DOCKER), *args],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
die(f"{label} failed: exit={result.returncode}")
|
|
try:
|
|
return json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
die(f"{label} returned invalid JSON")
|
|
|
|
|
|
def inspect_local_image(image_ref, label):
|
|
images = docker_json(["image", "inspect", image_ref], f"{label} inspect")
|
|
if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict):
|
|
die(f"{label} inspect shape mismatch")
|
|
image = images[0]
|
|
image_id = image.get("Id")
|
|
if (not isinstance(image_id, str)
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id)
|
|
or image.get("Architecture") != "amd64"
|
|
or image.get("Os") != "linux"):
|
|
die(f"{label} identity/platform mismatch")
|
|
return image_id
|
|
|
|
|
|
def inspect_optional_local_image(image_ref, label):
|
|
result = subprocess.run(
|
|
[str(DOCKER), "image", "inspect", image_ref],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
return None
|
|
try:
|
|
images = json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
die(f"{label} returned invalid JSON")
|
|
if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict):
|
|
die(f"{label} inspect shape mismatch")
|
|
image = images[0]
|
|
image_id = image.get("Id")
|
|
if (not isinstance(image_id, str)
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id)
|
|
or image.get("Architecture") != "amd64"
|
|
or image.get("Os") != "linux"):
|
|
die(f"{label} identity/platform mismatch")
|
|
return image_id
|
|
|
|
|
|
def gitea_compose_project_container_ids():
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"container",
|
|
"ls",
|
|
"--all",
|
|
"--filter",
|
|
f"label=com.docker.compose.project={GITEA_COMPOSE_PROJECT}",
|
|
"--format",
|
|
"{{.ID}}",
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
die("Gitea fresh-install container inventory failed")
|
|
container_ids = [
|
|
line.strip()
|
|
for line in result.stdout.splitlines()
|
|
if line.strip()
|
|
]
|
|
if any(not re.fullmatch(r"[a-f0-9]{12,64}", value) for value in container_ids):
|
|
die("Gitea fresh-install container inventory is invalid")
|
|
return tuple(container_ids)
|
|
|
|
|
|
def inspect_gitea_docker_compose_version():
|
|
result = subprocess.run(
|
|
[str(DOCKER), "compose", "version", "--short"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
raw = result.stdout.strip()
|
|
match = re.fullmatch(
|
|
r"v?(\d+)\.(\d+)\.(\d+)(?:[-+][A-Za-z0-9.-]+)?",
|
|
raw,
|
|
)
|
|
if result.returncode != 0 or match is None:
|
|
die("Gitea fresh-install Docker Compose version is unavailable or invalid")
|
|
version = tuple(int(part) for part in match.groups())
|
|
if version < GITEA_MINIMUM_COMPOSE_VERSION:
|
|
die(
|
|
"Gitea fresh-install requires Docker Compose >= "
|
|
+ ".".join(str(part) for part in GITEA_MINIMUM_COMPOSE_VERSION)
|
|
)
|
|
return raw
|
|
|
|
|
|
def inspect_gitea_docker_server_version():
|
|
result = subprocess.run(
|
|
[str(DOCKER), "version", "--format", "{{.Server.Version}}"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
version = result.stdout.strip()
|
|
if result.returncode != 0 or version != GITEA_REQUIRED_DOCKER_VERSION:
|
|
die(
|
|
"Gitea fresh-install requires exact Docker Server "
|
|
f"{GITEA_REQUIRED_DOCKER_VERSION}"
|
|
)
|
|
return version
|
|
|
|
|
|
def validate_gitea_compose_schema(compose_path):
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"compose",
|
|
"--project-name",
|
|
GITEA_COMPOSE_PROJECT,
|
|
"--file",
|
|
str(compose_path),
|
|
"config",
|
|
"--quiet",
|
|
],
|
|
cwd=str(compose_path.parent),
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
die("Gitea fresh-install Compose schema/config validation failed")
|
|
|
|
|
|
def inspect_gitea_local_image():
|
|
images = docker_json(
|
|
["image", "inspect", GITEA_IMAGE],
|
|
"Gitea pinned image inspect",
|
|
)
|
|
if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict):
|
|
die("Gitea pinned image inspect shape mismatch")
|
|
image = images[0]
|
|
if (
|
|
image.get("Id") != GITEA_IMAGE_ID
|
|
or tuple(image.get("RepoDigests") or ()) != (GITEA_REPO_DIGEST,)
|
|
or image.get("Architecture") != "amd64"
|
|
or image.get("Os") != "linux"
|
|
or (image.get("Config") or {}).get("User") != "1000:1000"
|
|
):
|
|
die("Gitea pinned image identity/repo-digest/platform/user mismatch")
|
|
return GITEA_IMAGE_ID
|
|
|
|
|
|
def gitea_loopback_listener_inodes(port):
|
|
expected_local = f"0100007F:{port:04X}"
|
|
try:
|
|
lines = Path("/proc/net/tcp").read_text(encoding="ascii").splitlines()
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Gitea Nginx listener inventory is unavailable")
|
|
inodes = set()
|
|
for line in lines[1:]:
|
|
fields = line.split()
|
|
if len(fields) < 10:
|
|
die("Gitea Nginx listener inventory is invalid")
|
|
if fields[1] == expected_local and fields[3] == "0A":
|
|
if not fields[9].isdigit() or fields[9] == "0":
|
|
die("Gitea Nginx listener inode is invalid")
|
|
inodes.add(fields[9])
|
|
if not inodes:
|
|
die(f"Gitea Nginx loopback listener is missing: {port}")
|
|
return inodes
|
|
|
|
|
|
def gitea_listener_process_owners(inodes):
|
|
pending = set(inodes)
|
|
owners = {}
|
|
try:
|
|
process_entries = tuple(Path("/proc").iterdir())
|
|
except OSError:
|
|
die("Gitea Nginx process inventory is unavailable")
|
|
for process in process_entries:
|
|
if not process.name.isdigit():
|
|
continue
|
|
try:
|
|
process_stat = process.stat()
|
|
comm = (process / "comm").read_text(encoding="ascii").strip()
|
|
descriptors = tuple((process / "fd").iterdir())
|
|
except FileNotFoundError:
|
|
continue
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Gitea Nginx process ownership inventory failed")
|
|
matched = set()
|
|
for descriptor in descriptors:
|
|
try:
|
|
target = os.readlink(descriptor)
|
|
except FileNotFoundError:
|
|
continue
|
|
except OSError:
|
|
die("Gitea Nginx descriptor ownership inventory failed")
|
|
match = re.fullmatch(r"socket:\[(\d+)\]", target)
|
|
if match and match.group(1) in inodes:
|
|
matched.add(match.group(1))
|
|
for inode in matched:
|
|
owners.setdefault(inode, set()).add((comm, process_stat.st_uid))
|
|
pending.discard(inode)
|
|
if pending:
|
|
die("Gitea Nginx listener owner is unproven")
|
|
return owners
|
|
|
|
|
|
def validate_gitea_nginx_listener():
|
|
inodes = gitea_loopback_listener_inodes(GITEA_HTTP_PORT)
|
|
owners = gitea_listener_process_owners(inodes)
|
|
allowed = {("nginx", 0), ("nginx", GITEA_NGINX_WORKER_UID)}
|
|
if any(not values or not values.issubset(allowed) for values in owners.values()):
|
|
die("Gitea TCP/3000 listener is not exclusively owned by Nginx")
|
|
return {
|
|
"address": "127.0.0.1:3000",
|
|
"owners": sorted({owner for values in owners.values() for owner in values}),
|
|
}
|
|
|
|
|
|
def validate_gitea_nginx_bridge_prerequisite():
|
|
try:
|
|
bridge_stat = GITEA_NGINX_BRIDGE_CONFIG.lstat()
|
|
bridge = GITEA_NGINX_BRIDGE_CONFIG.read_bytes()
|
|
except OSError:
|
|
die("Gitea Nginx UDS bridge config is missing or unreadable")
|
|
if (
|
|
stat.S_ISLNK(bridge_stat.st_mode)
|
|
or not stat.S_ISREG(bridge_stat.st_mode)
|
|
or bridge_stat.st_uid != 0
|
|
or bridge_stat.st_gid != 0
|
|
or stat.S_IMODE(bridge_stat.st_mode) != 0o644
|
|
or bridge != GITEA_NGINX_BRIDGE_CONTENT.encode("utf-8")
|
|
or hashlib.sha256(bridge).hexdigest() != GITEA_NGINX_BRIDGE_SHA256
|
|
):
|
|
die("Gitea Nginx UDS bridge identity/metadata mismatch")
|
|
version = subprocess.run(
|
|
[str(GITEA_NGINX), "-v"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
version_text = "\n".join(
|
|
value.strip() for value in (version.stdout, version.stderr) if value.strip()
|
|
)
|
|
if version.returncode != 0 or version_text != GITEA_NGINX_VERSION:
|
|
die("Gitea Nginx version mismatch")
|
|
config_test = subprocess.run(
|
|
[str(GITEA_NGINX), "-t", "-c", str(GITEA_NGINX_MAIN_CONFIG)],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if config_test.returncode != 0:
|
|
die("Gitea Nginx effective config test failed")
|
|
listener = validate_gitea_nginx_listener()
|
|
return {
|
|
"config": str(GITEA_NGINX_BRIDGE_CONFIG),
|
|
"sha256": GITEA_NGINX_BRIDGE_SHA256,
|
|
"version": GITEA_NGINX_VERSION.removeprefix("nginx version: "),
|
|
"listener": listener,
|
|
"unix_upstream": str(GITEA_SOCKET_FILE),
|
|
}
|
|
|
|
|
|
def validate_gitea_no_docker_port_publications():
|
|
result = subprocess.run(
|
|
[str(DOCKER), "container", "ls", "--format", "{{.ID}}"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
die("Gitea running Docker container inventory failed")
|
|
container_ids = tuple(
|
|
line.strip() for line in result.stdout.splitlines() if line.strip()
|
|
)
|
|
if any(not re.fullmatch(r"[a-f0-9]{12,64}", value) for value in container_ids):
|
|
die("Gitea running Docker container inventory is invalid")
|
|
if not container_ids:
|
|
return "no-running-docker-publications-3000-4022"
|
|
containers = docker_json(
|
|
["inspect", *container_ids],
|
|
"Gitea running Docker publication inspect",
|
|
)
|
|
if not isinstance(containers, list) or len(containers) != len(container_ids):
|
|
die("Gitea running Docker publication inventory shape mismatch")
|
|
forbidden_ports = {str(GITEA_HTTP_PORT), str(GITEA_DISABLED_SSH_HOST_PORT)}
|
|
for container in containers:
|
|
if not isinstance(container, dict):
|
|
die("Gitea running Docker publication inventory is invalid")
|
|
bindings = (container.get("HostConfig") or {}).get("PortBindings") or {}
|
|
if not isinstance(bindings, dict):
|
|
die("Gitea running Docker publication bindings are invalid")
|
|
for values in bindings.values():
|
|
if values is None:
|
|
continue
|
|
if not isinstance(values, list):
|
|
die("Gitea running Docker publication bindings are invalid")
|
|
for value in values:
|
|
if not isinstance(value, dict):
|
|
die("Gitea running Docker publication bindings are invalid")
|
|
if str(value.get("HostPort") or "") in forbidden_ports:
|
|
die("Docker must not publish Gitea TCP/3000 or TCP/4022")
|
|
return "no-running-docker-publications-3000-4022"
|
|
|
|
|
|
def validate_gitea_legacy_candidate_network_absent():
|
|
result = subprocess.run(
|
|
[str(DOCKER), "network", "inspect", GITEA_FORBIDDEN_LEGACY_NETWORK],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
die("Gitea forbidden legacy candidate network still exists")
|
|
error = result.stderr.strip()
|
|
if (
|
|
result.returncode != 1
|
|
or result.stdout.strip()
|
|
or not re.search(r"(?:No such network|network .+ not found)", error)
|
|
):
|
|
die("Gitea forbidden legacy candidate network absence is unproven")
|
|
return "absent"
|
|
|
|
|
|
def inspect_gitea_builtin_none_network():
|
|
networks = docker_json(
|
|
["network", "inspect", "none"],
|
|
"Gitea built-in none network inspect",
|
|
)
|
|
if (
|
|
not isinstance(networks, list)
|
|
or len(networks) != 1
|
|
or not isinstance(networks[0], dict)
|
|
):
|
|
die("Gitea built-in none network inspect shape mismatch")
|
|
network = networks[0]
|
|
network_id = network.get("Id")
|
|
if (
|
|
network.get("Name") != "none"
|
|
or not isinstance(network_id, str)
|
|
or not re.fullmatch(r"[a-f0-9]{64}", network_id)
|
|
or network.get("Scope") != "local"
|
|
or network.get("Driver") != "null"
|
|
or network.get("Internal") is not False
|
|
or network.get("Attachable") is not False
|
|
or network.get("Ingress") is not False
|
|
or network.get("ConfigOnly") is not False
|
|
):
|
|
die("Gitea built-in none network identity mismatch")
|
|
return network
|
|
|
|
|
|
def gitea_generated_reverse_proxy_server_block(generated):
|
|
marker = "server_name git.dcserve.ru ;"
|
|
if generated.count(marker) != 1:
|
|
die("Gitea DSM generated reverse-proxy server identity mismatch")
|
|
marker_index = generated.index(marker)
|
|
server_index = generated.rfind("server {", 0, marker_index)
|
|
if server_index < 0:
|
|
die("Gitea DSM generated reverse-proxy server block is missing")
|
|
brace_index = generated.index("{", server_index)
|
|
depth = 0
|
|
for index in range(brace_index, len(generated)):
|
|
character = generated[index]
|
|
if character == "{":
|
|
depth += 1
|
|
elif character == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return generated[server_index:index + 1]
|
|
die("Gitea DSM generated reverse-proxy server block is invalid")
|
|
|
|
|
|
def validate_gitea_reverse_proxy_prerequisite():
|
|
try:
|
|
config_stat = GITEA_REVERSE_PROXY_CONFIG.lstat()
|
|
except FileNotFoundError:
|
|
die("Gitea DSM reverse-proxy persistent config is missing")
|
|
if (
|
|
stat.S_ISLNK(config_stat.st_mode)
|
|
or not stat.S_ISREG(config_stat.st_mode)
|
|
or config_stat.st_uid != 0
|
|
or config_stat.st_gid != 0
|
|
or config_stat.st_size < 64
|
|
or config_stat.st_size > 8 * 1024 * 1024
|
|
):
|
|
die("Gitea DSM reverse-proxy persistent config is unsafe")
|
|
document = read_strict_json(
|
|
GITEA_REVERSE_PROXY_CONFIG,
|
|
"Gitea DSM reverse-proxy persistent config",
|
|
max_bytes=8 * 1024 * 1024,
|
|
)
|
|
rule = document.get(GITEA_REVERSE_PROXY_UUID) if isinstance(document, dict) else None
|
|
expected = {
|
|
"backend": {"fqdn": "127.0.0.1", "port": 3000, "protocol": 0},
|
|
"customize_headers": [],
|
|
"description": GITEA_REVERSE_PROXY_DESCRIPTION,
|
|
"frontend": {
|
|
"acl": None,
|
|
"fqdn": "git.dcserve.ru",
|
|
"https": {"hsts": False},
|
|
"port": 443,
|
|
"protocol": 1,
|
|
},
|
|
"proxy_connect_timeout": 60,
|
|
"proxy_http_version": 1,
|
|
"proxy_intercept_errors": False,
|
|
"proxy_read_timeout": 60,
|
|
"proxy_send_timeout": 60,
|
|
}
|
|
if rule != expected:
|
|
die(
|
|
"Gitea DSM reverse-proxy prerequisite mismatch; apply the "
|
|
"separate reviewed proxy transition first"
|
|
)
|
|
|
|
try:
|
|
generated_stat = GITEA_REVERSE_PROXY_GENERATED_CONFIG.lstat()
|
|
generated = GITEA_REVERSE_PROXY_GENERATED_CONFIG.read_text(
|
|
encoding="utf-8"
|
|
)
|
|
except (FileNotFoundError, UnicodeDecodeError):
|
|
die("Gitea DSM generated reverse-proxy config is missing or invalid")
|
|
server_block = gitea_generated_reverse_proxy_server_block(generated)
|
|
generated_headers = {}
|
|
for name, value in re.findall(
|
|
r"(?m)^\s*proxy_set_header\s+([A-Za-z0-9-]+)\s+([^;]+);\s*$",
|
|
server_block,
|
|
):
|
|
generated_headers.setdefault(name.casefold(), []).append(value.strip())
|
|
expected_headers = {
|
|
"host": ["$http_host"],
|
|
"x-real-ip": ["$remote_addr"],
|
|
"x-forwarded-for": ["$proxy_add_x_forwarded_for"],
|
|
"x-forwarded-proto": ["$scheme"],
|
|
}
|
|
host_guard = re.search(
|
|
r"if\s*\(\s*\$host\s+!~\s+\"\(\^git\.dcserve\.ru\$\)\"\s*\)"
|
|
r"\s*\{\s*return\s+404\s*;\s*\}",
|
|
server_block,
|
|
)
|
|
if (
|
|
stat.S_ISLNK(generated_stat.st_mode)
|
|
or not stat.S_ISREG(generated_stat.st_mode)
|
|
or generated_stat.st_uid != 0
|
|
or generated_stat.st_size < 64
|
|
or generated_stat.st_size > 4 * 1024 * 1024
|
|
or server_block.count("proxy_pass http://127.0.0.1:3000;") != 1
|
|
or "proxy_pass http://172.22.0.222:3000;" in server_block
|
|
or any(
|
|
generated_headers.get(name) != values
|
|
for name, values in expected_headers.items()
|
|
)
|
|
or host_guard is None
|
|
):
|
|
die("Gitea DSM generated reverse-proxy prerequisite mismatch")
|
|
return {
|
|
"persistent_rule": GITEA_REVERSE_PROXY_UUID,
|
|
"upstream": "http://127.0.0.1:3000",
|
|
"generated_config": str(GITEA_REVERSE_PROXY_GENERATED_CONFIG),
|
|
}
|
|
|
|
|
|
def gitea_iptables_rule_present(arguments):
|
|
result = subprocess.run(
|
|
[str(GITEA_IPTABLES), "-w", "5", "-C", *arguments],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode not in (0, 1):
|
|
die("Gitea firewall prerequisite inventory failed")
|
|
return result.returncode == 0
|
|
|
|
|
|
def validate_gitea_firewall_prerequisite():
|
|
if not GITEA_IPTABLES.is_file():
|
|
die("Gitea firewall prerequisite command is missing")
|
|
if gitea_iptables_rule_present(
|
|
("INPUT", "-p", "tcp", "--dport", "3000", "-j", "DROP")
|
|
):
|
|
die(
|
|
"Gitea loopback TCP/3000 is still blocked; apply the separate "
|
|
"reviewed firewall transition first"
|
|
)
|
|
required = (
|
|
(
|
|
"OUTPUT",
|
|
"-o",
|
|
"lo",
|
|
"-d",
|
|
f"{GITEA_LEGACY_REVERSE_PROXY_IP}/32",
|
|
"-p",
|
|
"tcp",
|
|
"--dport",
|
|
"3000",
|
|
"-j",
|
|
"DROP",
|
|
),
|
|
("INPUT", "-p", "tcp", "--dport", "4022", "-j", "DROP"),
|
|
)
|
|
if not all(gitea_iptables_rule_present(arguments) for arguments in required):
|
|
die("Gitea legacy firewall isolation prerequisite is incomplete")
|
|
return {
|
|
"loopback_3000": "not-blocked-by-emergency-input-drop",
|
|
"legacy_172_22_0_222_3000": "output-drop-present",
|
|
"legacy_4022": "input-drop-present",
|
|
}
|
|
|
|
|
|
def validate_legacy_gitea_container_isolation():
|
|
result = subprocess.run(
|
|
[str(DOCKER), "container", "inspect", GITEA_LEGACY_CONTAINER],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
return "absent"
|
|
try:
|
|
containers = json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
die("legacy Gitea container isolation inventory is invalid")
|
|
if (
|
|
not isinstance(containers, list)
|
|
or len(containers) != 1
|
|
or not isinstance(containers[0], dict)
|
|
):
|
|
die("legacy Gitea container isolation inventory shape mismatch")
|
|
container = containers[0]
|
|
if (
|
|
(container.get("State") or {}).get("Running") is not False
|
|
or (container.get("HostConfig") or {}).get("RestartPolicy", {}).get("Name")
|
|
!= "no"
|
|
):
|
|
die("legacy Gitea container is not stopped with restart disabled")
|
|
return "stopped-restart-disabled"
|
|
|
|
|
|
def preflight_gitea_fresh_install():
|
|
try:
|
|
GITEA_ROOT.lstat()
|
|
except FileNotFoundError:
|
|
pass
|
|
else:
|
|
die(
|
|
"Gitea fresh-install root already exists; updates, imports, and "
|
|
"partial-state reuse are forbidden"
|
|
)
|
|
if gitea_compose_project_container_ids():
|
|
die("Gitea fresh-install Compose project already has containers")
|
|
legacy_candidate_network = validate_gitea_legacy_candidate_network_absent()
|
|
assert_loopback_tcp_port_closed(GITEA_DISABLED_SSH_HOST_PORT)
|
|
nginx_bridge = validate_gitea_nginx_bridge_prerequisite()
|
|
reverse_proxy = validate_gitea_reverse_proxy_prerequisite()
|
|
firewall = validate_gitea_firewall_prerequisite()
|
|
legacy_container = validate_legacy_gitea_container_isolation()
|
|
docker_publications = validate_gitea_no_docker_port_publications()
|
|
docker_version = inspect_gitea_docker_server_version()
|
|
builtin_none_network = inspect_gitea_builtin_none_network()
|
|
compose_version = inspect_gitea_docker_compose_version()
|
|
image_id = inspect_gitea_local_image()
|
|
return {
|
|
"mode": "fresh-root-absent",
|
|
"image_id": image_id,
|
|
"transport": "unix:/run/gitea/gitea.sock",
|
|
"network_mode": "none",
|
|
"ssh_host_port": "closed:4022/tcp",
|
|
"reverse_proxy_prerequisite": "127.0.0.1:3000",
|
|
"nginx_bridge": nginx_bridge,
|
|
"docker_version": docker_version,
|
|
"builtin_none_network_id": builtin_none_network["Id"],
|
|
"compose_version": compose_version,
|
|
"reverse_proxy": reverse_proxy,
|
|
"firewall": firewall,
|
|
"docker_publications": docker_publications,
|
|
"legacy_candidate_network": legacy_candidate_network,
|
|
"legacy_container": legacy_container,
|
|
}
|
|
|
|
|
|
def inspect_gitea_salvage_local_image():
|
|
images = docker_json(
|
|
["image", "inspect", GITEA_SALVAGE_IMAGE],
|
|
"Gitea salvage pinned image inspect",
|
|
)
|
|
if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict):
|
|
die("Gitea salvage pinned image inspect shape mismatch")
|
|
image = images[0]
|
|
config = image.get("Config") or {}
|
|
if (
|
|
image.get("Id") != GITEA_SALVAGE_IMAGE_ID
|
|
or tuple(image.get("RepoDigests") or ()) != (GITEA_SALVAGE_REPO_DIGEST,)
|
|
or image.get("Architecture") != "amd64"
|
|
or image.get("Os") != "linux"
|
|
or config.get("User") != "1000:1000"
|
|
):
|
|
die("Gitea salvage pinned image identity/repo-digest/platform/user mismatch")
|
|
return GITEA_SALVAGE_IMAGE_ID
|
|
|
|
|
|
def docker_named_container_inspect_fail_closed(name, label):
|
|
result = subprocess.run(
|
|
[str(DOCKER), "container", "inspect", name],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
try:
|
|
containers = json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
die(f"{label} returned invalid JSON")
|
|
if (
|
|
not isinstance(containers, list)
|
|
or len(containers) != 1
|
|
or not isinstance(containers[0], dict)
|
|
):
|
|
die(f"{label} shape mismatch")
|
|
return containers[0]
|
|
|
|
stdout_raw = result.stdout.strip()
|
|
if not stdout_raw:
|
|
stdout_category = "empty"
|
|
stdout_proves_empty = True
|
|
elif stdout_raw == "[]":
|
|
stdout_category = "json-empty-list"
|
|
stdout_proves_empty = True
|
|
else:
|
|
try:
|
|
stdout_json = json.loads(stdout_raw)
|
|
except json.JSONDecodeError:
|
|
stdout_category = "non-json"
|
|
stdout_proves_empty = False
|
|
else:
|
|
stdout_category = f"json-{type(stdout_json).__name__}"
|
|
stdout_proves_empty = False
|
|
|
|
error = result.stderr.strip()
|
|
exact_no_such = {
|
|
f"No such object: {name}": "exact-no-such-object",
|
|
f"No such container: {name}": "exact-no-such-container",
|
|
f"Error: No such object: {name}": "exact-no-such-object",
|
|
f"Error: No such container: {name}": "exact-no-such-container",
|
|
f"Error response from daemon: No such object: {name}": (
|
|
"exact-no-such-object"
|
|
),
|
|
f"Error response from daemon: No such container: {name}": (
|
|
"exact-no-such-container"
|
|
),
|
|
}
|
|
stderr_category = exact_no_such.get(error)
|
|
if (
|
|
result.returncode == 1
|
|
and stdout_proves_empty
|
|
and stderr_category is not None
|
|
):
|
|
return None
|
|
if stderr_category is None:
|
|
lowered = error.lower()
|
|
if not error:
|
|
stderr_category = "empty"
|
|
elif "permission denied" in lowered or "access is denied" in lowered:
|
|
stderr_category = "permission-denied"
|
|
elif "cannot connect" in lowered or "daemon" in lowered:
|
|
stderr_category = "daemon-error"
|
|
elif "timed out" in lowered or "deadline exceeded" in lowered:
|
|
stderr_category = "timeout"
|
|
else:
|
|
stderr_category = "other"
|
|
die(
|
|
f"{label} absence is unproven: rc={result.returncode} "
|
|
f"stdout={stdout_category} stderr={stderr_category}"
|
|
)
|
|
|
|
|
|
def validate_gitea_salvage_legacy_container():
|
|
if (
|
|
GITEA_SALVAGE_EXPECTED_LEGACY_IMAGE is None
|
|
or GITEA_SALVAGE_EXPECTED_LEGACY_IMAGE_ID is None
|
|
):
|
|
die(
|
|
"Gitea salvage legacy container image/mount identity is not "
|
|
"reviewed and pinned"
|
|
)
|
|
container = docker_named_container_inspect_fail_closed(
|
|
GITEA_LEGACY_CONTAINER,
|
|
"Gitea salvage legacy container inspect",
|
|
)
|
|
if container is None:
|
|
die("Gitea salvage requires the exact stopped legacy container evidence")
|
|
config = container.get("Config") or {}
|
|
host = container.get("HostConfig") or {}
|
|
mounts = container.get("Mounts") or []
|
|
expected_mounts = {
|
|
(str(GITEA_SALVAGE_LEGACY_ROOT), "/data", "bind", True),
|
|
}
|
|
actual_mounts = set()
|
|
for mount in mounts:
|
|
if not isinstance(mount, dict) or mount.get("RW") is not True:
|
|
die("Gitea salvage legacy container mount inventory is invalid")
|
|
actual_mounts.add(
|
|
(
|
|
mount.get("Source"),
|
|
mount.get("Destination"),
|
|
mount.get("Type"),
|
|
mount.get("RW"),
|
|
)
|
|
)
|
|
if (
|
|
container.get("Name") != "/gitea"
|
|
or len(mounts) != 1
|
|
or (container.get("State") or {}).get("Running") is not False
|
|
or (host.get("RestartPolicy") or {}).get("Name") != "no"
|
|
or config.get("Image") != GITEA_SALVAGE_EXPECTED_LEGACY_IMAGE
|
|
or container.get("Image") != GITEA_SALVAGE_EXPECTED_LEGACY_IMAGE_ID
|
|
or actual_mounts != expected_mounts
|
|
):
|
|
die("Gitea salvage legacy container identity/isolation mismatch")
|
|
return {
|
|
"container_id": container.get("Id"),
|
|
"image": config.get("Image"),
|
|
"image_id": container.get("Image"),
|
|
"mounts": sorted(actual_mounts),
|
|
"name": container.get("Name"),
|
|
"state": "stopped-restart-no",
|
|
}
|
|
|
|
|
|
def gitea_salvage_mountpoints():
|
|
try:
|
|
lines = Path("/proc/self/mountinfo").read_text(encoding="utf-8").splitlines()
|
|
except (OSError, UnicodeError):
|
|
die("Gitea salvage cannot attest process mount boundaries")
|
|
mountpoints = set()
|
|
for line in lines:
|
|
fields = line.split()
|
|
if len(fields) < 10 or "-" not in fields:
|
|
die("Gitea salvage process mount inventory is malformed")
|
|
raw = fields[4]
|
|
if re.search(r"\\(?!(?:040|011|012|134))", raw):
|
|
die("Gitea salvage process mount inventory escape is malformed")
|
|
try:
|
|
decoded = re.sub(
|
|
r"\\([0-7]{3})",
|
|
lambda match: chr(int(match.group(1), 8)),
|
|
raw,
|
|
)
|
|
except (TypeError, ValueError):
|
|
die("Gitea salvage process mount inventory escape is malformed")
|
|
if not decoded.startswith("/"):
|
|
die("Gitea salvage process mount inventory path is unsafe")
|
|
mountpoints.add(os.path.normpath(decoded))
|
|
if not mountpoints:
|
|
die("Gitea salvage process mount inventory is empty")
|
|
return mountpoints
|
|
|
|
|
|
def validate_gitea_salvage_path_chain(
|
|
root,
|
|
target,
|
|
final_mode,
|
|
label,
|
|
allow_missing_final=False,
|
|
trusted_device=None,
|
|
mountpoints=None,
|
|
):
|
|
try:
|
|
relative = target.relative_to(root)
|
|
except (TypeError, ValueError):
|
|
die(f"Gitea salvage {label} escapes its trusted root")
|
|
if not relative.parts or any(part in ("", ".", "..") for part in relative.parts):
|
|
die(f"Gitea salvage {label} path is invalid")
|
|
try:
|
|
root_stat = root.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Gitea salvage {label} trusted root is missing")
|
|
if stat.S_ISLNK(root_stat.st_mode) or not stat.S_ISDIR(root_stat.st_mode):
|
|
die(f"Gitea salvage {label} trusted root is unsafe")
|
|
if trusted_device is None:
|
|
trusted_device = root_stat.st_dev
|
|
elif root_stat.st_dev != trusted_device:
|
|
die(f"Gitea salvage {label} trusted root crosses a filesystem boundary")
|
|
if mountpoints is None:
|
|
mountpoints = gitea_salvage_mountpoints()
|
|
current = root
|
|
for index, part in enumerate(relative.parts):
|
|
current = current / part
|
|
final = index == len(relative.parts) - 1
|
|
try:
|
|
current_stat = current.lstat()
|
|
except FileNotFoundError:
|
|
if final and allow_missing_final:
|
|
return None
|
|
die(f"Gitea salvage {label} is missing")
|
|
if stat.S_ISLNK(current_stat.st_mode):
|
|
die(f"Gitea salvage {label} path contains a symlink")
|
|
if current_stat.st_dev != trusted_device:
|
|
die(f"Gitea salvage {label} crosses a filesystem boundary")
|
|
if os.path.normpath(str(current)) in mountpoints:
|
|
die(f"Gitea salvage {label} crosses a mount boundary")
|
|
# Every Btrfs subvolume root has inode 256. The trusted root itself is
|
|
# the reviewed snapshot; seeing another inode-256 directory below it
|
|
# means the parent ro property does not attest the nested subvolume.
|
|
if stat.S_ISDIR(current_stat.st_mode) and current_stat.st_ino == 256:
|
|
die(f"Gitea salvage {label} crosses a nested subvolume boundary")
|
|
if final:
|
|
if not final_mode(current_stat.st_mode):
|
|
die(f"Gitea salvage {label} has an unsafe type")
|
|
elif not stat.S_ISDIR(current_stat.st_mode):
|
|
die(f"Gitea salvage {label} parent has an unsafe type")
|
|
return target
|
|
|
|
|
|
def validate_gitea_salvage_internal_entry(
|
|
path,
|
|
path_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
expected_mode,
|
|
label,
|
|
):
|
|
if stat.S_ISLNK(path_stat.st_mode) or not expected_mode(path_stat.st_mode):
|
|
die(f"Gitea salvage {label} has an unsafe type: {path}")
|
|
if path_stat.st_dev != trusted_device:
|
|
die(f"Gitea salvage {label} crosses a filesystem boundary: {path}")
|
|
if os.path.normpath(str(path)) in mountpoints:
|
|
die(f"Gitea salvage {label} crosses a mount boundary: {path}")
|
|
if stat.S_ISDIR(path_stat.st_mode) and path_stat.st_ino == 256:
|
|
die(f"Gitea salvage {label} crosses a nested subvolume boundary: {path}")
|
|
|
|
|
|
def probe_gitea_salvage_path_no_follow(
|
|
root,
|
|
relative,
|
|
trusted_device,
|
|
mountpoints,
|
|
label,
|
|
final_mode=lambda _mode: True,
|
|
):
|
|
parts = PurePosixPath(relative).parts
|
|
if (
|
|
not parts
|
|
or PurePosixPath(relative).is_absolute()
|
|
or any(part in ("", ".", "..") for part in parts)
|
|
):
|
|
die(f"Gitea salvage {label} probe path is invalid")
|
|
current = root
|
|
for index, part in enumerate(parts):
|
|
current = current / part
|
|
try:
|
|
current_stat = current.lstat()
|
|
except FileNotFoundError:
|
|
return None
|
|
except OSError as exc:
|
|
error_number = exc.errno if isinstance(exc.errno, int) else "unknown"
|
|
die(f"Gitea salvage {label} probe failed: errno={error_number}")
|
|
validate_gitea_salvage_internal_entry(
|
|
current,
|
|
current_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISDIR if index < len(parts) - 1 else final_mode,
|
|
label,
|
|
)
|
|
return current_stat
|
|
|
|
|
|
def validate_gitea_salvage_snapshot_boundary():
|
|
snapshot_parent = GITEA_SALVAGE_SNAPSHOT_ROOT.parent
|
|
try:
|
|
parent_stat = snapshot_parent.lstat()
|
|
snapshot_stat = GITEA_SALVAGE_SNAPSHOT_ROOT.lstat()
|
|
except FileNotFoundError:
|
|
die("Gitea salvage snapshot root is missing")
|
|
if (
|
|
stat.S_ISLNK(parent_stat.st_mode)
|
|
or not stat.S_ISDIR(parent_stat.st_mode)
|
|
or parent_stat.st_uid != 0
|
|
or parent_stat.st_gid != 0
|
|
or stat.S_IMODE(parent_stat.st_mode) != 0o700
|
|
or
|
|
stat.S_ISLNK(snapshot_stat.st_mode)
|
|
or not stat.S_ISDIR(snapshot_stat.st_mode)
|
|
or snapshot_stat.st_uid != 0
|
|
or snapshot_stat.st_gid != 0
|
|
):
|
|
die("Gitea salvage snapshot root metadata mismatch")
|
|
if not GITEA_SALVAGE_BTRFS.is_file():
|
|
die("Gitea salvage btrfs command is missing")
|
|
show = subprocess.run(
|
|
[str(GITEA_SALVAGE_BTRFS), "subvolume", "show", str(GITEA_SALVAGE_SNAPSHOT_ROOT)],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if show.returncode != 0:
|
|
die("Gitea salvage snapshot subvolume inventory failed")
|
|
uuids = re.findall(
|
|
r"(?mi)^\s*UUID:\s*([0-9a-f-]{36})\s*$",
|
|
show.stdout,
|
|
)
|
|
if uuids != [GITEA_SALVAGE_SNAPSHOT_UUID]:
|
|
die("Gitea salvage snapshot UUID mismatch")
|
|
readonly = subprocess.run(
|
|
[
|
|
str(GITEA_SALVAGE_BTRFS),
|
|
"property",
|
|
"get",
|
|
str(GITEA_SALVAGE_SNAPSHOT_ROOT),
|
|
"ro",
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if readonly.returncode != 0 or readonly.stdout.strip() != "ro=true":
|
|
die("Gitea salvage snapshot is not attestably read-only")
|
|
for path, expected_mode, label in (
|
|
(GITEA_SALVAGE_SNAPSHOT_DATABASE, stat.S_ISREG, "database"),
|
|
(GITEA_SALVAGE_SNAPSHOT_REPOSITORIES, stat.S_ISDIR, "repository root"),
|
|
):
|
|
validate_gitea_salvage_path_chain(
|
|
GITEA_SALVAGE_SNAPSHOT_ROOT,
|
|
path,
|
|
expected_mode,
|
|
f"snapshot {label}",
|
|
)
|
|
database_stat = GITEA_SALVAGE_SNAPSHOT_DATABASE.lstat()
|
|
if (
|
|
database_stat.st_size != GITEA_SALVAGE_SNAPSHOT_DATABASE_BYTES
|
|
or sha256_file(GITEA_SALVAGE_SNAPSHOT_DATABASE)
|
|
!= GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256
|
|
):
|
|
die("Gitea salvage snapshot database identity mismatch")
|
|
return {
|
|
"root": str(GITEA_SALVAGE_SNAPSHOT_ROOT),
|
|
"uuid": GITEA_SALVAGE_SNAPSHOT_UUID,
|
|
"readonly": True,
|
|
"database_sha256": GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256,
|
|
}
|
|
|
|
|
|
def gitea_salvage_sqlite_connection(database):
|
|
connection = sqlite3.connect(
|
|
f"file:{database}?mode=ro&immutable=1",
|
|
uri=True,
|
|
)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA query_only=ON")
|
|
connection.execute("PRAGMA trusted_schema=OFF")
|
|
query_only = connection.execute("PRAGMA query_only").fetchone()
|
|
trusted_schema = connection.execute("PRAGMA trusted_schema").fetchone()
|
|
if (
|
|
query_only is None
|
|
or len(query_only) != 1
|
|
or query_only[0] != 1
|
|
or trusted_schema is None
|
|
or len(trusted_schema) != 1
|
|
or trusted_schema[0] != 0
|
|
):
|
|
connection.close()
|
|
die("Gitea salvage SQLite read-only safety PRAGMA mismatch")
|
|
return connection
|
|
|
|
|
|
def gitea_salvage_table_columns(connection, table):
|
|
if not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", table):
|
|
die("Gitea salvage schema table name is unsafe")
|
|
rows = connection.execute(f'PRAGMA table_info("{table}")').fetchall()
|
|
return {str(row[1]) for row in rows}
|
|
|
|
|
|
def gitea_salvage_sqlite_version(connection):
|
|
try:
|
|
value = connection.execute("SELECT sqlite_version()").fetchone()[0]
|
|
except (sqlite3.DatabaseError, TypeError, IndexError):
|
|
die("Gitea salvage SQLite version inventory failed")
|
|
match = re.fullmatch(r"([0-9]+)\.([0-9]+)\.([0-9]+)", str(value))
|
|
if match is None:
|
|
die("Gitea salvage SQLite version inventory is invalid")
|
|
return str(value), tuple(int(part) for part in match.groups())
|
|
|
|
|
|
def gitea_salvage_table_schema(
|
|
connection,
|
|
table,
|
|
table_list_supported,
|
|
allowed_tables=None,
|
|
):
|
|
registry = (
|
|
GITEA_SALVAGE_UNSUPPORTED_SCHEMA_TABLES
|
|
if allowed_tables is None
|
|
else allowed_tables
|
|
)
|
|
if (
|
|
table not in registry
|
|
or not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", table)
|
|
):
|
|
die("Gitea salvage schema table is outside the review registry")
|
|
try:
|
|
object_rows = connection.execute(
|
|
"SELECT type,sql FROM sqlite_master WHERE name=? ORDER BY type",
|
|
(table,),
|
|
).fetchall()
|
|
table_list_rows = (
|
|
connection.execute(f'PRAGMA table_list("{table}")').fetchall()
|
|
if table_list_supported
|
|
else []
|
|
)
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage schema-only inventory failed")
|
|
object_type = None
|
|
object_sql = None
|
|
if len(object_rows) == 1 and len(object_rows[0]) == 2:
|
|
object_type = object_rows[0][0]
|
|
object_sql = object_rows[0][1]
|
|
ordinary_sql = (
|
|
isinstance(object_sql, str)
|
|
and re.match(
|
|
r"\A\s*CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+",
|
|
object_sql,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
is not None
|
|
)
|
|
virtual_sql = (
|
|
isinstance(object_sql, str)
|
|
and re.match(
|
|
r"\A\s*CREATE\s+VIRTUAL\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+",
|
|
object_sql,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
is not None
|
|
)
|
|
safe_sqlite_schema_object = object_type == "table" and ordinary_sql
|
|
safe_table_list_object = (
|
|
len(table_list_rows) == 1
|
|
and len(table_list_rows[0]) == 6
|
|
and tuple(table_list_rows[0][0:3]) == ("main", table, "table")
|
|
and isinstance(table_list_rows[0][3], int)
|
|
and table_list_rows[0][3] >= 0
|
|
and table_list_rows[0][4] in (0, 1)
|
|
and table_list_rows[0][5] in (0, 1)
|
|
)
|
|
safe_object = safe_sqlite_schema_object and (
|
|
safe_table_list_object if table_list_supported else True
|
|
)
|
|
rows = []
|
|
if safe_object:
|
|
try:
|
|
rows = connection.execute(f'PRAGMA table_xinfo("{table}")').fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage schema-only inventory failed")
|
|
columns = []
|
|
seen = set()
|
|
for row in rows:
|
|
if len(row) != 7:
|
|
die("Gitea salvage schema-only inventory shape mismatch")
|
|
cid = row[0]
|
|
name = str(row[1])
|
|
declared_type = str(row[2] or "")
|
|
not_null = row[3]
|
|
primary_key = row[5]
|
|
hidden = row[6]
|
|
if (
|
|
not isinstance(cid, int)
|
|
or cid < 0
|
|
or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,127}", name)
|
|
or name in seen
|
|
or len(declared_type) > 64
|
|
or re.fullmatch(r"[A-Za-z0-9_(), ]*", declared_type) is None
|
|
or not_null not in (0, 1)
|
|
or not isinstance(primary_key, int)
|
|
or primary_key < 0
|
|
or hidden not in (0, 1, 2, 3)
|
|
):
|
|
die("Gitea salvage schema-only inventory is unsafe")
|
|
seen.add(name)
|
|
columns.append(
|
|
{
|
|
"cid": cid,
|
|
"hidden": int(hidden),
|
|
"name": name,
|
|
"not_null": int(not_null),
|
|
"primary_key": int(primary_key),
|
|
"type": declared_type,
|
|
}
|
|
)
|
|
exists = bool(object_rows or table_list_rows)
|
|
sqlite_schema_kind = None
|
|
sqlite_schema_virtual = None
|
|
if object_type is not None:
|
|
sqlite_schema_kind = object_type
|
|
sqlite_schema_virtual = 1 if virtual_sql else 0
|
|
table_list = None
|
|
if len(table_list_rows) == 1 and len(table_list_rows[0]) == 6:
|
|
table_list = {
|
|
"columns": table_list_rows[0][3],
|
|
"schema": table_list_rows[0][0],
|
|
"strict": table_list_rows[0][5],
|
|
"type": table_list_rows[0][2],
|
|
"without_rowid": table_list_rows[0][4],
|
|
}
|
|
if (
|
|
table_list_supported
|
|
and safe_object
|
|
and table_list["columns"] != len(columns)
|
|
):
|
|
die("Gitea salvage schema-only column count mismatch")
|
|
return {
|
|
"columns": columns,
|
|
"exists": exists,
|
|
"ordinary_main_table": safe_object,
|
|
"object_kind_attestation": (
|
|
"sqlite-master-and-table-list"
|
|
if table_list_supported
|
|
else "sqlite-master-nonvirtual-table"
|
|
),
|
|
"sqlite_schema_kind": sqlite_schema_kind,
|
|
"sqlite_schema_virtual": sqlite_schema_virtual,
|
|
"table": table,
|
|
"table_list": table_list,
|
|
}
|
|
|
|
|
|
def gitea_salvage_unsupported_schema_catalog(connection):
|
|
sqlite_version, sqlite_version_tuple = gitea_salvage_sqlite_version(connection)
|
|
table_list_supported = sqlite_version_tuple >= (3, 37, 0)
|
|
catalog = {
|
|
"database_sha256": GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256,
|
|
"schema": "nodedc.gitea.salvage-unsupported-schema/v1",
|
|
"sqlite_version": sqlite_version,
|
|
"table_list_supported": table_list_supported,
|
|
"tables": [
|
|
gitea_salvage_table_schema(
|
|
connection,
|
|
table,
|
|
table_list_supported,
|
|
)
|
|
for table in GITEA_SALVAGE_UNSUPPORTED_SCHEMA_TABLES
|
|
],
|
|
}
|
|
canonical = canonical_gitea_salvage_evidence(
|
|
catalog,
|
|
"unsupported-state schema catalog",
|
|
)
|
|
return {"catalog": catalog, **canonical}
|
|
|
|
|
|
def gitea_salvage_schema_columns_by_name(schema):
|
|
return {column["name"]: column for column in schema["columns"]}
|
|
|
|
|
|
def gitea_salvage_declared_type_has_integer_affinity(declared_type):
|
|
return "INT" in declared_type.upper()
|
|
|
|
|
|
def gitea_salvage_declared_type_has_text_affinity(declared_type):
|
|
normalized = declared_type.upper()
|
|
return any(token in normalized for token in ("CHAR", "CLOB", "TEXT"))
|
|
|
|
|
|
def gitea_salvage_parse_semantic_topics(value):
|
|
if not isinstance(value, str):
|
|
die("Gitea salvage repository topics value is not text")
|
|
try:
|
|
encoded = value.encode("utf-8")
|
|
except UnicodeEncodeError:
|
|
die("Gitea salvage repository topics value is not valid UTF-8")
|
|
if len(encoded) > 16 * 1024:
|
|
die("Gitea salvage repository topics value exceeds the byte limit")
|
|
# Gitea persists a nil topics slice as the canonical JSON literal `null`.
|
|
# The incident snapshot has exactly this four-byte text representation for
|
|
# all 45 kept repositories. It is semantic empty state, not a topic value.
|
|
# Accept only the exact canonical literal; whitespace, case variants,
|
|
# quoted strings and SQL NULL remain rejected below/by the caller.
|
|
if value == "null":
|
|
return ()
|
|
try:
|
|
parsed = json.loads(value)
|
|
except (json.JSONDecodeError, RecursionError):
|
|
die("Gitea salvage repository topics JSON is invalid")
|
|
if not isinstance(parsed, list) or len(parsed) > 256:
|
|
die("Gitea salvage repository topics JSON is not a bounded array")
|
|
try:
|
|
canonical = json.dumps(
|
|
parsed,
|
|
ensure_ascii=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
)
|
|
except (TypeError, ValueError):
|
|
die("Gitea salvage repository topics JSON is not canonical")
|
|
if value != canonical:
|
|
die("Gitea salvage repository topics JSON is not canonical")
|
|
topics = []
|
|
seen = set()
|
|
for topic in parsed:
|
|
if not isinstance(topic, str):
|
|
die("Gitea salvage repository topic is not text")
|
|
try:
|
|
topic_bytes = topic.encode("utf-8")
|
|
except UnicodeEncodeError:
|
|
die("Gitea salvage repository topic is not valid UTF-8")
|
|
if (
|
|
not topic
|
|
or len(topic_bytes) > 35
|
|
or topic != topic.lower()
|
|
or re.fullmatch(r"[a-z0-9][-.a-z0-9]*", topic) is None
|
|
):
|
|
die("Gitea salvage repository topic is unsafe")
|
|
if topic in seen:
|
|
die("Gitea salvage repository topics contain a duplicate")
|
|
seen.add(topic)
|
|
topics.append(topic)
|
|
if topics != sorted(topics):
|
|
die("Gitea salvage repository topics are not sorted")
|
|
return tuple(topics)
|
|
|
|
|
|
def gitea_salvage_semantic_topics_inventory(connection, kept_repositories):
|
|
normalized = []
|
|
seen = set()
|
|
for record in kept_repositories:
|
|
try:
|
|
repo_id = int(record["repo_id"])
|
|
except (KeyError, TypeError, ValueError):
|
|
die("Gitea salvage semantic-topics repository identity is invalid")
|
|
if repo_id <= 0 or repo_id in seen:
|
|
die("Gitea salvage semantic-topics repository identity is invalid")
|
|
seen.add(repo_id)
|
|
normalized.append(repo_id)
|
|
normalized.sort()
|
|
if not normalized:
|
|
die("Gitea salvage semantic-topics repository set is empty")
|
|
placeholders = ",".join("?" for _ in normalized)
|
|
try:
|
|
rows = connection.execute(
|
|
f"SELECT id,topics,typeof(topics) FROM repository "
|
|
f"WHERE id IN ({placeholders}) ORDER BY id",
|
|
normalized,
|
|
).fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage semantic-topics query failed")
|
|
if len(rows) != len(normalized):
|
|
die("Gitea salvage semantic-topics repository set is incomplete")
|
|
repositories = []
|
|
total_topics = 0
|
|
material_repositories = 0
|
|
serialized_arrays = 0
|
|
serialized_nulls = 0
|
|
for expected_repo_id, row in zip(normalized, rows):
|
|
repo_id, value, value_type = row
|
|
if (
|
|
repo_id != expected_repo_id
|
|
or value_type != "text"
|
|
):
|
|
die("Gitea salvage semantic-topics row is invalid")
|
|
topics = gitea_salvage_parse_semantic_topics(value)
|
|
if value == "null":
|
|
encoding = "json-null"
|
|
serialized_nulls += 1
|
|
else:
|
|
encoding = "json-array"
|
|
serialized_arrays += 1
|
|
count = len(topics)
|
|
total_topics += count
|
|
material_repositories += int(bool(count))
|
|
repositories.append(
|
|
{
|
|
"encoding": encoding,
|
|
"material": bool(count),
|
|
"old_repo_id": repo_id,
|
|
"topic_count": count,
|
|
}
|
|
)
|
|
evidence = {
|
|
"database_sha256": GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256,
|
|
"decision_manifest_sha256": GITEA_SALVAGE_DECISION_MANIFEST_SHA256,
|
|
"material_repositories": material_repositories,
|
|
"repositories": repositories,
|
|
"schema": "nodedc.gitea.salvage-semantic-topics/v2",
|
|
"serialized_arrays": serialized_arrays,
|
|
"serialized_nulls": serialized_nulls,
|
|
"snapshot_uuid": GITEA_SALVAGE_SNAPSHOT_UUID,
|
|
"topics": total_topics,
|
|
}
|
|
canonical = canonical_gitea_salvage_evidence(
|
|
evidence,
|
|
"semantic topics evidence",
|
|
)
|
|
return {"evidence": evidence, **canonical}
|
|
|
|
|
|
def bind_gitea_salvage_decisions_to_snapshot(decisions):
|
|
connection = gitea_salvage_sqlite_connection(GITEA_SALVAGE_SNAPSHOT_DATABASE)
|
|
try:
|
|
quick = [row[0] for row in connection.execute("PRAGMA quick_check").fetchall()]
|
|
if quick != ["ok"]:
|
|
die("Gitea salvage snapshot SQLite quick_check failed")
|
|
user_columns = gitea_salvage_table_columns(connection, "user")
|
|
repository_columns = gitea_salvage_table_columns(connection, "repository")
|
|
required_user_columns = {
|
|
"id",
|
|
"name",
|
|
"lower_name",
|
|
"created_unix",
|
|
"updated_unix",
|
|
"is_admin",
|
|
"is_active",
|
|
"is_restricted",
|
|
}
|
|
required_repository_columns = {
|
|
"id",
|
|
"owner_id",
|
|
"owner_name",
|
|
"name",
|
|
"lower_name",
|
|
"created_unix",
|
|
"updated_unix",
|
|
"is_private",
|
|
"is_archived",
|
|
"is_mirror",
|
|
"is_fork",
|
|
}
|
|
if (
|
|
not required_user_columns.issubset(user_columns)
|
|
or not required_repository_columns.issubset(repository_columns)
|
|
):
|
|
die("Gitea salvage snapshot identity schema mismatch")
|
|
user_rows = connection.execute(
|
|
"SELECT id,name,lower_name,created_unix,updated_unix,is_admin,"
|
|
"is_active,is_restricted FROM user ORDER BY id"
|
|
).fetchall()
|
|
repository_rows = connection.execute(
|
|
"SELECT id,owner_id,owner_name,name,lower_name,created_unix,"
|
|
"updated_unix,is_private,is_archived,is_mirror,is_fork "
|
|
"FROM repository ORDER BY id"
|
|
).fetchall()
|
|
if len(user_rows) != 972 or len(repository_rows) != 2058:
|
|
die("Gitea salvage snapshot identity row count mismatch")
|
|
repositories_per_user = {}
|
|
lower_owner_by_id = {}
|
|
for row in user_rows:
|
|
lower_owner_by_id[int(row["id"])] = str(row["lower_name"])
|
|
for row in repository_rows:
|
|
owner_id = int(row["owner_id"])
|
|
repositories_per_user[owner_id] = repositories_per_user.get(owner_id, 0) + 1
|
|
decision_users = {int(row["user_id"]): row for row in decisions["users"]}
|
|
decision_repositories = {
|
|
int(row["repo_id"]): row for row in decisions["repositories"]
|
|
}
|
|
if (
|
|
set(decision_users) != {int(row["id"]) for row in user_rows}
|
|
or set(decision_repositories)
|
|
!= {int(row["id"]) for row in repository_rows}
|
|
):
|
|
die("Gitea salvage decision bundle is not a full database partition")
|
|
for row in user_rows:
|
|
user_id = int(row["id"])
|
|
fingerprint = {
|
|
"user_id": user_id,
|
|
"owner": str(row["name"]),
|
|
"lower_owner": str(row["lower_name"]),
|
|
"created_raw": str(row["created_unix"]),
|
|
"updated_raw": str(row["updated_unix"]),
|
|
"repository_count": repositories_per_user.get(user_id, 0),
|
|
"is_admin": int(row["is_admin"]),
|
|
"is_active": int(row["is_active"]),
|
|
"is_restricted": int(row["is_restricted"]),
|
|
}
|
|
if (
|
|
canonical_gitea_salvage_record_sha256(fingerprint)
|
|
!= decision_users[user_id]["record_sha256"]
|
|
):
|
|
die(f"Gitea salvage user fingerprint mismatch: {user_id}")
|
|
for row in repository_rows:
|
|
repo_id = int(row["id"])
|
|
owner_id = int(row["owner_id"])
|
|
decision = decision_repositories[repo_id]
|
|
fingerprint = {
|
|
"repo_id": repo_id,
|
|
"owner_id": owner_id,
|
|
"owner": str(row["owner_name"]),
|
|
"slug": str(row["name"]),
|
|
"lower_owner": lower_owner_by_id.get(owner_id),
|
|
"lower_slug": str(row["lower_name"]),
|
|
"repo_relative_path": decision["repo_relative_path"],
|
|
"created_raw": str(row["created_unix"]),
|
|
"updated_raw": str(row["updated_unix"]),
|
|
"is_private": int(row["is_private"]),
|
|
"is_archived": int(row["is_archived"]),
|
|
"is_mirror": int(row["is_mirror"]),
|
|
"is_fork": int(row["is_fork"]),
|
|
}
|
|
if (
|
|
canonical_gitea_salvage_record_sha256(fingerprint)
|
|
!= decision["record_sha256"]
|
|
):
|
|
die(f"Gitea salvage repository fingerprint mismatch: {repo_id}")
|
|
if decision["decision"] == "KEEP" and (
|
|
int(row["is_private"]) != (1 if owner_id == 1 else 0)
|
|
or int(row["is_archived"]) != 0
|
|
or int(row["is_mirror"]) != 0
|
|
or int(row["is_fork"]) != 0
|
|
):
|
|
die(f"Gitea salvage kept repository state mismatch: {repo_id}")
|
|
unsupported = gitea_salvage_unsupported_state_inventory(
|
|
connection,
|
|
decisions["kept_repositories"],
|
|
)
|
|
topics = gitea_salvage_semantic_topics_inventory(
|
|
connection,
|
|
decisions["kept_repositories"],
|
|
)
|
|
closure = gitea_salvage_incident_closure_inventory(
|
|
connection,
|
|
decisions,
|
|
unsupported,
|
|
topics,
|
|
)
|
|
return {
|
|
"closure": closure,
|
|
"quick_check": "ok",
|
|
"users": len(user_rows),
|
|
"repositories": len(repository_rows),
|
|
"topics": topics,
|
|
"unsupported": unsupported,
|
|
}
|
|
finally:
|
|
connection.close()
|
|
|
|
|
|
def gitea_salvage_unsupported_state_inventory(connection, kept_repositories):
|
|
normalized = []
|
|
seen_repo_ids = set()
|
|
for row in kept_repositories:
|
|
if not isinstance(row, dict):
|
|
die("Gitea salvage kept-repository evidence input is invalid")
|
|
try:
|
|
repo_id = int(row["repo_id"])
|
|
owner = str(row["owner"])
|
|
slug = str(row["slug"])
|
|
except (KeyError, TypeError, ValueError):
|
|
die("Gitea salvage kept-repository evidence input is invalid")
|
|
if (
|
|
repo_id <= 0
|
|
or repo_id in seen_repo_ids
|
|
or not re.fullmatch(r"[A-Za-z0-9_.-]{1,255}", owner)
|
|
or not re.fullmatch(r"[A-Za-z0-9_.-]{1,255}", slug)
|
|
):
|
|
die("Gitea salvage kept-repository evidence identity is invalid")
|
|
seen_repo_ids.add(repo_id)
|
|
normalized.append({"old_repo_id": repo_id, "owner": owner, "slug": slug})
|
|
normalized.sort(key=lambda item: item["old_repo_id"])
|
|
if not normalized:
|
|
die("Gitea salvage kept-repository evidence set is empty")
|
|
|
|
kept_repo_ids = tuple(item["old_repo_id"] for item in normalized)
|
|
placeholders = ",".join("?" for _ in kept_repo_ids)
|
|
schema_evidence = gitea_salvage_unsupported_schema_catalog(connection)
|
|
schemas = {
|
|
schema["table"]: schema
|
|
for schema in schema_evidence["catalog"]["tables"]
|
|
}
|
|
schema_columns = {
|
|
table: gitea_salvage_schema_columns_by_name(schema)
|
|
for table, schema in schemas.items()
|
|
}
|
|
missing_schema = set()
|
|
mismatched_schema = set()
|
|
anomalies = []
|
|
per_repository = []
|
|
by_repo_id = {}
|
|
count_labels = tuple(
|
|
label for label, _table, _column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TABLES
|
|
)
|
|
for identity in normalized:
|
|
record = {
|
|
**identity,
|
|
"attachment_links": {
|
|
label: {"logical_bytes": None, "rows": None}
|
|
for label in ("comment", "issue", "multi_link", "release", "unlinked")
|
|
},
|
|
"attachments": {
|
|
"association_rows": None,
|
|
"logical_bytes": None,
|
|
},
|
|
"counts": {label: None for label in count_labels},
|
|
"lfs": {
|
|
"association_logical_bytes": None,
|
|
"association_rows": None,
|
|
"distinct_oids": None,
|
|
},
|
|
"repo_unit_types": None,
|
|
"repository_hints": {
|
|
column: None
|
|
for column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_NUMERIC_HINTS
|
|
},
|
|
"repository_metadata_present": {
|
|
column: None
|
|
for column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TEXT_METADATA
|
|
},
|
|
}
|
|
per_repository.append(record)
|
|
by_repo_id[identity["old_repo_id"]] = record
|
|
|
|
def require_column(table, column, affinity=None):
|
|
schema = schemas[table]
|
|
columns = schema_columns[table]
|
|
if schema["exists"] and not schema["ordinary_main_table"]:
|
|
mismatched_schema.add(f"{table}:ordinary-main-table")
|
|
return False
|
|
if not schema["exists"] or column not in columns:
|
|
missing_schema.add(f"{table}.{column}")
|
|
return False
|
|
declared_type = columns[column]["type"]
|
|
if affinity == "integer" and not gitea_salvage_declared_type_has_integer_affinity(
|
|
declared_type
|
|
):
|
|
mismatched_schema.add(f"{table}.{column}:integer-affinity")
|
|
return False
|
|
if affinity == "text" and not gitea_salvage_declared_type_has_text_affinity(
|
|
declared_type
|
|
):
|
|
mismatched_schema.add(f"{table}.{column}:text-affinity")
|
|
return False
|
|
return True
|
|
|
|
metric_available = {}
|
|
for label, table, column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TABLES:
|
|
available = require_column(table, column, "integer")
|
|
metric_available[label] = available
|
|
if not available:
|
|
continue
|
|
try:
|
|
invalid_repo_ids = int(
|
|
connection.execute(
|
|
f'SELECT COUNT(*) FROM "{table}" '
|
|
f'WHERE "{column}" IN ({placeholders}) '
|
|
f'AND typeof("{column}") != \'integer\'',
|
|
kept_repo_ids,
|
|
).fetchone()[0]
|
|
)
|
|
grouped = connection.execute(
|
|
f'SELECT "{column}",COUNT(*) FROM "{table}" '
|
|
f'WHERE "{column}" IN ({placeholders}) GROUP BY "{column}"',
|
|
kept_repo_ids,
|
|
).fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage direct-relation evidence query failed")
|
|
if invalid_repo_ids:
|
|
anomalies.append(f"{table}.{column}:invalid_repo_ids={invalid_repo_ids}")
|
|
metric_available[label] = False
|
|
continue
|
|
for repo_id, count in grouped:
|
|
if not isinstance(repo_id, int) or repo_id not in by_repo_id:
|
|
die("Gitea salvage direct-relation grouping escaped the keep set")
|
|
by_repo_id[repo_id]["counts"][label] = int(count)
|
|
for record in per_repository:
|
|
if record["counts"][label] is None:
|
|
record["counts"][label] = 0
|
|
|
|
repo_unit_requirements = [
|
|
require_column("repo_unit", column, "integer")
|
|
for column in ("repo_id", "type")
|
|
]
|
|
repo_unit_ready = all(repo_unit_requirements)
|
|
if repo_unit_ready:
|
|
try:
|
|
unit_rows = connection.execute(
|
|
f"SELECT repo_id,type,COUNT(*) FROM repo_unit "
|
|
f"WHERE repo_id IN ({placeholders}) GROUP BY repo_id,type",
|
|
kept_repo_ids,
|
|
).fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage repo-unit evidence query failed")
|
|
unit_types = {repo_id: {} for repo_id in kept_repo_ids}
|
|
for repo_id, unit_type, count in unit_rows:
|
|
if (
|
|
not isinstance(repo_id, int)
|
|
or repo_id not in by_repo_id
|
|
or not isinstance(unit_type, int)
|
|
or unit_type < 0
|
|
):
|
|
anomalies.append("repo_unit.repo_id/type:invalid_group")
|
|
repo_unit_ready = False
|
|
break
|
|
unit_types[repo_id][str(unit_type)] = int(count)
|
|
if repo_unit_ready:
|
|
for repo_id, values in unit_types.items():
|
|
by_repo_id[repo_id]["repo_unit_types"] = values
|
|
|
|
repository_id_ready = require_column("repository", "id", "integer")
|
|
for column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TEXT_METADATA:
|
|
column_ready = require_column("repository", column, "text")
|
|
if not repository_id_ready or not column_ready:
|
|
continue
|
|
try:
|
|
invalid = int(
|
|
connection.execute(
|
|
f'SELECT COUNT(*) FROM repository WHERE id IN ({placeholders}) '
|
|
f'AND typeof("{column}") NOT IN (\'null\',\'text\')',
|
|
kept_repo_ids,
|
|
).fetchone()[0]
|
|
)
|
|
rows = connection.execute(
|
|
f'SELECT id,CASE WHEN "{column}" IS NULL OR "{column}" = \'\' '
|
|
f'THEN 0 ELSE 1 END FROM repository WHERE id IN ({placeholders})',
|
|
kept_repo_ids,
|
|
).fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage repository metadata evidence query failed")
|
|
if invalid:
|
|
anomalies.append(f"repository.{column}:invalid_values={invalid}")
|
|
continue
|
|
for repo_id, present in rows:
|
|
if repo_id not in by_repo_id or present not in (0, 1):
|
|
die("Gitea salvage repository metadata grouping is invalid")
|
|
by_repo_id[repo_id]["repository_metadata_present"][column] = bool(present)
|
|
|
|
for column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_NUMERIC_HINTS:
|
|
column_ready = require_column("repository", column, "integer")
|
|
if not repository_id_ready or not column_ready:
|
|
continue
|
|
try:
|
|
rows = connection.execute(
|
|
f'SELECT id,"{column}",typeof("{column}") FROM repository '
|
|
f'WHERE id IN ({placeholders})',
|
|
kept_repo_ids,
|
|
).fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage repository hint evidence query failed")
|
|
valid = True
|
|
for repo_id, value, value_type in rows:
|
|
if (
|
|
repo_id not in by_repo_id
|
|
or value_type not in ("integer", "null")
|
|
or (value is not None and (not isinstance(value, int) or value < 0))
|
|
):
|
|
anomalies.append(f"repository.{column}:invalid_value")
|
|
valid = False
|
|
break
|
|
if valid:
|
|
for repo_id, value, _value_type in rows:
|
|
by_repo_id[repo_id]["repository_hints"][column] = (
|
|
0 if value is None else int(value)
|
|
)
|
|
|
|
lfs_requirements = [
|
|
require_column("lfs_meta_object", "repository_id", "integer"),
|
|
require_column("lfs_meta_object", "oid", "text"),
|
|
require_column("lfs_meta_object", "size", "integer"),
|
|
]
|
|
lfs_required = all(lfs_requirements) and repository_id_ready
|
|
lfs_aggregate = {
|
|
"association_logical_bytes": None,
|
|
"association_rows": None,
|
|
"distinct_oids": None,
|
|
"invalid_oid_rows": None,
|
|
"invalid_related_repository_rows": None,
|
|
"invalid_related_size_rows": None,
|
|
"invalid_size_rows": None,
|
|
"non_kept_shared_logical_bytes": None,
|
|
"non_kept_shared_oids": None,
|
|
"physical_presence": "not-inventoried",
|
|
"orphan_related_repository_rows": None,
|
|
"size_conflict_oids": None,
|
|
"unique_logical_bytes": None,
|
|
}
|
|
if lfs_required:
|
|
try:
|
|
invalid_oid_rows = int(
|
|
connection.execute(
|
|
f"SELECT COUNT(*) FROM lfs_meta_object "
|
|
f"WHERE repository_id IN ({placeholders}) AND ("
|
|
"typeof(oid) != 'text' OR length(oid) != 64 OR "
|
|
"oid GLOB '*[^0-9a-f]*')",
|
|
kept_repo_ids,
|
|
).fetchone()[0]
|
|
)
|
|
invalid_size_rows = int(
|
|
connection.execute(
|
|
f"SELECT COUNT(*) FROM lfs_meta_object "
|
|
f"WHERE repository_id IN ({placeholders}) AND ("
|
|
"typeof(size) != 'integer' OR size < 0 OR size > ?)",
|
|
(*kept_repo_ids, GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES),
|
|
).fetchone()[0]
|
|
)
|
|
invalid_related_size_rows = int(
|
|
connection.execute(
|
|
f"SELECT COUNT(*) FROM lfs_meta_object WHERE oid IN ("
|
|
f"SELECT oid FROM lfs_meta_object "
|
|
f"WHERE repository_id IN ({placeholders})) AND ("
|
|
"typeof(size) != 'integer' OR size < 0 OR size > ?)",
|
|
(*kept_repo_ids, GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES),
|
|
).fetchone()[0]
|
|
)
|
|
invalid_related_repository_rows = int(
|
|
connection.execute(
|
|
f"SELECT COUNT(*) FROM lfs_meta_object WHERE oid IN ("
|
|
f"SELECT oid FROM lfs_meta_object "
|
|
f"WHERE repository_id IN ({placeholders})) AND ("
|
|
"typeof(repository_id) != 'integer' OR repository_id <= 0)",
|
|
kept_repo_ids,
|
|
).fetchone()[0]
|
|
)
|
|
orphan_related_repository_rows = int(
|
|
connection.execute(
|
|
f"SELECT COUNT(*) FROM lfs_meta_object AS related WHERE oid IN ("
|
|
f"SELECT oid FROM lfs_meta_object "
|
|
f"WHERE repository_id IN ({placeholders})) "
|
|
"AND typeof(related.repository_id) = 'integer' "
|
|
"AND related.repository_id > 0 AND NOT EXISTS ("
|
|
"SELECT 1 FROM repository "
|
|
"WHERE repository.id=related.repository_id)",
|
|
kept_repo_ids,
|
|
).fetchone()[0]
|
|
)
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage LFS validation query failed")
|
|
lfs_aggregate["invalid_oid_rows"] = invalid_oid_rows
|
|
lfs_aggregate["invalid_related_repository_rows"] = (
|
|
invalid_related_repository_rows
|
|
)
|
|
lfs_aggregate["invalid_related_size_rows"] = invalid_related_size_rows
|
|
lfs_aggregate["invalid_size_rows"] = invalid_size_rows
|
|
lfs_aggregate["orphan_related_repository_rows"] = (
|
|
orphan_related_repository_rows
|
|
)
|
|
if invalid_oid_rows:
|
|
anomalies.append(f"lfs_meta_object.oid:invalid_rows={invalid_oid_rows}")
|
|
if invalid_size_rows:
|
|
anomalies.append(f"lfs_meta_object.size:invalid_rows={invalid_size_rows}")
|
|
if invalid_related_size_rows:
|
|
anomalies.append(
|
|
"lfs_meta_object.size:"
|
|
f"invalid_related_rows={invalid_related_size_rows}"
|
|
)
|
|
if invalid_related_repository_rows:
|
|
anomalies.append(
|
|
"lfs_meta_object.repository_id:"
|
|
f"invalid_related_rows={invalid_related_repository_rows}"
|
|
)
|
|
if orphan_related_repository_rows:
|
|
anomalies.append(
|
|
"lfs_meta_object.repository_id:"
|
|
f"orphan_related_rows={orphan_related_repository_rows}"
|
|
)
|
|
if not any(
|
|
(
|
|
invalid_oid_rows,
|
|
invalid_size_rows,
|
|
invalid_related_size_rows,
|
|
invalid_related_repository_rows,
|
|
orphan_related_repository_rows,
|
|
)
|
|
):
|
|
try:
|
|
grouped = connection.execute(
|
|
f"SELECT repository_id,COUNT(*),COUNT(DISTINCT oid),"
|
|
f"COALESCE(SUM(size),0) FROM lfs_meta_object "
|
|
f"WHERE repository_id IN ({placeholders}) GROUP BY repository_id",
|
|
kept_repo_ids,
|
|
).fetchall()
|
|
unique = connection.execute(
|
|
f"SELECT COUNT(*),COALESCE(SUM(max_size),0) FROM ("
|
|
f"SELECT oid,MAX(size) AS max_size FROM lfs_meta_object "
|
|
f"WHERE repository_id IN ({placeholders}) GROUP BY oid)",
|
|
kept_repo_ids,
|
|
).fetchone()
|
|
conflicts = int(
|
|
connection.execute(
|
|
f"SELECT COUNT(*) FROM (SELECT oid FROM lfs_meta_object "
|
|
f"WHERE oid IN (SELECT oid FROM lfs_meta_object "
|
|
f"WHERE repository_id IN ({placeholders})) "
|
|
f"GROUP BY oid HAVING MIN(size) != MAX(size))",
|
|
kept_repo_ids,
|
|
).fetchone()[0]
|
|
)
|
|
shared = connection.execute(
|
|
f"SELECT COUNT(*),COALESCE(SUM(kept_size),0) FROM ("
|
|
f"SELECT kept.oid,MAX(kept.size) AS kept_size "
|
|
f"FROM lfs_meta_object AS kept "
|
|
f"WHERE kept.repository_id IN ({placeholders}) AND EXISTS ("
|
|
f"SELECT 1 FROM lfs_meta_object AS other "
|
|
f"WHERE other.oid=kept.oid AND other.repository_id NOT IN ({placeholders})"
|
|
f") GROUP BY kept.oid)",
|
|
(*kept_repo_ids, *kept_repo_ids),
|
|
).fetchone()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage LFS aggregate query failed")
|
|
association_rows = 0
|
|
association_bytes = 0
|
|
distinct_oids_per_repo = 0
|
|
for repo_id, rows, distinct_oids, logical_bytes in grouped:
|
|
if repo_id not in by_repo_id:
|
|
die("Gitea salvage LFS grouping escaped the keep set")
|
|
values = (rows, distinct_oids, logical_bytes)
|
|
if any(not isinstance(value, int) or value < 0 for value in values):
|
|
die("Gitea salvage LFS aggregate is invalid")
|
|
if logical_bytes > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES:
|
|
die("Gitea salvage LFS aggregate exceeds the byte limit")
|
|
by_repo_id[repo_id]["lfs"] = {
|
|
"association_logical_bytes": logical_bytes,
|
|
"association_rows": rows,
|
|
"distinct_oids": distinct_oids,
|
|
}
|
|
association_rows += rows
|
|
association_bytes += logical_bytes
|
|
distinct_oids_per_repo += distinct_oids
|
|
for record in per_repository:
|
|
if record["lfs"]["association_rows"] is None:
|
|
record["lfs"] = {
|
|
"association_logical_bytes": 0,
|
|
"association_rows": 0,
|
|
"distinct_oids": 0,
|
|
}
|
|
unique_oids, unique_bytes = unique
|
|
shared_oids, shared_bytes = shared
|
|
for value in (
|
|
association_rows,
|
|
association_bytes,
|
|
distinct_oids_per_repo,
|
|
unique_oids,
|
|
unique_bytes,
|
|
shared_oids,
|
|
shared_bytes,
|
|
conflicts,
|
|
):
|
|
if not isinstance(value, int) or value < 0:
|
|
die("Gitea salvage LFS aggregate is invalid")
|
|
if max(association_bytes, unique_bytes, shared_bytes) > (
|
|
GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES
|
|
):
|
|
die("Gitea salvage LFS aggregate exceeds the byte limit")
|
|
lfs_aggregate.update(
|
|
{
|
|
"association_logical_bytes": association_bytes,
|
|
"association_rows": association_rows,
|
|
"distinct_oids": unique_oids,
|
|
"non_kept_shared_logical_bytes": shared_bytes,
|
|
"non_kept_shared_oids": shared_oids,
|
|
"size_conflict_oids": conflicts,
|
|
"unique_logical_bytes": unique_bytes,
|
|
}
|
|
)
|
|
if conflicts:
|
|
anomalies.append(f"lfs_meta_object.size:conflict_oids={conflicts}")
|
|
|
|
attachment_requirements = [
|
|
require_column("attachment", column, "integer")
|
|
for column in ("id", "repo_id", "issue_id", "release_id", "comment_id", "size")
|
|
]
|
|
attachment_required = all(attachment_requirements)
|
|
attachment_aggregate = {
|
|
"association_rows": None,
|
|
"invalid_association_rows": None,
|
|
"invalid_size_rows": None,
|
|
"link_splits": {
|
|
label: {"logical_bytes": None, "rows": None}
|
|
for label in ("comment", "issue", "multi_link", "release", "unlinked")
|
|
},
|
|
"logical_bytes": None,
|
|
"physical_presence": "not-inventoried",
|
|
}
|
|
if attachment_required:
|
|
try:
|
|
invalid_associations = int(
|
|
connection.execute(
|
|
f"SELECT COUNT(*) FROM attachment WHERE repo_id IN ({placeholders}) "
|
|
"AND (typeof(repo_id) != 'integer' OR "
|
|
"typeof(issue_id) NOT IN ('integer','null') OR "
|
|
"typeof(release_id) NOT IN ('integer','null') OR "
|
|
"typeof(comment_id) NOT IN ('integer','null') OR "
|
|
"COALESCE(issue_id,0) < 0 OR COALESCE(release_id,0) < 0 OR "
|
|
"COALESCE(comment_id,0) < 0)",
|
|
kept_repo_ids,
|
|
).fetchone()[0]
|
|
)
|
|
invalid_sizes = int(
|
|
connection.execute(
|
|
f"SELECT COUNT(*) FROM attachment WHERE repo_id IN ({placeholders}) "
|
|
"AND (typeof(size) != 'integer' OR size < 0 OR size > ?)",
|
|
(*kept_repo_ids, GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES),
|
|
).fetchone()[0]
|
|
)
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage attachment validation query failed")
|
|
attachment_aggregate["invalid_association_rows"] = invalid_associations
|
|
attachment_aggregate["invalid_size_rows"] = invalid_sizes
|
|
if invalid_associations:
|
|
anomalies.append(f"attachment.association:invalid_rows={invalid_associations}")
|
|
if invalid_sizes:
|
|
anomalies.append(f"attachment.size:invalid_rows={invalid_sizes}")
|
|
if not invalid_associations and not invalid_sizes:
|
|
try:
|
|
grouped = connection.execute(
|
|
f"SELECT repo_id,COUNT(*),COALESCE(SUM(size),0) "
|
|
f"FROM attachment WHERE repo_id IN ({placeholders}) GROUP BY repo_id",
|
|
kept_repo_ids,
|
|
).fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage attachment aggregate query failed")
|
|
association_rows = 0
|
|
logical_bytes = 0
|
|
for repo_id, rows, size_bytes in grouped:
|
|
if (
|
|
repo_id not in by_repo_id
|
|
or not isinstance(rows, int)
|
|
or rows < 0
|
|
or not isinstance(size_bytes, int)
|
|
or size_bytes < 0
|
|
or size_bytes > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES
|
|
):
|
|
die("Gitea salvage attachment aggregate is invalid")
|
|
by_repo_id[repo_id]["attachments"] = {
|
|
"association_rows": rows,
|
|
"logical_bytes": size_bytes,
|
|
}
|
|
association_rows += rows
|
|
logical_bytes += size_bytes
|
|
for record in per_repository:
|
|
if record["attachments"]["association_rows"] is None:
|
|
record["attachments"] = {
|
|
"association_rows": 0,
|
|
"logical_bytes": 0,
|
|
}
|
|
if logical_bytes > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES:
|
|
die("Gitea salvage attachment aggregate exceeds the byte limit")
|
|
attachment_aggregate["association_rows"] = association_rows
|
|
attachment_aggregate["logical_bytes"] = logical_bytes
|
|
link_predicates = {
|
|
"comment": "COALESCE(comment_id,0) > 0",
|
|
"issue": "COALESCE(issue_id,0) > 0",
|
|
"multi_link": "((COALESCE(issue_id,0) > 0) + (COALESCE(release_id,0) > 0) + (COALESCE(comment_id,0) > 0)) > 1",
|
|
"release": "COALESCE(release_id,0) > 0",
|
|
"unlinked": "COALESCE(issue_id,0) = 0 AND COALESCE(release_id,0) = 0 AND COALESCE(comment_id,0) = 0",
|
|
}
|
|
for label, predicate in link_predicates.items():
|
|
try:
|
|
split_rows = connection.execute(
|
|
f"SELECT repo_id,COUNT(*),COALESCE(SUM(size),0) "
|
|
f"FROM attachment WHERE repo_id IN ({placeholders}) "
|
|
f"AND ({predicate}) GROUP BY repo_id",
|
|
kept_repo_ids,
|
|
).fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die("Gitea salvage attachment link-split query failed")
|
|
total_rows = 0
|
|
total_bytes = 0
|
|
for repo_id, rows, size_bytes in split_rows:
|
|
if (
|
|
repo_id not in by_repo_id
|
|
or not isinstance(rows, int)
|
|
or rows < 0
|
|
or not isinstance(size_bytes, int)
|
|
or size_bytes < 0
|
|
):
|
|
die("Gitea salvage attachment link split is invalid")
|
|
by_repo_id[repo_id]["attachment_links"][label] = {
|
|
"logical_bytes": size_bytes,
|
|
"rows": rows,
|
|
}
|
|
total_rows += rows
|
|
total_bytes += size_bytes
|
|
for record in per_repository:
|
|
if record["attachment_links"][label]["rows"] is None:
|
|
record["attachment_links"][label] = {
|
|
"logical_bytes": 0,
|
|
"rows": 0,
|
|
}
|
|
if total_bytes > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES:
|
|
die("Gitea salvage attachment link split exceeds the byte limit")
|
|
attachment_aggregate["link_splits"][label] = {
|
|
"logical_bytes": total_bytes,
|
|
"rows": total_rows,
|
|
}
|
|
|
|
direct_totals = {
|
|
label: (
|
|
sum(record["counts"][label] for record in per_repository)
|
|
if metric_available[label]
|
|
and all(record["counts"][label] is not None for record in per_repository)
|
|
else None
|
|
)
|
|
for label in count_labels
|
|
}
|
|
metadata_totals = {
|
|
column: (
|
|
sum(
|
|
1
|
|
for record in per_repository
|
|
if record["repository_metadata_present"][column] is True
|
|
)
|
|
if all(
|
|
record["repository_metadata_present"][column] is not None
|
|
for record in per_repository
|
|
)
|
|
else None
|
|
)
|
|
for column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TEXT_METADATA
|
|
}
|
|
nonzero_categories = sorted(
|
|
[label for label, value in direct_totals.items() if isinstance(value, int) and value]
|
|
+ [
|
|
f"repository_metadata:{column}"
|
|
for column, value in metadata_totals.items()
|
|
if isinstance(value, int) and value
|
|
]
|
|
+ (
|
|
["repo_units"]
|
|
if repo_unit_ready
|
|
and any(record["repo_unit_types"] for record in per_repository)
|
|
else []
|
|
)
|
|
+ [
|
|
f"repository_hint:{column}"
|
|
for column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_NUMERIC_HINTS
|
|
if any(
|
|
isinstance(record["repository_hints"][column], int)
|
|
and record["repository_hints"][column] > 0
|
|
for record in per_repository
|
|
)
|
|
]
|
|
)
|
|
schema_review_matches = (
|
|
GITEA_SALVAGE_EXPECTED_UNSUPPORTED_SCHEMA_SHA256 is not None
|
|
and schema_evidence["sha256"]
|
|
== GITEA_SALVAGE_EXPECTED_UNSUPPORTED_SCHEMA_SHA256
|
|
)
|
|
coverage_blockers = [
|
|
"issue-pull-dependent-closure-unreviewed",
|
|
"package-blob-closure-unreviewed",
|
|
"actions-artifact-closure-unreviewed",
|
|
"lfs-physical-object-inventory-unreviewed",
|
|
"attachment-physical-object-inventory-unreviewed",
|
|
]
|
|
if not schema_review_matches:
|
|
coverage_blockers.append("unsupported-schema-catalog-unreviewed")
|
|
if not schema_evidence["catalog"]["table_list_supported"]:
|
|
coverage_blockers.append("sqlite-table-list-object-kind-unavailable")
|
|
report = {
|
|
"aggregates": {
|
|
"attachments": attachment_aggregate,
|
|
"direct_relation_counts": direct_totals,
|
|
"lfs": lfs_aggregate,
|
|
"repository_metadata_presence": metadata_totals,
|
|
},
|
|
"anomalies": sorted(set(anomalies)),
|
|
"coverage": {
|
|
"attachments": "database-metadata-only-no-physical-presence-claim",
|
|
"direct_repository_relations": "counted-per-kept-repository",
|
|
"lfs": "database-associations-and-logical-bytes-no-physical-presence-claim",
|
|
"repository_hints": "denormalized-non-authoritative",
|
|
"schema_only_unreviewed_tables": [
|
|
"action_artifact",
|
|
"action_run_index",
|
|
"action_run_job",
|
|
"action_task",
|
|
"comment",
|
|
"issue_assignees",
|
|
"issue_content_history",
|
|
"issue_dependency",
|
|
"issue_label",
|
|
"issue_user",
|
|
"issue_watch",
|
|
"notification",
|
|
"package_blob",
|
|
"package_file",
|
|
"package_property",
|
|
"package_version",
|
|
"project",
|
|
"project_board",
|
|
"project_issue",
|
|
"pull_auto_merge",
|
|
"reaction",
|
|
"review",
|
|
"review_state",
|
|
"stopwatch",
|
|
"tracked_time",
|
|
],
|
|
},
|
|
"coverage_blockers": sorted(coverage_blockers),
|
|
"database_sha256": GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256,
|
|
"decision_manifest_sha256": GITEA_SALVAGE_DECISION_MANIFEST_SHA256,
|
|
"direct_relation_contract": sorted(
|
|
(
|
|
{
|
|
"label": label,
|
|
"repository_column": column,
|
|
"table": table,
|
|
}
|
|
for label, table, column in GITEA_SALVAGE_UNSUPPORTED_REPOSITORY_TABLES
|
|
),
|
|
key=lambda item: item["label"],
|
|
),
|
|
"kept_repository_ids": list(kept_repo_ids),
|
|
"material_present": bool(nonzero_categories),
|
|
"nonzero_categories": nonzero_categories,
|
|
"per_repository": per_repository,
|
|
"schema": "nodedc.gitea.salvage-unsupported-state/v2",
|
|
"schema_catalog": schema_evidence["catalog"],
|
|
"schema_catalog_sha256": schema_evidence["sha256"],
|
|
"schema_mismatch": sorted(mismatched_schema),
|
|
"schema_missing": sorted(missing_schema),
|
|
"schema_review": {
|
|
"expected_sha256": GITEA_SALVAGE_EXPECTED_UNSUPPORTED_SCHEMA_SHA256,
|
|
"matches": schema_review_matches,
|
|
},
|
|
"snapshot_uuid": GITEA_SALVAGE_SNAPSHOT_UUID,
|
|
}
|
|
canonical = canonical_gitea_salvage_evidence(
|
|
report,
|
|
"unsupported-state report",
|
|
)
|
|
return {"report": report, **canonical}
|
|
|
|
|
|
def gitea_salvage_closure_actor_class(user_id, user_classes, label):
|
|
if (
|
|
not isinstance(user_id, int)
|
|
or isinstance(user_id, bool)
|
|
or user_id < 0
|
|
):
|
|
die(f"Gitea salvage closure {label} actor identity is invalid")
|
|
if user_id == 0:
|
|
return "system-or-external"
|
|
actor_class = user_classes.get(user_id)
|
|
if actor_class is None:
|
|
die(f"Gitea salvage closure {label} actor is outside user decisions")
|
|
return actor_class
|
|
|
|
|
|
def gitea_salvage_closure_metric_template(label):
|
|
return {
|
|
"actor_classes": {
|
|
"deleted": 0,
|
|
"kept": 0,
|
|
"system-or-external": 0,
|
|
},
|
|
"logical_bytes": 0,
|
|
"numeric_totals": {
|
|
column: 0
|
|
for column in GITEA_SALVAGE_CLOSURE_NUMERIC_COLUMNS.get(label, ())
|
|
},
|
|
"rows": 0,
|
|
"text_bytes": {
|
|
column: 0
|
|
for column in GITEA_SALVAGE_CLOSURE_TEXT_COLUMNS.get(label, ())
|
|
},
|
|
}
|
|
|
|
|
|
def gitea_salvage_closure_add_metric(
|
|
record,
|
|
label,
|
|
actor_class=None,
|
|
logical_bytes=0,
|
|
numeric_values=None,
|
|
text_bytes=None,
|
|
):
|
|
metric = record["closure"][label]
|
|
metric["rows"] += 1
|
|
if actor_class is not None:
|
|
if actor_class not in metric["actor_classes"]:
|
|
die("Gitea salvage closure actor classification is invalid")
|
|
metric["actor_classes"][actor_class] += 1
|
|
if (
|
|
not isinstance(logical_bytes, int)
|
|
or isinstance(logical_bytes, bool)
|
|
or logical_bytes < 0
|
|
or logical_bytes > GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES
|
|
):
|
|
die("Gitea salvage closure logical-byte aggregate is invalid")
|
|
metric["logical_bytes"] += logical_bytes
|
|
if metric["logical_bytes"] > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES:
|
|
die("Gitea salvage closure logical-byte aggregate exceeds the limit")
|
|
for column, value in (numeric_values or {}).items():
|
|
if (
|
|
column not in metric["numeric_totals"]
|
|
or not isinstance(value, int)
|
|
or isinstance(value, bool)
|
|
or value < 0
|
|
or value > GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES
|
|
):
|
|
die("Gitea salvage closure declared numeric value is invalid")
|
|
metric["numeric_totals"][column] += value
|
|
if metric["numeric_totals"][column] > (
|
|
GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES
|
|
):
|
|
die("Gitea salvage closure declared numeric total exceeds the limit")
|
|
for column, value in (text_bytes or {}).items():
|
|
if (
|
|
column not in metric["text_bytes"]
|
|
or not isinstance(value, int)
|
|
or isinstance(value, bool)
|
|
or value < 0
|
|
or value > GITEA_SALVAGE_CLOSURE_MAX_TEXT_BYTES_PER_FIELD
|
|
):
|
|
die("Gitea salvage closure text-byte aggregate is invalid")
|
|
metric["text_bytes"][column] += value
|
|
if metric["text_bytes"][column] > (
|
|
GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES
|
|
):
|
|
die("Gitea salvage closure text-byte aggregate exceeds the limit")
|
|
|
|
|
|
def gitea_salvage_incident_closure_inventory(
|
|
connection,
|
|
decisions,
|
|
unsupported,
|
|
topics,
|
|
):
|
|
if (
|
|
unsupported.get("sha256")
|
|
!= GITEA_SALVAGE_EXPECTED_UNSUPPORTED_REPORT_SHA256
|
|
or (unsupported.get("report") or {}).get("schema_catalog_sha256")
|
|
!= GITEA_SALVAGE_DISPOSITION_UNSUPPORTED_SCHEMA_SHA256
|
|
or topics.get("sha256") != GITEA_SALVAGE_DISPOSITION_TOPICS_SHA256
|
|
):
|
|
die("Gitea salvage closure predecessor evidence mismatch")
|
|
report = unsupported["report"]
|
|
if (
|
|
report.get("database_sha256")
|
|
!= GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256
|
|
or report.get("decision_manifest_sha256")
|
|
!= GITEA_SALVAGE_DECISION_MANIFEST_SHA256
|
|
or report.get("snapshot_uuid") != GITEA_SALVAGE_SNAPSHOT_UUID
|
|
or report.get("schema_missing") != []
|
|
or report.get("schema_mismatch") != []
|
|
or report.get("anomalies") != []
|
|
):
|
|
die("Gitea salvage closure source report is not clean")
|
|
|
|
user_classes = {}
|
|
for row in decisions.get("users", []):
|
|
try:
|
|
user_id = int(row["user_id"])
|
|
except (KeyError, TypeError, ValueError):
|
|
die("Gitea salvage closure user decision is invalid")
|
|
decision = row.get("decision")
|
|
if (
|
|
user_id <= 0
|
|
or user_id in user_classes
|
|
or decision not in {"KEEP_ACTIVE", "KEEP_LOCKED", "DELETE"}
|
|
):
|
|
die("Gitea salvage closure user decision is invalid")
|
|
user_classes[user_id] = (
|
|
"deleted" if decision == "DELETE" else "kept"
|
|
)
|
|
if (
|
|
len(user_classes) != 972
|
|
or sum(value == "kept" for value in user_classes.values()) != 10
|
|
or sum(value == "deleted" for value in user_classes.values()) != 962
|
|
):
|
|
die("Gitea salvage closure user partition mismatch")
|
|
|
|
kept_repo_ids = []
|
|
all_repo_ids = set()
|
|
for row in decisions.get("repositories", []):
|
|
try:
|
|
repo_id = int(row["repo_id"])
|
|
except (KeyError, TypeError, ValueError):
|
|
die("Gitea salvage closure repository decision is invalid")
|
|
if repo_id <= 0 or repo_id in all_repo_ids:
|
|
die("Gitea salvage closure repository decision is invalid")
|
|
all_repo_ids.add(repo_id)
|
|
if row.get("decision") == "KEEP":
|
|
kept_repo_ids.append(repo_id)
|
|
elif row.get("decision") != "DELETE":
|
|
die("Gitea salvage closure repository decision is invalid")
|
|
kept_repo_ids.sort()
|
|
if len(all_repo_ids) != 2058 or len(kept_repo_ids) != 45:
|
|
die("Gitea salvage closure repository partition mismatch")
|
|
if report.get("kept_repository_ids") != kept_repo_ids:
|
|
die("Gitea salvage closure kept-repository evidence mismatch")
|
|
kept_repo_set = set(kept_repo_ids)
|
|
placeholders = ",".join("?" for _ in kept_repo_ids)
|
|
|
|
schemas = {
|
|
row["table"]: row
|
|
for row in (report.get("schema_catalog") or {}).get("tables", [])
|
|
if isinstance(row, dict) and isinstance(row.get("table"), str)
|
|
}
|
|
required_schema = {}
|
|
closure_extra_schema = {}
|
|
|
|
def require_columns(table, columns):
|
|
required_schema.setdefault(table, set()).update(columns)
|
|
schema = schemas.get(table)
|
|
if (
|
|
not isinstance(schema, dict)
|
|
or schema.get("ordinary_main_table") is not True
|
|
):
|
|
die(f"Gitea salvage closure table is not an ordinary table: {table}")
|
|
available = {
|
|
column.get("name")
|
|
for column in schema.get("columns", [])
|
|
if isinstance(column, dict)
|
|
}
|
|
if not set(columns).issubset(available):
|
|
die(f"Gitea salvage closure schema is missing required columns: {table}")
|
|
|
|
def require_extra_columns(table, columns):
|
|
if table != "team":
|
|
die("Gitea salvage closure extra schema table is outside the registry")
|
|
table_list_supported = bool(
|
|
(report.get("schema_catalog") or {}).get("table_list_supported")
|
|
)
|
|
schema = gitea_salvage_table_schema(
|
|
connection,
|
|
table,
|
|
table_list_supported,
|
|
allowed_tables={"team"},
|
|
)
|
|
available = {
|
|
column.get("name")
|
|
for column in schema.get("columns", [])
|
|
if isinstance(column, dict)
|
|
}
|
|
if (
|
|
schema.get("ordinary_main_table") is not True
|
|
or not set(columns).issubset(available)
|
|
):
|
|
die(f"Gitea salvage closure extra schema is unsafe: {table}")
|
|
required_schema.setdefault(table, set()).update(columns)
|
|
closure_extra_schema[table] = schema
|
|
|
|
def fetch_rows(label, sql, parameters):
|
|
try:
|
|
rows = connection.execute(sql, parameters).fetchall()
|
|
except sqlite3.DatabaseError:
|
|
die(f"Gitea salvage closure {label} query failed")
|
|
if len(rows) > GITEA_SALVAGE_CLOSURE_MAX_ROWS_PER_RELATION:
|
|
die(f"Gitea salvage closure {label} exceeds the row limit")
|
|
return rows
|
|
|
|
per_repository = [
|
|
{
|
|
"closure": {
|
|
label: gitea_salvage_closure_metric_template(label)
|
|
for label in GITEA_SALVAGE_CLOSURE_METRICS
|
|
},
|
|
"issue_states": {
|
|
"ordinary_closed": 0,
|
|
"ordinary_open": 0,
|
|
"pull_wrapper_closed": 0,
|
|
"pull_wrapper_open": 0,
|
|
},
|
|
"old_repo_id": repo_id,
|
|
"pull_states": {"merged": 0, "unmerged": 0},
|
|
}
|
|
for repo_id in kept_repo_ids
|
|
]
|
|
by_repo_id = {row["old_repo_id"]: row for row in per_repository}
|
|
actor_relations = []
|
|
query_contract = []
|
|
|
|
for label, table, target_disposition in GITEA_SALVAGE_CLOSURE_ACTOR_RELATIONS:
|
|
require_columns(table, ("id", "repo_id", "user_id", "mode"))
|
|
rows = fetch_rows(
|
|
label,
|
|
f'SELECT id,repo_id,user_id,mode,typeof(id),typeof(repo_id),'
|
|
f'typeof(user_id),typeof(mode) FROM "{table}" '
|
|
f'WHERE repo_id IN ({placeholders}) ORDER BY repo_id,user_id,id',
|
|
kept_repo_ids,
|
|
)
|
|
seen_pairs = set()
|
|
for row in rows:
|
|
relation_id, repo_id, user_id, mode = row[0:4]
|
|
if (
|
|
tuple(row[4:8]) != ("integer", "integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (relation_id, repo_id, user_id, mode)
|
|
)
|
|
or relation_id <= 0
|
|
or repo_id not in kept_repo_set
|
|
or user_id <= 0
|
|
or not 0 <= mode <= 5
|
|
or (repo_id, user_id) in seen_pairs
|
|
):
|
|
die(f"Gitea salvage closure {label} row is invalid")
|
|
seen_pairs.add((repo_id, user_id))
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
user_id,
|
|
user_classes,
|
|
label,
|
|
)
|
|
actor_relations.append(
|
|
{
|
|
"actor_class": actor_class,
|
|
"disposition": (
|
|
target_disposition
|
|
if label == "access_cache" or actor_class == "kept"
|
|
else "DROP_DELETED_ACTOR"
|
|
),
|
|
"legacy_mode": mode,
|
|
"old_relation_id": relation_id,
|
|
"old_repo_id": repo_id,
|
|
"old_user_id": user_id,
|
|
"relation": label,
|
|
}
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "direct-repository-and-full-user-decision-partition",
|
|
"label": label,
|
|
"output": "ids-mode-actor-class-disposition-no-user-payload",
|
|
"table": table,
|
|
}
|
|
)
|
|
|
|
require_columns(
|
|
"issue",
|
|
(
|
|
"id",
|
|
"repo_id",
|
|
"poster_id",
|
|
"name",
|
|
"content",
|
|
"is_pull",
|
|
"is_closed",
|
|
"milestone_id",
|
|
),
|
|
)
|
|
issue_rows = fetch_rows(
|
|
"issues",
|
|
f"SELECT id,repo_id,poster_id,is_pull,is_closed,milestone_id,"
|
|
"typeof(id),typeof(repo_id),typeof(poster_id),typeof(is_pull),"
|
|
"typeof(is_closed),typeof(milestone_id),typeof(name),"
|
|
"CASE WHEN name IS NULL THEN 0 ELSE length(CAST(name AS BLOB)) END,"
|
|
"typeof(content),CASE WHEN content IS NULL THEN 0 "
|
|
"ELSE length(CAST(content AS BLOB)) END FROM issue "
|
|
f"WHERE repo_id IN ({placeholders}) ORDER BY repo_id,id",
|
|
kept_repo_ids,
|
|
)
|
|
issue_to_repo = {}
|
|
issue_is_pull = {}
|
|
issue_milestones = []
|
|
for row in issue_rows:
|
|
(
|
|
issue_id,
|
|
repo_id,
|
|
poster_id,
|
|
is_pull,
|
|
is_closed,
|
|
milestone_id,
|
|
id_type,
|
|
repo_type,
|
|
poster_type,
|
|
pull_type,
|
|
closed_type,
|
|
milestone_type,
|
|
name_type,
|
|
name_bytes,
|
|
content_type,
|
|
content_bytes,
|
|
) = row
|
|
if (
|
|
(id_type, repo_type, poster_type, pull_type, closed_type)
|
|
!= ("integer", "integer", "integer", "integer", "integer")
|
|
or milestone_type not in ("integer", "null")
|
|
or name_type not in ("null", "text")
|
|
or content_type not in ("null", "text")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (
|
|
issue_id,
|
|
repo_id,
|
|
poster_id,
|
|
is_pull,
|
|
is_closed,
|
|
name_bytes,
|
|
content_bytes,
|
|
)
|
|
)
|
|
or issue_id <= 0
|
|
or issue_id in issue_to_repo
|
|
or repo_id not in kept_repo_set
|
|
or is_pull not in (0, 1)
|
|
or is_closed not in (0, 1)
|
|
or milestone_id is not None
|
|
and (
|
|
not isinstance(milestone_id, int)
|
|
or isinstance(milestone_id, bool)
|
|
or milestone_id < 0
|
|
)
|
|
):
|
|
die("Gitea salvage closure issue row is invalid")
|
|
issue_to_repo[issue_id] = repo_id
|
|
issue_is_pull[issue_id] = bool(is_pull)
|
|
if milestone_id:
|
|
issue_milestones.append((repo_id, issue_id, milestone_id))
|
|
label = "pull_request_wrappers" if is_pull else "issues_ordinary"
|
|
state_label = (
|
|
"pull_wrapper_closed"
|
|
if is_pull and is_closed
|
|
else "pull_wrapper_open"
|
|
if is_pull
|
|
else "ordinary_closed"
|
|
if is_closed
|
|
else "ordinary_open"
|
|
)
|
|
by_repo_id[repo_id]["issue_states"][state_label] += 1
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
poster_id,
|
|
user_classes,
|
|
"issue poster",
|
|
)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
label,
|
|
actor_class,
|
|
text_bytes={"content": content_bytes, "name": name_bytes},
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "issue.repo_id-in-exact-kept-set",
|
|
"label": "issues-and-pull-wrappers",
|
|
"output": "counts-actor-classes-name-content-byte-lengths",
|
|
"table": "issue",
|
|
}
|
|
)
|
|
|
|
require_columns(
|
|
"pull_request",
|
|
("id", "issue_id", "base_repo_id", "head_repo_id", "has_merged"),
|
|
)
|
|
pull_rows = fetch_rows(
|
|
"pull requests",
|
|
f"SELECT p.id,p.issue_id,p.base_repo_id,p.head_repo_id,p.has_merged,"
|
|
"typeof(p.id),typeof(p.issue_id),typeof(p.base_repo_id),"
|
|
"typeof(p.head_repo_id),typeof(p.has_merged) FROM pull_request AS p "
|
|
"INNER JOIN issue AS i ON i.id=p.issue_id "
|
|
f"WHERE i.repo_id IN ({placeholders}) ORDER BY i.repo_id,p.id",
|
|
kept_repo_ids,
|
|
)
|
|
pull_to_repo = {}
|
|
pull_issue_ids = set()
|
|
pull_head_partitions = {"deleted": 0, "kept": 0}
|
|
for row in pull_rows:
|
|
pull_id, issue_id, base_repo_id, head_repo_id, has_merged = row[0:5]
|
|
if (
|
|
tuple(row[5:10])
|
|
!= ("integer", "integer", "integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (
|
|
pull_id,
|
|
issue_id,
|
|
base_repo_id,
|
|
head_repo_id,
|
|
has_merged,
|
|
)
|
|
)
|
|
or pull_id <= 0
|
|
or pull_id in pull_to_repo
|
|
or issue_id not in issue_to_repo
|
|
or not issue_is_pull[issue_id]
|
|
or issue_id in pull_issue_ids
|
|
or base_repo_id != issue_to_repo[issue_id]
|
|
or head_repo_id not in all_repo_ids
|
|
or has_merged not in (0, 1)
|
|
):
|
|
die("Gitea salvage closure pull-request row is invalid")
|
|
pull_to_repo[pull_id] = base_repo_id
|
|
pull_issue_ids.add(issue_id)
|
|
by_repo_id[base_repo_id]["pull_states"][
|
|
"merged" if has_merged else "unmerged"
|
|
] += 1
|
|
pull_head_partitions[
|
|
"kept" if head_repo_id in kept_repo_set else "deleted"
|
|
] += 1
|
|
if pull_issue_ids != {
|
|
issue_id for issue_id, is_pull in issue_is_pull.items() if is_pull
|
|
}:
|
|
die("Gitea salvage closure pull-wrapper relation is incomplete")
|
|
query_contract.append(
|
|
{
|
|
"join": "pull_request.issue_id-to-kept-issue-base-repo-exact",
|
|
"label": "pull-request-identity",
|
|
"output": "counts-and-head-repository-partition-no-branch-text",
|
|
"table": "pull_request",
|
|
}
|
|
)
|
|
|
|
comment_to_repo = {}
|
|
comment_to_issue = {}
|
|
content_history_to_issue = {}
|
|
review_to_issue = {}
|
|
tracked_time_to_issue = {}
|
|
issue_label_pairs = []
|
|
for label, table, actor_column, text_columns in (
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_DEPENDENTS
|
|
):
|
|
required = ["id", "issue_id", *text_columns]
|
|
if actor_column is not None:
|
|
required.append(actor_column)
|
|
if table == "issue_label":
|
|
required.append("label_id")
|
|
require_columns(table, required)
|
|
select_parts = [
|
|
"d.id AS relation_id",
|
|
"d.issue_id AS issue_id",
|
|
"i.repo_id AS repo_id",
|
|
"typeof(d.id) AS relation_id_type",
|
|
"typeof(d.issue_id) AS issue_id_type",
|
|
"typeof(i.repo_id) AS repo_id_type",
|
|
]
|
|
if actor_column is not None:
|
|
select_parts.extend(
|
|
(
|
|
f'd."{actor_column}" AS actor_id',
|
|
f'typeof(d."{actor_column}") AS actor_id_type',
|
|
)
|
|
)
|
|
if table == "issue_label":
|
|
select_parts.extend(
|
|
(
|
|
"d.label_id AS label_id",
|
|
"typeof(d.label_id) AS label_id_type",
|
|
)
|
|
)
|
|
for column in text_columns:
|
|
select_parts.extend(
|
|
(
|
|
f'typeof(d."{column}") AS "{column}_type"',
|
|
f'CASE WHEN d."{column}" IS NULL THEN 0 '
|
|
f'ELSE length(CAST(d."{column}" AS BLOB)) '
|
|
f'END AS "{column}_bytes"',
|
|
)
|
|
)
|
|
rows = fetch_rows(
|
|
label,
|
|
f'SELECT {",".join(select_parts)} FROM "{table}" AS d '
|
|
"INNER JOIN issue AS i ON i.id=d.issue_id "
|
|
f"WHERE i.repo_id IN ({placeholders}) ORDER BY i.repo_id,d.id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_ids = set()
|
|
for row in rows:
|
|
relation_id = row["relation_id"]
|
|
issue_id = row["issue_id"]
|
|
repo_id = row["repo_id"]
|
|
if (
|
|
(row["relation_id_type"], row["issue_id_type"], row["repo_id_type"])
|
|
!= ("integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (relation_id, issue_id, repo_id)
|
|
)
|
|
or relation_id <= 0
|
|
or relation_id in seen_ids
|
|
or issue_to_repo.get(issue_id) != repo_id
|
|
):
|
|
die(f"Gitea salvage closure {label} row is invalid")
|
|
seen_ids.add(relation_id)
|
|
actor_class = None
|
|
if actor_column is not None:
|
|
if row["actor_id_type"] != "integer":
|
|
die(f"Gitea salvage closure {label} actor is invalid")
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
row["actor_id"],
|
|
user_classes,
|
|
label,
|
|
)
|
|
byte_values = {}
|
|
for column in text_columns:
|
|
if row[f"{column}_type"] not in ("null", "text"):
|
|
die(f"Gitea salvage closure {label} text type is invalid")
|
|
byte_values[column] = row[f"{column}_bytes"]
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
label,
|
|
actor_class,
|
|
text_bytes=byte_values,
|
|
)
|
|
if table == "comment":
|
|
comment_to_repo[relation_id] = repo_id
|
|
comment_to_issue[relation_id] = issue_id
|
|
elif table == "issue_content_history":
|
|
content_history_to_issue[relation_id] = issue_id
|
|
elif table == "issue_label":
|
|
if (
|
|
row["label_id_type"] != "integer"
|
|
or not isinstance(row["label_id"], int)
|
|
or isinstance(row["label_id"], bool)
|
|
or row["label_id"] <= 0
|
|
):
|
|
die("Gitea salvage closure issue-label row is invalid")
|
|
issue_label_pairs.append(
|
|
(repo_id, issue_id, row["label_id"])
|
|
)
|
|
elif table == "review":
|
|
review_to_issue[relation_id] = issue_id
|
|
elif table == "tracked_time":
|
|
tracked_time_to_issue[relation_id] = issue_id
|
|
query_contract.append(
|
|
{
|
|
"join": f"{table}.issue_id-to-kept-issue",
|
|
"label": label,
|
|
"output": (
|
|
"counts-actor-classes-and-text-byte-lengths-no-payload"
|
|
),
|
|
"table": table,
|
|
}
|
|
)
|
|
|
|
require_columns(
|
|
"reaction",
|
|
("id", "issue_id", "comment_id", "user_id"),
|
|
)
|
|
reaction_rows = fetch_rows(
|
|
"reactions",
|
|
"SELECT r.id,r.issue_id,r.comment_id,r.user_id,"
|
|
"typeof(r.id),typeof(r.issue_id),typeof(r.comment_id),typeof(r.user_id),"
|
|
"direct_issue.repo_id,comment_issue.repo_id "
|
|
"FROM reaction AS r "
|
|
"LEFT JOIN issue AS direct_issue ON direct_issue.id=r.issue_id "
|
|
"LEFT JOIN comment AS c ON c.id=r.comment_id "
|
|
"LEFT JOIN issue AS comment_issue ON comment_issue.id=c.issue_id "
|
|
f"WHERE direct_issue.repo_id IN ({placeholders}) "
|
|
f"OR comment_issue.repo_id IN ({placeholders}) ORDER BY r.id",
|
|
(*kept_repo_ids, *kept_repo_ids),
|
|
)
|
|
seen_reactions = set()
|
|
for row in reaction_rows:
|
|
reaction_id, issue_id, comment_id, user_id = row[0:4]
|
|
direct_repo, comment_repo = row[8:10]
|
|
if (
|
|
tuple(row[4:8]) != ("integer", "integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (reaction_id, issue_id, comment_id, user_id)
|
|
)
|
|
or reaction_id <= 0
|
|
or reaction_id in seen_reactions
|
|
or issue_id < 0
|
|
or comment_id < 0
|
|
):
|
|
die("Gitea salvage closure reaction row is invalid")
|
|
candidate_repos = {
|
|
value
|
|
for value in (direct_repo, comment_repo)
|
|
if isinstance(value, int) and value in kept_repo_set
|
|
}
|
|
if len(candidate_repos) != 1:
|
|
die("Gitea salvage closure reaction association is ambiguous")
|
|
repo_id = candidate_repos.pop()
|
|
if issue_id and issue_to_repo.get(issue_id) != repo_id:
|
|
die("Gitea salvage closure reaction issue association is invalid")
|
|
if comment_id and comment_to_repo.get(comment_id) != repo_id:
|
|
die("Gitea salvage closure reaction comment association is invalid")
|
|
seen_reactions.add(reaction_id)
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
user_id,
|
|
user_classes,
|
|
"reaction",
|
|
)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"reactions",
|
|
actor_class,
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "reaction.issue-or-comment-to-kept-issue-exact-one-repo",
|
|
"label": "reactions",
|
|
"output": "counts-and-actor-classes-no-reaction-text",
|
|
"table": "reaction",
|
|
}
|
|
)
|
|
|
|
require_columns("review_state", ("id", "pull_id", "user_id", "updated_files"))
|
|
review_state_rows = fetch_rows(
|
|
"review states",
|
|
"SELECT s.id,s.pull_id,s.user_id,p.base_repo_id,"
|
|
"typeof(s.id),typeof(s.pull_id),typeof(s.user_id),typeof(p.base_repo_id),"
|
|
"typeof(s.updated_files),CASE WHEN s.updated_files IS NULL THEN 0 "
|
|
"ELSE length(CAST(s.updated_files AS BLOB)) END "
|
|
"FROM review_state AS s INNER JOIN pull_request AS p ON p.id=s.pull_id "
|
|
"INNER JOIN issue AS i ON i.id=p.issue_id "
|
|
f"WHERE i.repo_id IN ({placeholders}) ORDER BY i.repo_id,s.id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_review_states = set()
|
|
for row in review_state_rows:
|
|
state_id, pull_id, user_id, repo_id = row[0:4]
|
|
if (
|
|
tuple(row[4:8]) != ("integer", "integer", "integer", "integer")
|
|
or row[8] not in ("null", "text")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (state_id, pull_id, user_id, repo_id, row[9])
|
|
)
|
|
or state_id <= 0
|
|
or state_id in seen_review_states
|
|
or pull_to_repo.get(pull_id) != repo_id
|
|
):
|
|
die("Gitea salvage closure review-state row is invalid")
|
|
seen_review_states.add(state_id)
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
user_id,
|
|
user_classes,
|
|
"review state",
|
|
)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"review_states",
|
|
actor_class,
|
|
text_bytes={"updated_files": row[9]},
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "review_state.pull_id-to-kept-pull-wrapper",
|
|
"label": "review-states",
|
|
"output": "counts-actor-classes-updated-files-byte-length-no-payload",
|
|
"table": "review_state",
|
|
}
|
|
)
|
|
|
|
require_columns(
|
|
"issue_dependency",
|
|
("id", "user_id", "issue_id", "dependency_id"),
|
|
)
|
|
dependency_rows = fetch_rows(
|
|
"issue dependencies",
|
|
"SELECT d.id,d.user_id,d.issue_id,d.dependency_id,source.repo_id,"
|
|
"target.repo_id,typeof(d.id),typeof(d.user_id),typeof(d.issue_id),"
|
|
"typeof(d.dependency_id) FROM issue_dependency AS d "
|
|
"INNER JOIN issue AS source ON source.id=d.issue_id "
|
|
"LEFT JOIN issue AS target ON target.id=d.dependency_id "
|
|
f"WHERE source.repo_id IN ({placeholders}) ORDER BY source.repo_id,d.id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_dependencies = set()
|
|
cross_repo_dependencies = 0
|
|
for row in dependency_rows:
|
|
relation_id, user_id, issue_id, dependency_id, repo_id, target_repo_id = row[0:6]
|
|
if (
|
|
tuple(row[6:10]) != ("integer", "integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in row[0:6]
|
|
)
|
|
or relation_id <= 0
|
|
or relation_id in seen_dependencies
|
|
or issue_to_repo.get(issue_id) != repo_id
|
|
or target_repo_id not in all_repo_ids
|
|
):
|
|
die("Gitea salvage closure issue-dependency row is invalid")
|
|
seen_dependencies.add(relation_id)
|
|
cross_repo_dependencies += int(repo_id != target_repo_id)
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
user_id,
|
|
user_classes,
|
|
"issue dependency",
|
|
)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"issue_dependencies",
|
|
actor_class,
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "issue_dependency.issue_id-to-kept-source-issue",
|
|
"label": "issue-dependencies",
|
|
"output": "counts-actor-classes-cross-repo-count-no-text",
|
|
"table": "issue_dependency",
|
|
}
|
|
)
|
|
|
|
direct_text_contracts = (
|
|
("labels", "label", None, ("name", "description", "color")),
|
|
("milestones", "milestone", None, ("name", "content")),
|
|
(
|
|
"projects",
|
|
"project",
|
|
"creator_id",
|
|
("title", "description"),
|
|
),
|
|
(
|
|
"releases",
|
|
"release",
|
|
"publisher_id",
|
|
("tag_name", "target", "title", "note"),
|
|
),
|
|
)
|
|
label_to_repo = {}
|
|
milestone_to_repo = {}
|
|
project_to_repo = {}
|
|
release_to_repo = {}
|
|
for label, table, actor_column, text_columns in direct_text_contracts:
|
|
required = ["id", "repo_id", *text_columns]
|
|
if actor_column is not None:
|
|
required.append(actor_column)
|
|
require_columns(table, required)
|
|
select_parts = [
|
|
"id",
|
|
"repo_id",
|
|
"typeof(id) AS id_type",
|
|
"typeof(repo_id) AS repo_id_type",
|
|
]
|
|
if actor_column is not None:
|
|
select_parts.extend(
|
|
(
|
|
f'"{actor_column}" AS actor_id',
|
|
f'typeof("{actor_column}") AS actor_id_type',
|
|
)
|
|
)
|
|
for column in text_columns:
|
|
select_parts.extend(
|
|
(
|
|
f'typeof("{column}") AS "{column}_type"',
|
|
f'CASE WHEN "{column}" IS NULL THEN 0 '
|
|
f'ELSE length(CAST("{column}" AS BLOB)) '
|
|
f'END AS "{column}_bytes"',
|
|
)
|
|
)
|
|
rows = fetch_rows(
|
|
label,
|
|
f'SELECT {",".join(select_parts)} FROM "{table}" '
|
|
f"WHERE repo_id IN ({placeholders}) ORDER BY repo_id,id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_ids = set()
|
|
for row in rows:
|
|
relation_id = row["id"]
|
|
repo_id = row["repo_id"]
|
|
if (
|
|
(row["id_type"], row["repo_id_type"])
|
|
!= ("integer", "integer")
|
|
or not isinstance(relation_id, int)
|
|
or isinstance(relation_id, bool)
|
|
or relation_id <= 0
|
|
or relation_id in seen_ids
|
|
or repo_id not in kept_repo_set
|
|
):
|
|
die(f"Gitea salvage closure {label} row is invalid")
|
|
seen_ids.add(relation_id)
|
|
actor_class = None
|
|
if actor_column is not None:
|
|
if row["actor_id_type"] != "integer":
|
|
die(f"Gitea salvage closure {label} actor is invalid")
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
row["actor_id"],
|
|
user_classes,
|
|
label,
|
|
)
|
|
byte_values = {}
|
|
for column in text_columns:
|
|
if row[f"{column}_type"] not in ("null", "text"):
|
|
die(f"Gitea salvage closure {label} text type is invalid")
|
|
byte_values[column] = row[f"{column}_bytes"]
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
label,
|
|
actor_class,
|
|
text_bytes=byte_values,
|
|
)
|
|
if table == "label":
|
|
label_to_repo[relation_id] = repo_id
|
|
elif table == "milestone":
|
|
milestone_to_repo[relation_id] = repo_id
|
|
elif table == "project":
|
|
project_to_repo[relation_id] = repo_id
|
|
elif table == "release":
|
|
release_to_repo[relation_id] = repo_id
|
|
query_contract.append(
|
|
{
|
|
"join": f"{table}.repo_id-in-exact-kept-set",
|
|
"label": label,
|
|
"output": "counts-actor-classes-text-byte-lengths-no-payload",
|
|
"table": table,
|
|
}
|
|
)
|
|
|
|
for repo_id, _issue_id, label_id in issue_label_pairs:
|
|
if label_to_repo.get(label_id) != repo_id:
|
|
die("Gitea salvage closure issue-label repository relation is invalid")
|
|
for repo_id, _issue_id, milestone_id in issue_milestones:
|
|
if milestone_to_repo.get(milestone_id) != repo_id:
|
|
die("Gitea salvage closure issue-milestone repository relation is invalid")
|
|
|
|
require_columns(
|
|
"project_board",
|
|
("id", "project_id", "title", "color"),
|
|
)
|
|
board_rows = fetch_rows(
|
|
"project boards",
|
|
"SELECT b.id,b.project_id,p.repo_id,typeof(b.id),typeof(b.project_id),"
|
|
"typeof(p.repo_id),typeof(b.title),CASE WHEN b.title IS NULL THEN 0 "
|
|
"ELSE length(CAST(b.title AS BLOB)) END,typeof(b.color),"
|
|
"CASE WHEN b.color IS NULL THEN 0 ELSE length(CAST(b.color AS BLOB)) END "
|
|
"FROM project_board AS b INNER JOIN project AS p ON p.id=b.project_id "
|
|
f"WHERE p.repo_id IN ({placeholders}) ORDER BY p.repo_id,b.id",
|
|
kept_repo_ids,
|
|
)
|
|
board_to_repo = {}
|
|
for row in board_rows:
|
|
board_id, project_id, repo_id = row[0:3]
|
|
if (
|
|
tuple(row[3:6]) != ("integer", "integer", "integer")
|
|
or row[6] not in ("null", "text")
|
|
or row[8] not in ("null", "text")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (board_id, project_id, repo_id, row[7], row[9])
|
|
)
|
|
or board_id <= 0
|
|
or board_id in board_to_repo
|
|
or project_to_repo.get(project_id) != repo_id
|
|
):
|
|
die("Gitea salvage closure project-board row is invalid")
|
|
board_to_repo[board_id] = repo_id
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"project_boards",
|
|
text_bytes={"color": row[9], "title": row[7]},
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "project_board.project_id-to-kept-project",
|
|
"label": "project-boards",
|
|
"output": "counts-title-color-byte-lengths-no-payload",
|
|
"table": "project_board",
|
|
}
|
|
)
|
|
|
|
require_columns(
|
|
"project_issue",
|
|
("id", "issue_id", "project_id", "project_board_id"),
|
|
)
|
|
project_issue_rows = fetch_rows(
|
|
"project issue links",
|
|
"SELECT pi.id,pi.issue_id,pi.project_id,pi.project_board_id,"
|
|
"i.repo_id,p.repo_id,typeof(pi.id),typeof(pi.issue_id),"
|
|
"typeof(pi.project_id),typeof(pi.project_board_id) "
|
|
"FROM project_issue AS pi LEFT JOIN issue AS i ON i.id=pi.issue_id "
|
|
"LEFT JOIN project AS p ON p.id=pi.project_id "
|
|
f"WHERE i.repo_id IN ({placeholders}) OR p.repo_id IN ({placeholders}) "
|
|
"ORDER BY pi.id",
|
|
(*kept_repo_ids, *kept_repo_ids),
|
|
)
|
|
seen_project_issue = set()
|
|
for row in project_issue_rows:
|
|
link_id, issue_id, project_id, board_id, issue_repo, project_repo = row[0:6]
|
|
if (
|
|
tuple(row[6:10]) != ("integer", "integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in row[0:6]
|
|
)
|
|
or link_id <= 0
|
|
or link_id in seen_project_issue
|
|
or issue_repo != project_repo
|
|
or issue_to_repo.get(issue_id) != issue_repo
|
|
or project_to_repo.get(project_id) != project_repo
|
|
or board_id > 0 and board_to_repo.get(board_id) != project_repo
|
|
or board_id < 0
|
|
):
|
|
die("Gitea salvage closure project-issue relation is invalid")
|
|
seen_project_issue.add(link_id)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[project_repo],
|
|
"project_issue_links",
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "project_issue-to-same-kept-repository-issue-project-board",
|
|
"label": "project-issue-links",
|
|
"output": "counts-only-no-project-or-issue-text",
|
|
"table": "project_issue",
|
|
}
|
|
)
|
|
|
|
# Close the concrete FK-like comment/history/pull subrelations present in
|
|
# the pinned snapshot schema. Optional legacy INTEGER fields may be SQL
|
|
# NULL or zero; both are treated as absent, but NULL representation counts
|
|
# remain explicit. No comment type, body, path, ref text or commit value is
|
|
# selected. Team IDs are existence-checked and sealed as HOLD because the
|
|
# incident decision has no organization/team allowlist.
|
|
subrelation_per_repo = {
|
|
repo_id: {
|
|
"actor_classes": {
|
|
label: {
|
|
"deleted": 0,
|
|
"kept": 0,
|
|
"system-or-external": 0,
|
|
}
|
|
for label in GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_ACTORS
|
|
},
|
|
"counts": {
|
|
label: 0
|
|
for label in GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_COUNTS
|
|
},
|
|
"external_author_provenance": {
|
|
source: {
|
|
"id_without_name": 0,
|
|
"name_bytes": 0,
|
|
"name_without_id": 0,
|
|
"rows_with_id": 0,
|
|
"rows_with_name": 0,
|
|
}
|
|
for source in GITEA_SALVAGE_CLOSURE_EXTERNAL_AUTHOR_SOURCES
|
|
},
|
|
"null_encodings": {
|
|
column: 0
|
|
for column in (
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_NULLABLE_COLUMNS
|
|
)
|
|
},
|
|
"old_repo_id": repo_id,
|
|
"target_repository_classes": {
|
|
label: {"deleted": 0, "global": 0, "kept": 0}
|
|
for label in (
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_REPOSITORIES
|
|
)
|
|
},
|
|
}
|
|
for repo_id in kept_repo_ids
|
|
}
|
|
subrelation_holds = []
|
|
|
|
def add_external_author_provenance(
|
|
repo_id,
|
|
source,
|
|
external_id,
|
|
external_id_type,
|
|
external_name_type,
|
|
external_name_bytes,
|
|
):
|
|
if source not in GITEA_SALVAGE_CLOSURE_EXTERNAL_AUTHOR_SOURCES:
|
|
die("Gitea salvage external-author source is invalid")
|
|
if external_id_type == "null" and external_id is None:
|
|
subrelation_per_repo[repo_id]["null_encodings"][
|
|
f"{source}.original_author_id"
|
|
] += 1
|
|
external_id = 0
|
|
if (
|
|
external_id_type not in ("integer", "null")
|
|
or not isinstance(external_id, int)
|
|
or isinstance(external_id, bool)
|
|
or external_id < 0
|
|
or external_name_type not in ("null", "text")
|
|
or not isinstance(external_name_bytes, int)
|
|
or isinstance(external_name_bytes, bool)
|
|
or external_name_bytes < 0
|
|
or external_name_bytes
|
|
> GITEA_SALVAGE_CLOSURE_MAX_TEXT_BYTES_PER_FIELD
|
|
):
|
|
die("Gitea salvage external-author provenance is invalid")
|
|
record = subrelation_per_repo[repo_id]["external_author_provenance"][
|
|
source
|
|
]
|
|
has_id = external_id > 0
|
|
has_name = external_name_bytes > 0
|
|
record["rows_with_id"] += int(has_id)
|
|
record["rows_with_name"] += int(has_name)
|
|
record["id_without_name"] += int(has_id and not has_name)
|
|
record["name_without_id"] += int(has_name and not has_id)
|
|
record["name_bytes"] += external_name_bytes
|
|
if record["name_bytes"] > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES:
|
|
die("Gitea salvage external-author byte total exceeds the limit")
|
|
|
|
def add_subrelation_count(repo_id, label):
|
|
if repo_id not in subrelation_per_repo or label not in (
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_COUNTS
|
|
):
|
|
die("Gitea salvage issue subrelation count is invalid")
|
|
subrelation_per_repo[repo_id]["counts"][label] += 1
|
|
|
|
def add_subrelation_actor(repo_id, label, actor_id):
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
actor_id,
|
|
user_classes,
|
|
label,
|
|
)
|
|
subrelation_per_repo[repo_id]["actor_classes"][label][actor_class] += 1
|
|
|
|
def add_subrelation_target_repo(repo_id, label, target_repo_id):
|
|
if target_repo_id == 0:
|
|
target_class = "global"
|
|
elif target_repo_id in kept_repo_set:
|
|
target_class = "kept"
|
|
elif target_repo_id in all_repo_ids:
|
|
target_class = "deleted"
|
|
else:
|
|
die("Gitea salvage issue subrelation target repository is invalid")
|
|
subrelation_per_repo[repo_id]["target_repository_classes"][label][
|
|
target_class
|
|
] += 1
|
|
|
|
comment_optional_columns = (
|
|
"assignee_id",
|
|
"assignee_team_id",
|
|
"dependent_issue_id",
|
|
"label_id",
|
|
"milestone_id",
|
|
"old_milestone_id",
|
|
"old_project_id",
|
|
"original_author_id",
|
|
"project_id",
|
|
"ref_action",
|
|
"ref_comment_id",
|
|
"ref_is_pull",
|
|
"ref_issue_id",
|
|
"ref_repo_id",
|
|
"resolve_doer_id",
|
|
"review_id",
|
|
"time_id",
|
|
)
|
|
require_columns(
|
|
"comment",
|
|
("id", "issue_id", "original_author", *comment_optional_columns),
|
|
)
|
|
require_extra_columns("team", ("id", "org_id"))
|
|
comment_select = [
|
|
"c.id AS comment_id",
|
|
"c.issue_id AS source_issue_id",
|
|
"source.repo_id AS source_repo_id",
|
|
"typeof(c.id) AS comment_id_type",
|
|
"typeof(c.issue_id) AS source_issue_id_type",
|
|
"typeof(source.repo_id) AS source_repo_id_type",
|
|
]
|
|
for column in comment_optional_columns:
|
|
comment_select.extend(
|
|
(
|
|
f'c."{column}" AS "{column}"',
|
|
f'typeof(c."{column}") AS "{column}_type"',
|
|
)
|
|
)
|
|
comment_select.extend(
|
|
(
|
|
"label_target.id AS label_target_id",
|
|
"label_target.repo_id AS label_target_repo_id",
|
|
"old_project.id AS old_project_target_id",
|
|
"old_project.repo_id AS old_project_target_repo_id",
|
|
"current_project.id AS project_target_id",
|
|
"current_project.repo_id AS project_target_repo_id",
|
|
"old_milestone.id AS old_milestone_target_id",
|
|
"old_milestone.repo_id AS old_milestone_target_repo_id",
|
|
"current_milestone.id AS milestone_target_id",
|
|
"current_milestone.repo_id AS milestone_target_repo_id",
|
|
"tracked.id AS tracked_target_id",
|
|
"tracked.issue_id AS tracked_target_issue_id",
|
|
"team_target.id AS team_target_id",
|
|
"dependent.id AS dependent_target_id",
|
|
"dependent.repo_id AS dependent_target_repo_id",
|
|
"review_target.id AS review_target_id",
|
|
"review_target.issue_id AS review_target_issue_id",
|
|
"ref_issue.id AS ref_issue_target_id",
|
|
"ref_issue.repo_id AS ref_issue_target_repo_id",
|
|
"ref_issue.is_pull AS ref_issue_target_is_pull",
|
|
"ref_comment.id AS ref_comment_target_id",
|
|
"ref_comment.issue_id AS ref_comment_target_issue_id",
|
|
"team_target.org_id AS team_target_org_id",
|
|
"typeof(c.original_author) AS original_author_type",
|
|
"CASE WHEN c.original_author IS NULL THEN 0 "
|
|
"ELSE length(CAST(c.original_author AS BLOB)) END "
|
|
"AS original_author_bytes",
|
|
)
|
|
)
|
|
comment_relation_rows = fetch_rows(
|
|
"comment polymorphic subrelations",
|
|
f"SELECT {','.join(comment_select)} FROM comment AS c "
|
|
"INNER JOIN issue AS source ON source.id=c.issue_id "
|
|
"LEFT JOIN label AS label_target ON label_target.id=c.label_id "
|
|
"LEFT JOIN project AS old_project ON old_project.id=c.old_project_id "
|
|
"LEFT JOIN project AS current_project ON current_project.id=c.project_id "
|
|
"LEFT JOIN milestone AS old_milestone "
|
|
"ON old_milestone.id=c.old_milestone_id "
|
|
"LEFT JOIN milestone AS current_milestone "
|
|
"ON current_milestone.id=c.milestone_id "
|
|
"LEFT JOIN tracked_time AS tracked ON tracked.id=c.time_id "
|
|
"LEFT JOIN team AS team_target ON team_target.id=c.assignee_team_id "
|
|
"LEFT JOIN issue AS dependent ON dependent.id=c.dependent_issue_id "
|
|
"LEFT JOIN review AS review_target ON review_target.id=c.review_id "
|
|
"LEFT JOIN issue AS ref_issue ON ref_issue.id=c.ref_issue_id "
|
|
"LEFT JOIN comment AS ref_comment ON ref_comment.id=c.ref_comment_id "
|
|
f"WHERE source.repo_id IN ({placeholders}) ORDER BY source.repo_id,c.id",
|
|
kept_repo_ids,
|
|
)
|
|
|
|
def optional_comment_integer(row, column, repo_id):
|
|
value_type = row[f"{column}_type"]
|
|
value = row[column]
|
|
if value_type == "null" and value is None:
|
|
if column == "assignee_team_id":
|
|
die("Gitea salvage comment assignee-team identity is NULL")
|
|
subrelation_per_repo[repo_id]["null_encodings"][
|
|
f"comment.{column}"
|
|
] += 1
|
|
return 0
|
|
if (
|
|
value_type != "integer"
|
|
or not isinstance(value, int)
|
|
or isinstance(value, bool)
|
|
or value < 0
|
|
):
|
|
die("Gitea salvage comment subrelation value is invalid")
|
|
return value
|
|
|
|
def validate_scoped_comment_target(
|
|
repo_id,
|
|
label,
|
|
target_id,
|
|
joined_id,
|
|
joined_repo_id,
|
|
allow_global=False,
|
|
):
|
|
if target_id == 0:
|
|
return
|
|
if (
|
|
joined_id != target_id
|
|
or not isinstance(joined_repo_id, int)
|
|
or isinstance(joined_repo_id, bool)
|
|
or (
|
|
joined_repo_id != repo_id
|
|
and not (allow_global and joined_repo_id == 0)
|
|
)
|
|
):
|
|
die(f"Gitea salvage {label} relation is invalid")
|
|
add_subrelation_count(repo_id, label)
|
|
add_subrelation_target_repo(repo_id, label, joined_repo_id)
|
|
|
|
seen_comment_relations = set()
|
|
for row in comment_relation_rows:
|
|
comment_id = row["comment_id"]
|
|
issue_id = row["source_issue_id"]
|
|
repo_id = row["source_repo_id"]
|
|
if (
|
|
(
|
|
row["comment_id_type"],
|
|
row["source_issue_id_type"],
|
|
row["source_repo_id_type"],
|
|
)
|
|
!= ("integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (comment_id, issue_id, repo_id)
|
|
)
|
|
or comment_id <= 0
|
|
or comment_id in seen_comment_relations
|
|
or comment_to_issue.get(comment_id) != issue_id
|
|
or issue_to_repo.get(issue_id) != repo_id
|
|
):
|
|
die("Gitea salvage comment subrelation source is invalid")
|
|
seen_comment_relations.add(comment_id)
|
|
values = {
|
|
column: optional_comment_integer(row, column, repo_id)
|
|
for column in comment_optional_columns
|
|
}
|
|
if values["assignee_id"] and values["assignee_team_id"]:
|
|
die("Gitea salvage comment assignee identities conflict")
|
|
add_external_author_provenance(
|
|
repo_id,
|
|
"comment",
|
|
values["original_author_id"],
|
|
row["original_author_id_type"],
|
|
row["original_author_type"],
|
|
row["original_author_bytes"],
|
|
)
|
|
|
|
validate_scoped_comment_target(
|
|
repo_id,
|
|
"comment_label",
|
|
values["label_id"],
|
|
row["label_target_id"],
|
|
row["label_target_repo_id"],
|
|
allow_global=True,
|
|
)
|
|
for label, column, id_key, repo_key, allow_global in (
|
|
(
|
|
"comment_old_project",
|
|
"old_project_id",
|
|
"old_project_target_id",
|
|
"old_project_target_repo_id",
|
|
True,
|
|
),
|
|
(
|
|
"comment_current_project",
|
|
"project_id",
|
|
"project_target_id",
|
|
"project_target_repo_id",
|
|
True,
|
|
),
|
|
(
|
|
"comment_old_milestone",
|
|
"old_milestone_id",
|
|
"old_milestone_target_id",
|
|
"old_milestone_target_repo_id",
|
|
False,
|
|
),
|
|
(
|
|
"comment_current_milestone",
|
|
"milestone_id",
|
|
"milestone_target_id",
|
|
"milestone_target_repo_id",
|
|
False,
|
|
),
|
|
):
|
|
validate_scoped_comment_target(
|
|
repo_id,
|
|
label,
|
|
values[column],
|
|
row[id_key],
|
|
row[repo_key],
|
|
allow_global=allow_global,
|
|
)
|
|
|
|
time_id = values["time_id"]
|
|
if time_id:
|
|
if (
|
|
row["tracked_target_id"] != time_id
|
|
or row["tracked_target_issue_id"] != issue_id
|
|
or tracked_time_to_issue.get(time_id) != issue_id
|
|
):
|
|
die("Gitea salvage comment tracked-time relation is invalid")
|
|
add_subrelation_count(repo_id, "comment_tracked_time")
|
|
|
|
for label, column in (
|
|
("comment_assignee", "assignee_id"),
|
|
("comment_resolve_doer", "resolve_doer_id"),
|
|
):
|
|
actor_id = values[column]
|
|
if actor_id:
|
|
add_subrelation_count(repo_id, label)
|
|
add_subrelation_actor(repo_id, label, actor_id)
|
|
|
|
team_id = values["assignee_team_id"]
|
|
if team_id:
|
|
team_org_id = row["team_target_org_id"]
|
|
if (
|
|
row["team_target_id"] != team_id
|
|
or not isinstance(team_org_id, int)
|
|
or isinstance(team_org_id, bool)
|
|
or team_org_id <= 0
|
|
):
|
|
die("Gitea salvage comment assignee-team relation is orphaned")
|
|
add_subrelation_count(repo_id, "comment_assignee_team")
|
|
subrelation_holds.append(
|
|
{
|
|
"kind": "comment-assignee-team-mapping",
|
|
"old_org_identity_class": (
|
|
gitea_salvage_closure_actor_class(
|
|
team_org_id,
|
|
user_classes,
|
|
"comment assignee-team organization",
|
|
)
|
|
),
|
|
"old_org_id": team_org_id,
|
|
"old_repo_id": repo_id,
|
|
"old_row_id": comment_id,
|
|
"old_team_id": team_id,
|
|
"source_table": "comment",
|
|
}
|
|
)
|
|
|
|
dependent_id = values["dependent_issue_id"]
|
|
if dependent_id:
|
|
target_repo_id = row["dependent_target_repo_id"]
|
|
if (
|
|
row["dependent_target_id"] != dependent_id
|
|
or not isinstance(target_repo_id, int)
|
|
or isinstance(target_repo_id, bool)
|
|
or target_repo_id not in all_repo_ids
|
|
):
|
|
die("Gitea salvage comment dependent-issue relation is invalid")
|
|
add_subrelation_count(repo_id, "comment_dependent_issue")
|
|
add_subrelation_target_repo(
|
|
repo_id,
|
|
"comment_dependent_issue",
|
|
target_repo_id,
|
|
)
|
|
|
|
review_id = values["review_id"]
|
|
if review_id:
|
|
if (
|
|
row["review_target_id"] != review_id
|
|
or row["review_target_issue_id"] != issue_id
|
|
or review_to_issue.get(review_id) != issue_id
|
|
):
|
|
die("Gitea salvage comment review relation is invalid")
|
|
add_subrelation_count(repo_id, "comment_review")
|
|
|
|
ref_repo_id = values["ref_repo_id"]
|
|
ref_issue_id = values["ref_issue_id"]
|
|
ref_comment_id = values["ref_comment_id"]
|
|
ref_action = values["ref_action"]
|
|
ref_is_pull = values["ref_is_pull"]
|
|
if ref_action not in (0, 1, 2, 3) or ref_is_pull not in (0, 1):
|
|
die("Gitea salvage comment cross-reference state is invalid")
|
|
if ref_repo_id or ref_issue_id or ref_comment_id:
|
|
if (
|
|
ref_repo_id <= 0
|
|
or ref_issue_id <= 0
|
|
or ref_repo_id not in all_repo_ids
|
|
or row["ref_issue_target_id"] != ref_issue_id
|
|
or row["ref_issue_target_repo_id"] != ref_repo_id
|
|
or row["ref_issue_target_is_pull"] != ref_is_pull
|
|
):
|
|
die("Gitea salvage comment cross-reference relation is invalid")
|
|
if ref_comment_id and (
|
|
row["ref_comment_target_id"] != ref_comment_id
|
|
or row["ref_comment_target_issue_id"] != ref_issue_id
|
|
):
|
|
die("Gitea salvage comment cross-reference comment is invalid")
|
|
add_subrelation_count(repo_id, "comment_cross_reference")
|
|
add_subrelation_target_repo(
|
|
repo_id,
|
|
"comment_cross_reference",
|
|
ref_repo_id,
|
|
)
|
|
if ref_comment_id:
|
|
add_subrelation_count(
|
|
repo_id,
|
|
"comment_cross_reference_comment",
|
|
)
|
|
elif ref_action or ref_is_pull:
|
|
die("Gitea salvage empty comment cross-reference state is invalid")
|
|
|
|
if seen_comment_relations != set(comment_to_issue):
|
|
die("Gitea salvage comment subrelation coverage is incomplete")
|
|
|
|
require_columns(
|
|
"review",
|
|
(
|
|
"id",
|
|
"issue_id",
|
|
"reviewer_id",
|
|
"reviewer_team_id",
|
|
"original_author_id",
|
|
"original_author",
|
|
),
|
|
)
|
|
review_relation_rows = fetch_rows(
|
|
"review team and external-author relations",
|
|
"SELECT r.id,r.issue_id,i.repo_id,r.reviewer_id,r.reviewer_team_id,"
|
|
"r.original_author_id,typeof(r.id),typeof(r.issue_id),"
|
|
"typeof(i.repo_id),typeof(r.reviewer_team_id),"
|
|
"typeof(r.reviewer_id),typeof(r.original_author_id),"
|
|
"team_target.id,team_target.org_id,"
|
|
"typeof(r.original_author),CASE WHEN r.original_author IS NULL THEN 0 "
|
|
"ELSE length(CAST(r.original_author AS BLOB)) END "
|
|
"FROM review AS r INNER JOIN issue AS i ON i.id=r.issue_id "
|
|
"LEFT JOIN team AS team_target ON team_target.id=r.reviewer_team_id "
|
|
f"WHERE i.repo_id IN ({placeholders}) ORDER BY i.repo_id,r.id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_review_relations = set()
|
|
for row in review_relation_rows:
|
|
(
|
|
review_id,
|
|
issue_id,
|
|
repo_id,
|
|
reviewer_id,
|
|
reviewer_team_id,
|
|
external_author_id,
|
|
) = row[0:6]
|
|
if (
|
|
tuple(row[6:11])
|
|
!= ("integer", "integer", "integer", "integer", "integer")
|
|
or row[11] not in ("integer", "null")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (
|
|
review_id,
|
|
issue_id,
|
|
repo_id,
|
|
reviewer_id,
|
|
reviewer_team_id,
|
|
)
|
|
)
|
|
or review_id <= 0
|
|
or review_id in seen_review_relations
|
|
or reviewer_team_id < 0
|
|
or reviewer_id < 0
|
|
or reviewer_id and reviewer_team_id
|
|
or review_to_issue.get(review_id) != issue_id
|
|
or issue_to_repo.get(issue_id) != repo_id
|
|
):
|
|
die("Gitea salvage review subrelation source is invalid")
|
|
seen_review_relations.add(review_id)
|
|
add_external_author_provenance(
|
|
repo_id,
|
|
"review",
|
|
external_author_id,
|
|
row[11],
|
|
row[14],
|
|
row[15],
|
|
)
|
|
if reviewer_team_id:
|
|
team_id, team_org_id = row[12:14]
|
|
if (
|
|
team_id != reviewer_team_id
|
|
or not isinstance(team_org_id, int)
|
|
or isinstance(team_org_id, bool)
|
|
or team_org_id <= 0
|
|
):
|
|
die("Gitea salvage review reviewer-team relation is orphaned")
|
|
add_subrelation_count(repo_id, "review_reviewer_team")
|
|
subrelation_holds.append(
|
|
{
|
|
"kind": "review-reviewer-team-mapping",
|
|
"old_org_identity_class": (
|
|
gitea_salvage_closure_actor_class(
|
|
team_org_id,
|
|
user_classes,
|
|
"reviewer-team organization",
|
|
)
|
|
),
|
|
"old_org_id": team_org_id,
|
|
"old_repo_id": repo_id,
|
|
"old_row_id": review_id,
|
|
"old_team_id": reviewer_team_id,
|
|
"source_table": "review",
|
|
}
|
|
)
|
|
if seen_review_relations != set(review_to_issue):
|
|
die("Gitea salvage review subrelation coverage is incomplete")
|
|
|
|
require_columns(
|
|
"issue_content_history",
|
|
("id", "issue_id", "comment_id"),
|
|
)
|
|
history_relation_rows = fetch_rows(
|
|
"content-history comment relations",
|
|
"SELECT h.id,h.issue_id,h.comment_id,i.repo_id,typeof(h.id),"
|
|
"typeof(h.issue_id),typeof(h.comment_id),typeof(i.repo_id) "
|
|
"FROM issue_content_history AS h "
|
|
"INNER JOIN issue AS i ON i.id=h.issue_id "
|
|
f"WHERE i.repo_id IN ({placeholders}) ORDER BY i.repo_id,h.id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_history_relations = set()
|
|
for row in history_relation_rows:
|
|
history_id, issue_id, comment_id, repo_id = row[0:4]
|
|
if (
|
|
tuple(row[4:6]) != ("integer", "integer")
|
|
or row[6] not in ("integer", "null")
|
|
or row[7] != "integer"
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (history_id, issue_id, repo_id)
|
|
)
|
|
or history_id <= 0
|
|
or history_id in seen_history_relations
|
|
or issue_to_repo.get(issue_id) != repo_id
|
|
):
|
|
die("Gitea salvage content-history subrelation is invalid")
|
|
seen_history_relations.add(history_id)
|
|
if content_history_to_issue.get(history_id) != issue_id:
|
|
die("Gitea salvage content-history coverage is incomplete")
|
|
if comment_id is None:
|
|
subrelation_per_repo[repo_id]["null_encodings"][
|
|
"issue_content_history.comment_id"
|
|
] += 1
|
|
comment_id = 0
|
|
if (
|
|
not isinstance(comment_id, int)
|
|
or isinstance(comment_id, bool)
|
|
or comment_id < 0
|
|
):
|
|
die("Gitea salvage content-history comment identity is invalid")
|
|
if comment_id:
|
|
if comment_to_issue.get(comment_id) != issue_id:
|
|
die("Gitea salvage content-history comment relation is invalid")
|
|
add_subrelation_count(repo_id, "content_history_comment")
|
|
else:
|
|
add_subrelation_count(repo_id, "content_history_issue")
|
|
if seen_history_relations != set(content_history_to_issue):
|
|
die("Gitea salvage content-history subrelation coverage is incomplete")
|
|
|
|
require_columns(
|
|
"pull_request",
|
|
("id", "base_repo_id", "has_merged", "merger_id"),
|
|
)
|
|
merger_rows = fetch_rows(
|
|
"pull merger relations",
|
|
"SELECT p.id,p.base_repo_id,p.has_merged,p.merger_id,typeof(p.id),"
|
|
"typeof(p.base_repo_id),typeof(p.has_merged),typeof(p.merger_id) "
|
|
"FROM pull_request AS p INNER JOIN issue AS i ON i.id=p.issue_id "
|
|
f"WHERE i.repo_id IN ({placeholders}) ORDER BY p.base_repo_id,p.id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_mergers = set()
|
|
for row in merger_rows:
|
|
pull_id, repo_id, has_merged, merger_id = row[0:4]
|
|
if (
|
|
tuple(row[4:7]) != ("integer", "integer", "integer")
|
|
or row[7] not in ("integer", "null")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (pull_id, repo_id, has_merged)
|
|
)
|
|
or pull_id <= 0
|
|
or pull_id in seen_mergers
|
|
or pull_to_repo.get(pull_id) != repo_id
|
|
or has_merged not in (0, 1)
|
|
):
|
|
die("Gitea salvage pull merger subrelation is invalid")
|
|
seen_mergers.add(pull_id)
|
|
if merger_id is None:
|
|
subrelation_per_repo[repo_id]["null_encodings"][
|
|
"pull_request.merger_id"
|
|
] += 1
|
|
merger_id = 0
|
|
if (
|
|
not isinstance(merger_id, int)
|
|
or isinstance(merger_id, bool)
|
|
or merger_id < 0
|
|
or (not has_merged and merger_id != 0)
|
|
):
|
|
die("Gitea salvage pull merger identity is invalid")
|
|
if has_merged:
|
|
add_subrelation_count(repo_id, "pull_merger")
|
|
add_subrelation_actor(repo_id, "pull_merger", merger_id)
|
|
if seen_mergers != set(pull_to_repo):
|
|
die("Gitea salvage pull merger subrelation coverage is incomplete")
|
|
|
|
issue_pr_subrelations = {
|
|
"aggregates": {
|
|
"actor_classes": {
|
|
label: {
|
|
actor_class: sum(
|
|
record["actor_classes"][label][actor_class]
|
|
for record in subrelation_per_repo.values()
|
|
)
|
|
for actor_class in ("deleted", "kept", "system-or-external")
|
|
}
|
|
for label in GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_ACTORS
|
|
},
|
|
"counts": {
|
|
label: sum(
|
|
record["counts"][label]
|
|
for record in subrelation_per_repo.values()
|
|
)
|
|
for label in GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_COUNTS
|
|
},
|
|
"external_author_provenance": {
|
|
source: {
|
|
field: sum(
|
|
record["external_author_provenance"][source][field]
|
|
for record in subrelation_per_repo.values()
|
|
)
|
|
for field in (
|
|
"id_without_name",
|
|
"name_bytes",
|
|
"name_without_id",
|
|
"rows_with_id",
|
|
"rows_with_name",
|
|
)
|
|
}
|
|
for source in GITEA_SALVAGE_CLOSURE_EXTERNAL_AUTHOR_SOURCES
|
|
},
|
|
"null_encodings": {
|
|
column: sum(
|
|
record["null_encodings"][column]
|
|
for record in subrelation_per_repo.values()
|
|
)
|
|
for column in (
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_NULLABLE_COLUMNS
|
|
)
|
|
},
|
|
"target_repository_classes": {
|
|
label: {
|
|
target_class: sum(
|
|
record["target_repository_classes"][label][target_class]
|
|
for record in subrelation_per_repo.values()
|
|
)
|
|
for target_class in ("deleted", "global", "kept")
|
|
}
|
|
for label in (
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_REPOSITORIES
|
|
)
|
|
},
|
|
},
|
|
"conditional_hold_blockers": (
|
|
["issue-pr-team-mapping-hold"]
|
|
if subrelation_holds
|
|
else []
|
|
),
|
|
"extra_schema_coverage": [
|
|
closure_extra_schema[table]
|
|
for table in sorted(closure_extra_schema)
|
|
],
|
|
"holds": sorted(
|
|
subrelation_holds,
|
|
key=lambda row: (
|
|
row["kind"],
|
|
row["old_repo_id"],
|
|
row["old_row_id"],
|
|
row["old_team_id"],
|
|
),
|
|
),
|
|
"per_repository": [
|
|
subrelation_per_repo[repo_id]
|
|
for repo_id in kept_repo_ids
|
|
],
|
|
"schema": "nodedc.gitea.salvage-issue-pr-subrelation-closure/v1",
|
|
}
|
|
query_contract.extend(
|
|
(
|
|
{
|
|
"join": (
|
|
"comment-optional-identities-to-exact-label-project-"
|
|
"milestone-time-team-issue-review-and-xref-targets"
|
|
),
|
|
"label": "comment-polymorphic-subrelations",
|
|
"output": (
|
|
"counts-classes-null-encodings-external-author-byte-"
|
|
"lengths-and-team-hold-ids-only"
|
|
),
|
|
"table": "comment",
|
|
},
|
|
{
|
|
"join": "issue_content_history.comment_id-to-same-kept-issue",
|
|
"label": "content-history-comment-relations",
|
|
"output": "issue-vs-comment-history-counts-only",
|
|
"table": "issue_content_history",
|
|
},
|
|
{
|
|
"join": (
|
|
"review.reviewer_team_id-to-existing-team-and-external-"
|
|
"author-presence-classification"
|
|
),
|
|
"label": "review-team-and-external-author-relations",
|
|
"output": "counts-byte-lengths-and-exact-team-hold-ids-only",
|
|
"table": "review",
|
|
},
|
|
{
|
|
"join": "pull_request.merger_id-to-full-user-decision-partition",
|
|
"label": "pull-merger-relations",
|
|
"output": "counts-and-actor-classes-no-commit-or-user-payload",
|
|
"table": "pull_request",
|
|
},
|
|
)
|
|
)
|
|
|
|
require_columns(
|
|
"pull_auto_merge",
|
|
("id", "pull_id", "doer_id", "merge_style", "message"),
|
|
)
|
|
auto_merge_rows = fetch_rows(
|
|
"pull auto merges",
|
|
"SELECT a.id,a.pull_id,a.doer_id,p.base_repo_id,typeof(a.id),"
|
|
"typeof(a.pull_id),typeof(a.doer_id),typeof(p.base_repo_id),"
|
|
"typeof(a.merge_style),CASE WHEN a.merge_style IS NULL THEN 0 "
|
|
"ELSE length(CAST(a.merge_style AS BLOB)) END,typeof(a.message),"
|
|
"CASE WHEN a.message IS NULL THEN 0 ELSE length(CAST(a.message AS BLOB)) END "
|
|
"FROM pull_auto_merge AS a INNER JOIN pull_request AS p ON p.id=a.pull_id "
|
|
"INNER JOIN issue AS i ON i.id=p.issue_id "
|
|
f"WHERE i.repo_id IN ({placeholders}) ORDER BY i.repo_id,a.id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_auto_merges = set()
|
|
for row in auto_merge_rows:
|
|
merge_id, pull_id, doer_id, repo_id = row[0:4]
|
|
if (
|
|
tuple(row[4:8]) != ("integer", "integer", "integer", "integer")
|
|
or row[8] not in ("null", "text")
|
|
or row[10] not in ("null", "text")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (merge_id, pull_id, doer_id, repo_id, row[9], row[11])
|
|
)
|
|
or merge_id <= 0
|
|
or merge_id in seen_auto_merges
|
|
or pull_to_repo.get(pull_id) != repo_id
|
|
):
|
|
die("Gitea salvage closure pull-auto-merge row is invalid")
|
|
seen_auto_merges.add(merge_id)
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
doer_id,
|
|
user_classes,
|
|
"pull auto merge",
|
|
)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"pull_auto_merges",
|
|
actor_class,
|
|
text_bytes={"merge_style": row[9], "message": row[11]},
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "pull_auto_merge.pull_id-to-kept-pull-wrapper",
|
|
"label": "pull-auto-merges",
|
|
"output": "counts-actor-classes-text-byte-lengths-no-payload",
|
|
"table": "pull_auto_merge",
|
|
}
|
|
)
|
|
|
|
require_columns(
|
|
"notification",
|
|
("id", "user_id", "repo_id", "issue_id", "comment_id"),
|
|
)
|
|
notification_rows = fetch_rows(
|
|
"notifications",
|
|
f"SELECT id,user_id,repo_id,issue_id,comment_id,typeof(id),"
|
|
"typeof(user_id),typeof(repo_id),typeof(issue_id),typeof(comment_id) "
|
|
f"FROM notification WHERE repo_id IN ({placeholders}) ORDER BY repo_id,id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_notifications = set()
|
|
for row in notification_rows:
|
|
notification_id, user_id, repo_id, issue_id, comment_id = row[0:5]
|
|
if (
|
|
tuple(row[5:8]) != ("integer", "integer", "integer")
|
|
or row[8] not in ("integer", "null")
|
|
or row[9] not in ("integer", "null")
|
|
or not all(
|
|
value is None
|
|
or isinstance(value, int) and not isinstance(value, bool)
|
|
for value in row[0:5]
|
|
)
|
|
or notification_id <= 0
|
|
or notification_id in seen_notifications
|
|
or repo_id not in kept_repo_set
|
|
or (issue_id or 0) < 0
|
|
or (comment_id or 0) < 0
|
|
or issue_id and issue_to_repo.get(issue_id) != repo_id
|
|
or comment_id and comment_to_repo.get(comment_id) != repo_id
|
|
):
|
|
die("Gitea salvage closure notification row is invalid")
|
|
seen_notifications.add(notification_id)
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
user_id,
|
|
user_classes,
|
|
"notification",
|
|
)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"notifications",
|
|
actor_class,
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "notification.repo_id-and-optional-kept-issue-comment",
|
|
"label": "notifications",
|
|
"output": "counts-and-actor-classes-no-commit-or-content",
|
|
"table": "notification",
|
|
}
|
|
)
|
|
|
|
require_columns("repo_unit", ("id", "repo_id", "type", "config"))
|
|
unit_rows = fetch_rows(
|
|
"repo units",
|
|
f"SELECT id,repo_id,type,typeof(id),typeof(repo_id),typeof(type),"
|
|
"typeof(config),CASE WHEN config IS NULL THEN 0 "
|
|
"ELSE length(CAST(config AS BLOB)) END FROM repo_unit "
|
|
f"WHERE repo_id IN ({placeholders}) ORDER BY repo_id,type,id",
|
|
kept_repo_ids,
|
|
)
|
|
unit_types_by_repo = {repo_id: {} for repo_id in kept_repo_ids}
|
|
seen_units = set()
|
|
for row in unit_rows:
|
|
unit_id, repo_id, unit_type = row[0:3]
|
|
if (
|
|
tuple(row[3:6]) != ("integer", "integer", "integer")
|
|
or row[6] not in ("null", "text")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (unit_id, repo_id, unit_type, row[7])
|
|
)
|
|
or unit_id <= 0
|
|
or unit_id in seen_units
|
|
or repo_id not in kept_repo_set
|
|
or not 1 <= unit_type <= 10
|
|
or unit_type in unit_types_by_repo[repo_id]
|
|
):
|
|
die("Gitea salvage closure repo-unit row is invalid")
|
|
seen_units.add(unit_id)
|
|
unit_types_by_repo[repo_id][str(unit_type)] = 1
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"repo_units",
|
|
text_bytes={"config": row[7]},
|
|
)
|
|
for record in per_repository:
|
|
record["unit_types"] = unit_types_by_repo[record["old_repo_id"]]
|
|
query_contract.append(
|
|
{
|
|
"join": "repo_unit.repo_id-in-exact-kept-set",
|
|
"label": "repo-units",
|
|
"output": "type-counts-and-config-byte-length-no-config-payload",
|
|
"table": "repo_unit",
|
|
}
|
|
)
|
|
|
|
require_columns(
|
|
"attachment",
|
|
(
|
|
"id",
|
|
"uuid",
|
|
"repo_id",
|
|
"issue_id",
|
|
"release_id",
|
|
"uploader_id",
|
|
"comment_id",
|
|
"size",
|
|
),
|
|
)
|
|
attachment_rows = fetch_rows(
|
|
"attachment manifest",
|
|
f"SELECT id,uuid,repo_id,issue_id,release_id,uploader_id,comment_id,size,"
|
|
"typeof(id),typeof(uuid),typeof(repo_id),typeof(issue_id),"
|
|
"typeof(release_id),typeof(uploader_id),typeof(comment_id),typeof(size) "
|
|
f"FROM attachment WHERE repo_id IN ({placeholders}) ORDER BY repo_id,id",
|
|
kept_repo_ids,
|
|
)
|
|
attachment_manifest = []
|
|
seen_attachment_ids = set()
|
|
seen_attachment_uuids = set()
|
|
attachment_link_classes = {
|
|
"comment": 0,
|
|
"issue": 0,
|
|
"multi-link": 0,
|
|
"release": 0,
|
|
"unlinked": 0,
|
|
}
|
|
for row in attachment_rows:
|
|
(
|
|
attachment_id,
|
|
attachment_uuid,
|
|
repo_id,
|
|
issue_id,
|
|
release_id,
|
|
uploader_id,
|
|
comment_id,
|
|
declared_size,
|
|
) = row[0:8]
|
|
if (
|
|
row[8] != "integer"
|
|
or row[9] != "text"
|
|
or row[10] != "integer"
|
|
or any(value not in ("integer", "null") for value in row[11:15])
|
|
or row[15] != "integer"
|
|
or not all(
|
|
value is None
|
|
or isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (
|
|
attachment_id,
|
|
repo_id,
|
|
issue_id,
|
|
release_id,
|
|
uploader_id,
|
|
comment_id,
|
|
declared_size,
|
|
)
|
|
)
|
|
or attachment_id <= 0
|
|
or attachment_id in seen_attachment_ids
|
|
or not isinstance(attachment_uuid, str)
|
|
or re.fullmatch(
|
|
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
|
|
attachment_uuid,
|
|
)
|
|
is None
|
|
or attachment_uuid in seen_attachment_uuids
|
|
or repo_id not in kept_repo_set
|
|
or (issue_id or 0) < 0
|
|
or (release_id or 0) < 0
|
|
or (comment_id or 0) < 0
|
|
or declared_size < 0
|
|
or declared_size > GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES
|
|
):
|
|
die("Gitea salvage closure attachment manifest row is invalid")
|
|
links = []
|
|
if issue_id:
|
|
if issue_to_repo.get(issue_id) != repo_id:
|
|
die("Gitea salvage closure attachment issue relation is invalid")
|
|
links.append("issue")
|
|
if release_id:
|
|
if release_to_repo.get(release_id) != repo_id:
|
|
die("Gitea salvage closure attachment release relation is invalid")
|
|
links.append("release")
|
|
if comment_id:
|
|
if comment_to_repo.get(comment_id) != repo_id:
|
|
die("Gitea salvage closure attachment comment relation is invalid")
|
|
links.append("comment")
|
|
link_class = (
|
|
"unlinked"
|
|
if not links
|
|
else "multi-link"
|
|
if len(links) > 1
|
|
else links[0]
|
|
)
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
uploader_id,
|
|
user_classes,
|
|
"attachment uploader",
|
|
)
|
|
attachment_link_classes[link_class] += 1
|
|
seen_attachment_ids.add(attachment_id)
|
|
seen_attachment_uuids.add(attachment_uuid)
|
|
attachment_manifest.append(
|
|
{
|
|
"content_hash": "unavailable-in-schema",
|
|
"declared_size": declared_size,
|
|
"disposition": "PHYSICAL_VERIFY_THEN_SANITIZED_ARCHIVE",
|
|
"link_class": link_class,
|
|
"link_ids": {
|
|
"comment_id": int(comment_id or 0),
|
|
"issue_id": int(issue_id or 0),
|
|
"release_id": int(release_id or 0),
|
|
},
|
|
"old_attachment_id": attachment_id,
|
|
"old_repo_id": repo_id,
|
|
"uploader_class": actor_class,
|
|
"uuid": attachment_uuid,
|
|
}
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "attachment.repo-and-exact-issue-release-comment-relations",
|
|
"label": "attachment-manifest",
|
|
"output": (
|
|
"id-uuid-relations-size-uploader-class-no-name-or-file-bytes"
|
|
),
|
|
"table": "attachment",
|
|
}
|
|
)
|
|
|
|
# Package rows are intentionally reduced to relationship identifiers and
|
|
# declared blob sizes. Names, versions, metadata_json and property
|
|
# name/value payloads are never selected because Packages remain disabled.
|
|
require_columns("package", ("id", "repo_id", "owner_id"))
|
|
package_rows = fetch_rows(
|
|
"packages",
|
|
f"SELECT id,repo_id,owner_id,typeof(id),typeof(repo_id),typeof(owner_id) "
|
|
f"FROM package WHERE repo_id IN ({placeholders}) ORDER BY repo_id,id",
|
|
kept_repo_ids,
|
|
)
|
|
package_to_repo = {}
|
|
for row in package_rows:
|
|
package_id, repo_id, owner_id = row[0:3]
|
|
if (
|
|
tuple(row[3:6]) != ("integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (package_id, repo_id, owner_id)
|
|
)
|
|
or package_id <= 0
|
|
or package_id in package_to_repo
|
|
or repo_id not in kept_repo_set
|
|
or owner_id <= 0
|
|
):
|
|
die("Gitea salvage closure package row is invalid")
|
|
package_to_repo[package_id] = repo_id
|
|
gitea_salvage_closure_add_metric(by_repo_id[repo_id], "packages")
|
|
query_contract.append(
|
|
{
|
|
"join": "package.repo_id-in-exact-kept-set",
|
|
"label": "packages",
|
|
"output": "counts-and-relationship-ids-no-name-or-metadata",
|
|
"table": "package",
|
|
}
|
|
)
|
|
|
|
require_columns("package_version", ("id", "package_id", "creator_id"))
|
|
package_version_rows = fetch_rows(
|
|
"package versions",
|
|
"SELECT v.id,v.package_id,v.creator_id,p.repo_id,typeof(v.id),"
|
|
"typeof(v.package_id),typeof(v.creator_id),typeof(p.repo_id) "
|
|
"FROM package_version AS v INNER JOIN package AS p ON p.id=v.package_id "
|
|
f"WHERE p.repo_id IN ({placeholders}) ORDER BY p.repo_id,v.id",
|
|
kept_repo_ids,
|
|
)
|
|
package_version_to_repo = {}
|
|
for row in package_version_rows:
|
|
version_id, package_id, creator_id, repo_id = row[0:4]
|
|
if (
|
|
tuple(row[4:8]) != ("integer", "integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (version_id, package_id, creator_id, repo_id)
|
|
)
|
|
or version_id <= 0
|
|
or version_id in package_version_to_repo
|
|
or package_to_repo.get(package_id) != repo_id
|
|
):
|
|
die("Gitea salvage closure package-version row is invalid")
|
|
package_version_to_repo[version_id] = repo_id
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
creator_id,
|
|
user_classes,
|
|
"package version creator",
|
|
)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"package_versions",
|
|
actor_class,
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "package_version.package_id-to-kept-package",
|
|
"label": "package-versions",
|
|
"output": "counts-and-creator-class-no-version-or-metadata",
|
|
"table": "package_version",
|
|
}
|
|
)
|
|
|
|
require_columns("package_file", ("id", "version_id", "blob_id"))
|
|
package_file_rows = fetch_rows(
|
|
"package files",
|
|
"SELECT f.id,f.version_id,f.blob_id,p.repo_id,typeof(f.id),"
|
|
"typeof(f.version_id),typeof(f.blob_id),typeof(p.repo_id) "
|
|
"FROM package_file AS f "
|
|
"INNER JOIN package_version AS v ON v.id=f.version_id "
|
|
"INNER JOIN package AS p ON p.id=v.package_id "
|
|
f"WHERE p.repo_id IN ({placeholders}) ORDER BY p.repo_id,f.id",
|
|
kept_repo_ids,
|
|
)
|
|
package_file_to_repo = {}
|
|
package_file_blob = {}
|
|
repo_blob_ids = {repo_id: set() for repo_id in kept_repo_ids}
|
|
for row in package_file_rows:
|
|
file_id, version_id, blob_id, repo_id = row[0:4]
|
|
if (
|
|
tuple(row[4:8]) != ("integer", "integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (file_id, version_id, blob_id, repo_id)
|
|
)
|
|
or file_id <= 0
|
|
or file_id in package_file_to_repo
|
|
or blob_id <= 0
|
|
or package_version_to_repo.get(version_id) != repo_id
|
|
):
|
|
die("Gitea salvage closure package-file row is invalid")
|
|
package_file_to_repo[file_id] = repo_id
|
|
package_file_blob[file_id] = blob_id
|
|
repo_blob_ids[repo_id].add(blob_id)
|
|
gitea_salvage_closure_add_metric(by_repo_id[repo_id], "package_files")
|
|
query_contract.append(
|
|
{
|
|
"join": "package_file.version_id-to-kept-package-version",
|
|
"label": "package-files",
|
|
"output": "counts-and-blob-relationship-ids-no-file-name",
|
|
"table": "package_file",
|
|
}
|
|
)
|
|
|
|
require_columns("package_blob", ("id", "size"))
|
|
all_package_blob_ids = sorted(
|
|
{blob_id for values in repo_blob_ids.values() for blob_id in values}
|
|
)
|
|
package_blob_sizes = {}
|
|
if all_package_blob_ids:
|
|
blob_placeholders = ",".join("?" for _ in all_package_blob_ids)
|
|
package_blob_rows = fetch_rows(
|
|
"package blobs",
|
|
f"SELECT id,size,typeof(id),typeof(size) FROM package_blob "
|
|
f"WHERE id IN ({blob_placeholders}) ORDER BY id",
|
|
tuple(all_package_blob_ids),
|
|
)
|
|
for blob_id, blob_size, id_type, size_type in package_blob_rows:
|
|
if (
|
|
(id_type, size_type) != ("integer", "integer")
|
|
or not isinstance(blob_id, int)
|
|
or isinstance(blob_id, bool)
|
|
or not isinstance(blob_size, int)
|
|
or isinstance(blob_size, bool)
|
|
or blob_id <= 0
|
|
or blob_id in package_blob_sizes
|
|
or blob_size < 0
|
|
or blob_size > GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES
|
|
):
|
|
die("Gitea salvage closure package-blob row is invalid")
|
|
package_blob_sizes[blob_id] = blob_size
|
|
if set(package_blob_sizes) != set(all_package_blob_ids):
|
|
die("Gitea salvage closure package blob relation is incomplete")
|
|
for repo_id in kept_repo_ids:
|
|
for blob_id in sorted(repo_blob_ids[repo_id]):
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"package_blobs",
|
|
logical_bytes=package_blob_sizes[blob_id],
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": "package_blob.id-to-distinct-kept-package-file-blob-per-repository",
|
|
"label": "package-blobs",
|
|
"output": "association-counts-and-declared-logical-bytes-no-hashes",
|
|
"table": "package_blob",
|
|
}
|
|
)
|
|
|
|
require_columns("package_property", ("id", "ref_type", "ref_id"))
|
|
property_targets = {
|
|
0: package_version_to_repo,
|
|
1: package_file_to_repo,
|
|
2: package_to_repo,
|
|
}
|
|
all_property_target_ids = sorted(
|
|
{
|
|
target_id
|
|
for target_map in property_targets.values()
|
|
for target_id in target_map
|
|
}
|
|
)
|
|
if all_property_target_ids:
|
|
target_placeholders = ",".join("?" for _ in all_property_target_ids)
|
|
invalid_property_types = fetch_rows(
|
|
"package property reference types",
|
|
f"SELECT COUNT(*) FROM package_property WHERE "
|
|
f"ref_id IN ({target_placeholders}) AND ("
|
|
"typeof(ref_type) != 'integer' OR ref_type NOT IN (0,1,2))",
|
|
tuple(all_property_target_ids),
|
|
)[0][0]
|
|
if (
|
|
not isinstance(invalid_property_types, int)
|
|
or isinstance(invalid_property_types, bool)
|
|
or invalid_property_types != 0
|
|
):
|
|
die("Gitea salvage closure package-property type is invalid")
|
|
property_clauses = []
|
|
property_parameters = []
|
|
for ref_type in (0, 1, 2):
|
|
target_ids = sorted(property_targets[ref_type])
|
|
if not target_ids:
|
|
continue
|
|
property_clauses.append(
|
|
f"(ref_type=? AND ref_id IN ({','.join('?' for _ in target_ids)}))"
|
|
)
|
|
property_parameters.extend((ref_type, *target_ids))
|
|
package_property_rows = []
|
|
if property_clauses:
|
|
package_property_rows = fetch_rows(
|
|
"package properties",
|
|
"SELECT id,ref_type,ref_id,typeof(id),typeof(ref_type),"
|
|
"typeof(ref_id) FROM package_property WHERE "
|
|
+ " OR ".join(property_clauses)
|
|
+ " ORDER BY id",
|
|
tuple(property_parameters),
|
|
)
|
|
seen_properties = set()
|
|
for row in package_property_rows:
|
|
property_id, ref_type, ref_id = row[0:3]
|
|
if (
|
|
tuple(row[3:6]) != ("integer", "integer", "integer")
|
|
or not all(
|
|
isinstance(value, int) and not isinstance(value, bool)
|
|
for value in (property_id, ref_type, ref_id)
|
|
)
|
|
or property_id <= 0
|
|
or property_id in seen_properties
|
|
or ref_type not in property_targets
|
|
or ref_id not in property_targets[ref_type]
|
|
):
|
|
die("Gitea salvage closure package-property row is invalid")
|
|
seen_properties.add(property_id)
|
|
repo_id = property_targets[ref_type][ref_id]
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
"package_properties",
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": (
|
|
"package_property.ref_type-0-version-1-file-2-package-"
|
|
"to-exact-kept-package-closure"
|
|
),
|
|
"label": "package-properties",
|
|
"output": "counts-and-reference-types-no-property-name-or-value",
|
|
"table": "package_property",
|
|
}
|
|
)
|
|
|
|
# Actions are disabled in the target. The report therefore inventories
|
|
# only relationship/count/size metadata and never selects event payloads,
|
|
# workflow content, variables, secrets, runner tokens or task logs.
|
|
action_run_to_repo = {}
|
|
|
|
def inventory_direct_action_table(
|
|
label,
|
|
table,
|
|
extra_columns=(),
|
|
actor_column=None,
|
|
logical_byte_columns=(),
|
|
declared_numeric_columns=(),
|
|
):
|
|
required = ("id", "repo_id", *extra_columns)
|
|
require_columns(table, required)
|
|
select_columns = ["id", "repo_id", *extra_columns]
|
|
select_parts = [
|
|
*[f'"{column}"' for column in select_columns],
|
|
*[f'typeof("{column}") AS "{column}_type"' for column in select_columns],
|
|
]
|
|
rows = fetch_rows(
|
|
label,
|
|
f'SELECT {",".join(select_parts)} FROM "{table}" '
|
|
f"WHERE repo_id IN ({placeholders}) ORDER BY repo_id,id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_ids = set()
|
|
normalized_rows = []
|
|
for row in rows:
|
|
values = {column: row[column] for column in select_columns}
|
|
if any(row[f"{column}_type"] != "integer" for column in select_columns):
|
|
die(f"Gitea salvage closure {label} row type is invalid")
|
|
if any(
|
|
not isinstance(value, int) or isinstance(value, bool)
|
|
for value in values.values()
|
|
):
|
|
die(f"Gitea salvage closure {label} row is invalid")
|
|
relation_id = values["id"]
|
|
repo_id = values["repo_id"]
|
|
if (
|
|
relation_id <= 0
|
|
or relation_id in seen_ids
|
|
or repo_id not in kept_repo_set
|
|
):
|
|
die(f"Gitea salvage closure {label} row is invalid")
|
|
seen_ids.add(relation_id)
|
|
actor_class = None
|
|
if actor_column is not None:
|
|
actor_class = gitea_salvage_closure_actor_class(
|
|
values[actor_column],
|
|
user_classes,
|
|
label,
|
|
)
|
|
logical_bytes = 0
|
|
numeric_values = {}
|
|
for column in declared_numeric_columns:
|
|
value = values[column]
|
|
if (
|
|
value < 0
|
|
or value > GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES
|
|
):
|
|
die(f"Gitea salvage closure {label} declared value is invalid")
|
|
numeric_values[column] = value
|
|
for column in logical_byte_columns:
|
|
value = values[column]
|
|
logical_bytes += value
|
|
if logical_bytes > GITEA_SALVAGE_UNSUPPORTED_SIZE_PER_ROW_MAX_BYTES:
|
|
die(f"Gitea salvage closure {label} size exceeds the row limit")
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[repo_id],
|
|
label,
|
|
actor_class,
|
|
logical_bytes=logical_bytes,
|
|
numeric_values=numeric_values,
|
|
)
|
|
normalized_rows.append(values)
|
|
query_contract.append(
|
|
{
|
|
"join": f"{table}.repo_id-in-exact-kept-set",
|
|
"label": label.replace("_", "-"),
|
|
"output": (
|
|
"counts-actor-class-and-declared-size-metadata-only-"
|
|
"no-action-payload-secret-token-or-log"
|
|
),
|
|
"table": table,
|
|
}
|
|
)
|
|
return normalized_rows
|
|
|
|
for values in inventory_direct_action_table(
|
|
"action_runs",
|
|
"action_run",
|
|
("trigger_user_id",),
|
|
actor_column="trigger_user_id",
|
|
):
|
|
action_run_to_repo[values["id"]] = values["repo_id"]
|
|
inventory_direct_action_table(
|
|
"action_schedules",
|
|
"action_schedule",
|
|
("trigger_user_id",),
|
|
actor_column="trigger_user_id",
|
|
)
|
|
inventory_direct_action_table("action_runners", "action_runner")
|
|
inventory_direct_action_table("action_variables", "action_variable")
|
|
inventory_direct_action_table("action_secrets", "secret")
|
|
|
|
artifact_rows = inventory_direct_action_table(
|
|
"action_artifacts",
|
|
"action_artifact",
|
|
("run_id", "file_size", "file_compressed_size"),
|
|
logical_byte_columns=("file_size",),
|
|
declared_numeric_columns=("file_size", "file_compressed_size"),
|
|
)
|
|
for values in artifact_rows:
|
|
run_id = values["run_id"]
|
|
if run_id <= 0 or action_run_to_repo.get(run_id) != values["repo_id"]:
|
|
die("Gitea salvage closure action-artifact run relation is invalid")
|
|
if action_run_to_repo:
|
|
action_run_ids = sorted(action_run_to_repo)
|
|
action_run_placeholders = ",".join("?" for _ in action_run_ids)
|
|
for table in ("action_artifact", "action_run_job"):
|
|
missed = fetch_rows(
|
|
f"{table} indirect rows",
|
|
f'SELECT COUNT(*) FROM "{table}" WHERE '
|
|
f"run_id IN ({action_run_placeholders}) "
|
|
f"AND (typeof(repo_id) != 'integer' "
|
|
f"OR repo_id NOT IN ({placeholders}))",
|
|
(*action_run_ids, *kept_repo_ids),
|
|
)[0][0]
|
|
if not isinstance(missed, int) or isinstance(missed, bool) or missed:
|
|
die(f"Gitea salvage closure {table} indirect relation is invalid")
|
|
|
|
action_job_to_repo = {}
|
|
job_rows = inventory_direct_action_table(
|
|
"action_run_jobs",
|
|
"action_run_job",
|
|
("run_id",),
|
|
)
|
|
for values in job_rows:
|
|
run_id = values["run_id"]
|
|
if run_id <= 0 or action_run_to_repo.get(run_id) != values["repo_id"]:
|
|
die("Gitea salvage closure action-job run relation is invalid")
|
|
action_job_to_repo[values["id"]] = values["repo_id"]
|
|
|
|
task_rows = inventory_direct_action_table(
|
|
"action_tasks",
|
|
"action_task",
|
|
("job_id", "log_length", "log_size"),
|
|
logical_byte_columns=("log_size",),
|
|
declared_numeric_columns=("log_length", "log_size"),
|
|
)
|
|
for values in task_rows:
|
|
job_id = values["job_id"]
|
|
if (
|
|
job_id <= 0
|
|
or action_job_to_repo.get(job_id) != values["repo_id"]
|
|
or values["log_length"] < 0
|
|
):
|
|
die("Gitea salvage closure action-task job relation is invalid")
|
|
if action_job_to_repo:
|
|
action_job_ids = sorted(action_job_to_repo)
|
|
action_job_placeholders = ",".join("?" for _ in action_job_ids)
|
|
missed = fetch_rows(
|
|
"action task indirect rows",
|
|
f"SELECT COUNT(*) FROM action_task WHERE "
|
|
f"job_id IN ({action_job_placeholders}) "
|
|
f"AND (typeof(repo_id) != 'integer' "
|
|
f"OR repo_id NOT IN ({placeholders}))",
|
|
(*action_job_ids, *kept_repo_ids),
|
|
)[0][0]
|
|
if not isinstance(missed, int) or isinstance(missed, bool) or missed:
|
|
die("Gitea salvage closure action_task indirect relation is invalid")
|
|
|
|
require_columns("action_run_index", ("group_id", "max_index"))
|
|
action_index_rows = fetch_rows(
|
|
"action run indexes",
|
|
f"SELECT group_id,max_index,typeof(group_id),typeof(max_index) "
|
|
f"FROM action_run_index WHERE group_id IN ({placeholders}) "
|
|
"ORDER BY group_id",
|
|
kept_repo_ids,
|
|
)
|
|
seen_action_indexes = set()
|
|
for group_id, max_index, group_type, max_type in action_index_rows:
|
|
if (
|
|
(group_type, max_type) != ("integer", "integer")
|
|
or not isinstance(group_id, int)
|
|
or isinstance(group_id, bool)
|
|
or not isinstance(max_index, int)
|
|
or isinstance(max_index, bool)
|
|
or group_id not in kept_repo_set
|
|
or group_id in seen_action_indexes
|
|
or max_index < 0
|
|
):
|
|
die("Gitea salvage closure action-run-index row is invalid")
|
|
seen_action_indexes.add(group_id)
|
|
gitea_salvage_closure_add_metric(
|
|
by_repo_id[group_id],
|
|
"action_run_indexes",
|
|
)
|
|
query_contract.append(
|
|
{
|
|
"join": (
|
|
"action_run_index.group_id-as-gitea-resource-index-"
|
|
"repository-id-in-exact-kept-set"
|
|
),
|
|
"label": "action-run-indexes",
|
|
"output": "counts-only-no-action-payload",
|
|
"table": "action_run_index",
|
|
}
|
|
)
|
|
|
|
metric_aggregates = {}
|
|
for label in GITEA_SALVAGE_CLOSURE_METRICS:
|
|
aggregate = gitea_salvage_closure_metric_template(label)
|
|
for record in per_repository:
|
|
metric = record["closure"][label]
|
|
aggregate["rows"] += metric["rows"]
|
|
aggregate["logical_bytes"] += metric["logical_bytes"]
|
|
for actor_class, count in metric["actor_classes"].items():
|
|
aggregate["actor_classes"][actor_class] += count
|
|
for column, value in metric["numeric_totals"].items():
|
|
aggregate["numeric_totals"][column] += value
|
|
for column, byte_count in metric["text_bytes"].items():
|
|
aggregate["text_bytes"][column] += byte_count
|
|
if aggregate["logical_bytes"] > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES:
|
|
die("Gitea salvage closure aggregate byte count exceeds the limit")
|
|
if any(
|
|
value > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES
|
|
for value in aggregate["text_bytes"].values()
|
|
):
|
|
die("Gitea salvage closure aggregate text-byte count exceeds the limit")
|
|
if any(
|
|
value > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES
|
|
for value in aggregate["numeric_totals"].values()
|
|
):
|
|
die("Gitea salvage closure aggregate numeric total exceeds the limit")
|
|
metric_aggregates[label] = aggregate
|
|
|
|
actor_relation_counts = {
|
|
relation: {
|
|
"deleted": sum(
|
|
row["actor_class"] == "deleted"
|
|
for row in actor_relations
|
|
if row["relation"] == relation
|
|
),
|
|
"kept": sum(
|
|
row["actor_class"] == "kept"
|
|
for row in actor_relations
|
|
if row["relation"] == relation
|
|
),
|
|
"rows": sum(row["relation"] == relation for row in actor_relations),
|
|
}
|
|
for relation, _table, _disposition in GITEA_SALVAGE_CLOSURE_ACTOR_RELATIONS
|
|
}
|
|
unit_type_counts = {
|
|
str(unit_type): sum(
|
|
record["unit_types"].get(str(unit_type), 0)
|
|
for record in per_repository
|
|
)
|
|
for unit_type in range(1, 11)
|
|
}
|
|
issue_state_totals = {
|
|
state: sum(record["issue_states"][state] for record in per_repository)
|
|
for state in (
|
|
"ordinary_closed",
|
|
"ordinary_open",
|
|
"pull_wrapper_closed",
|
|
"pull_wrapper_open",
|
|
)
|
|
}
|
|
pull_state_totals = {
|
|
state: sum(record["pull_states"][state] for record in per_repository)
|
|
for state in ("merged", "unmerged")
|
|
}
|
|
direct_counts = (report.get("aggregates") or {}).get("direct_relation_counts")
|
|
attachment_aggregate = (report.get("aggregates") or {}).get("attachments")
|
|
cross_checks = {
|
|
"access_grants": actor_relation_counts["access_cache"]["rows"],
|
|
"action_runners": metric_aggregates["action_runners"]["rows"],
|
|
"action_runs": metric_aggregates["action_runs"]["rows"],
|
|
"action_schedules": metric_aggregates["action_schedules"]["rows"],
|
|
"action_secrets": metric_aggregates["action_secrets"]["rows"],
|
|
"action_variables": metric_aggregates["action_variables"]["rows"],
|
|
"attachments": len(attachment_manifest),
|
|
"collaborators": actor_relation_counts["collaborations"]["rows"],
|
|
"issues": (
|
|
metric_aggregates["issues_ordinary"]["rows"]
|
|
+ metric_aggregates["pull_request_wrappers"]["rows"]
|
|
),
|
|
"labels": metric_aggregates["labels"]["rows"],
|
|
"milestones": metric_aggregates["milestones"]["rows"],
|
|
"packages": metric_aggregates["packages"]["rows"],
|
|
"pull_requests_base": len(pull_to_repo),
|
|
"releases": metric_aggregates["releases"]["rows"],
|
|
}
|
|
if not isinstance(direct_counts, dict) or any(
|
|
direct_counts.get(label) != value
|
|
for label, value in cross_checks.items()
|
|
):
|
|
die("Gitea salvage closure predecessor count cross-check failed")
|
|
if (
|
|
direct_counts.get("pull_requests_head") != len(pull_to_repo)
|
|
or sum(pull_head_partitions.values()) != len(pull_to_repo)
|
|
or not isinstance(attachment_aggregate, dict)
|
|
or attachment_aggregate.get("association_rows") != len(attachment_manifest)
|
|
or attachment_aggregate.get("logical_bytes")
|
|
!= sum(row["declared_size"] for row in attachment_manifest)
|
|
):
|
|
die("Gitea salvage closure predecessor relation cross-check failed")
|
|
|
|
attachment_summary = {
|
|
"declared_logical_bytes": sum(
|
|
row["declared_size"] for row in attachment_manifest
|
|
),
|
|
"link_classes": attachment_link_classes,
|
|
"physical_presence": "not-inventoried",
|
|
"rows": len(attachment_manifest),
|
|
"unique_uuids": len(seen_attachment_uuids),
|
|
}
|
|
closure_report = {
|
|
"actor_relation_counts": actor_relation_counts,
|
|
"actor_relations": actor_relations,
|
|
"aggregates": metric_aggregates,
|
|
"attachment_manifest": attachment_manifest,
|
|
"attachment_summary": attachment_summary,
|
|
"coverage": {
|
|
"access_collaboration": (
|
|
"exact-id-mode-and-full-kept-deleted-user-partition"
|
|
),
|
|
"actions": (
|
|
"relationship-count-size-metadata-only-no-payload-"
|
|
"target-disabled"
|
|
),
|
|
"attachments": (
|
|
"database-identifier-relation-size-manifest-only-"
|
|
"physical-verifier-pending"
|
|
),
|
|
"issues_pull_requests": (
|
|
"schema-bound-primary-dependent-and-concrete-polymorphic-"
|
|
"subrelations-counts-classes-text-byte-lengths-only"
|
|
),
|
|
"packages": (
|
|
"relationship-count-size-metadata-only-no-payload-"
|
|
"target-disabled"
|
|
),
|
|
"projects_releases_labels_units": (
|
|
"relationship-counts-text-byte-lengths-and-unit-types-only"
|
|
),
|
|
},
|
|
"cross_repo_issue_dependencies": cross_repo_dependencies,
|
|
"database_sha256": GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256,
|
|
"decision_manifest_sha256": GITEA_SALVAGE_DECISION_MANIFEST_SHA256,
|
|
"kept_repository_ids": kept_repo_ids,
|
|
"issue_state_totals": issue_state_totals,
|
|
"issue_pr_subrelations": issue_pr_subrelations,
|
|
"per_repository": per_repository,
|
|
"privacy_contract": {
|
|
"attachment_identifier": "strict-lowercase-uuid-only",
|
|
"payload_values_selected": False,
|
|
"prohibited_exports": [
|
|
"action-event-or-workflow-payload",
|
|
"action-log-or-storage-path",
|
|
"email-password-token-key-or-secret",
|
|
"issue-comment-review-release-or-project-text",
|
|
"package-name-version-metadata-hash-property-name-or-value",
|
|
],
|
|
"text_evidence": "sqlite-byte-lengths-only",
|
|
},
|
|
"pull_head_repository_partition": pull_head_partitions,
|
|
"pull_state_totals": pull_state_totals,
|
|
"query_contract": sorted(query_contract, key=lambda item: item["label"]),
|
|
"remaining_blockers": sorted(
|
|
[
|
|
"attachment-physical-verifier-pending",
|
|
"closure-report-review-pin-pending",
|
|
"collaboration-kept-user-mapping-verifier-pending",
|
|
"issue-pr-metadata-sanitized-archive-verifier-pending",
|
|
"package-action-physical-closure-verifier-pending",
|
|
"target-unit-policy-acceptance-pending",
|
|
]
|
|
+ issue_pr_subrelations["conditional_hold_blockers"]
|
|
),
|
|
"schema": "nodedc.gitea.salvage-closure-inventory/v1",
|
|
"schema_catalog_sha256": report["schema_catalog_sha256"],
|
|
"schema_coverage": [
|
|
{
|
|
"columns": sorted(columns),
|
|
"table": table,
|
|
}
|
|
for table, columns in sorted(required_schema.items())
|
|
],
|
|
"scope": {
|
|
"all_repositories": len(all_repo_ids),
|
|
"all_users": len(user_classes),
|
|
"deleted_repositories": len(all_repo_ids) - len(kept_repo_ids),
|
|
"deleted_users": sum(
|
|
value == "deleted" for value in user_classes.values()
|
|
),
|
|
"kept_repositories": len(kept_repo_ids),
|
|
"kept_users": sum(value == "kept" for value in user_classes.values()),
|
|
},
|
|
"snapshot_uuid": GITEA_SALVAGE_SNAPSHOT_UUID,
|
|
"source_evidence": {
|
|
"predecessor_artifact_sha256": (
|
|
GITEA_SALVAGE_CLOSURE_PREDECESSOR_ARTIFACT_SHA256
|
|
),
|
|
"predecessor_disposition_sha256": (
|
|
GITEA_SALVAGE_CLOSURE_PREDECESSOR_DISPOSITION_SHA256
|
|
),
|
|
"reference_manifest_sha256": (
|
|
GITEA_SALVAGE_DISPOSITION_REFERENCE_MANIFEST_SHA256
|
|
),
|
|
"semantic_topics_sha256": topics["sha256"],
|
|
"unsupported_report_sha256": unsupported["sha256"],
|
|
"unsupported_schema_sha256": report["schema_catalog_sha256"],
|
|
},
|
|
"target_policy": {
|
|
"access_cache": "RESET_AND_RECOMPUTE",
|
|
"actions": "DISABLED_DROP_ALL_LEGACY_ROWS_AND_PHYSICAL_STATE",
|
|
"attachments": "SANITIZED_ARCHIVE_ONLY_AFTER_PHYSICAL_VERIFIER",
|
|
"collaborations": "RECREATE_EXACT_KEPT_ACTORS_AFTER_ID_MAP",
|
|
"issues_pull_requests_and_dependents": "SANITIZED_ARCHIVE_ONLY",
|
|
"packages": "DISABLED_DROP_ALL_LEGACY_ROWS_AND_PHYSICAL_STATE",
|
|
"repository_counters_watches_stars": "RESET_AND_RECOMPUTE",
|
|
"secrets_integrations_credentials": "IMPORT_ZERO",
|
|
},
|
|
"unit_type_counts": unit_type_counts,
|
|
}
|
|
canonical = canonical_gitea_salvage_evidence(
|
|
closure_report,
|
|
"incident closure report",
|
|
)
|
|
return {"report": closure_report, **canonical}
|
|
|
|
def gitea_salvage_refname_is_safe(refname):
|
|
try:
|
|
encoded_refname = refname.encode("utf-8") if isinstance(refname, str) else b""
|
|
except UnicodeEncodeError:
|
|
return False
|
|
if (
|
|
not isinstance(refname, str)
|
|
or not refname.startswith("refs/")
|
|
or len(encoded_refname) > 512
|
|
or refname.endswith(("/", "."))
|
|
or ".." in refname
|
|
or "@{" in refname
|
|
or "//" in refname
|
|
or any(char in refname for char in " ~^:?*[\\")
|
|
or any(ord(char) < 0x20 or ord(char) == 0x7F for char in refname)
|
|
):
|
|
return False
|
|
parts = refname.split("/")
|
|
return all(
|
|
part
|
|
and not part.startswith(".")
|
|
and not part.endswith(".lock")
|
|
for part in parts
|
|
)
|
|
|
|
|
|
def gitea_salvage_file_has_nocow(path):
|
|
flags = array.array("I", [0])
|
|
try:
|
|
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
try:
|
|
fcntl.ioctl(descriptor, GITEA_SALVAGE_FS_IOC_GETFLAGS, flags, True)
|
|
finally:
|
|
os.close(descriptor)
|
|
except OSError:
|
|
die(f"Gitea salvage cannot attest file flags: {path}")
|
|
return bool(flags[0] & GITEA_SALVAGE_FS_NOCOW_FL)
|
|
|
|
|
|
def read_gitea_salvage_ref_file(path, label):
|
|
try:
|
|
path_stat = path.lstat()
|
|
raw = path.read_bytes()
|
|
except (FileNotFoundError, OSError):
|
|
die(f"Gitea salvage {label} is unreadable")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_nlink != 1
|
|
or len(raw) > 1024
|
|
or b"\x00" in raw
|
|
):
|
|
die(f"Gitea salvage {label} is unsafe")
|
|
try:
|
|
return raw.decode("ascii")
|
|
except UnicodeDecodeError:
|
|
die(f"Gitea salvage {label} is not ASCII")
|
|
|
|
|
|
def gitea_salvage_walk_error(label):
|
|
def fail(error):
|
|
error_number = getattr(error, "errno", None)
|
|
if not isinstance(error_number, int):
|
|
error_number = "unknown"
|
|
die(f"Gitea salvage {label} traversal failed: errno={error_number}")
|
|
|
|
return fail
|
|
|
|
|
|
def inventory_gitea_salvage_bare_repository(
|
|
path,
|
|
relative_path,
|
|
trusted_device,
|
|
mountpoints,
|
|
):
|
|
try:
|
|
repo_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
return None
|
|
validate_gitea_salvage_internal_entry(
|
|
path,
|
|
repo_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISDIR,
|
|
f"repository source {relative_path}",
|
|
)
|
|
objects = path / "objects"
|
|
objects_stat = probe_gitea_salvage_path_no_follow(
|
|
path,
|
|
"objects",
|
|
trusted_device,
|
|
mountpoints,
|
|
f"repository objects root {relative_path}",
|
|
final_mode=stat.S_ISDIR,
|
|
)
|
|
if objects_stat is None:
|
|
die(f"Gitea salvage repository objects are missing: {relative_path}")
|
|
|
|
loose_refs_root = path / "refs"
|
|
loose_root_stat = probe_gitea_salvage_path_no_follow(
|
|
path,
|
|
"refs",
|
|
trusted_device,
|
|
mountpoints,
|
|
f"repository loose refs root {relative_path}",
|
|
final_mode=stat.S_ISDIR,
|
|
)
|
|
repository_info = path / "info"
|
|
repository_info_stat = probe_gitea_salvage_path_no_follow(
|
|
path,
|
|
"info",
|
|
trusted_device,
|
|
mountpoints,
|
|
f"repository info root {relative_path}",
|
|
final_mode=stat.S_ISDIR,
|
|
)
|
|
forbidden_probes = (
|
|
(objects, "info/alternates"),
|
|
(objects, "info/http-alternates"),
|
|
(path, "shallow"),
|
|
(repository_info, "grafts") if repository_info_stat is not None else None,
|
|
(loose_refs_root, "replace") if loose_root_stat is not None else None,
|
|
(path, "worktrees"),
|
|
(path, "commondir"),
|
|
)
|
|
for probe in forbidden_probes:
|
|
if probe is None:
|
|
continue
|
|
probe_root, forbidden = probe
|
|
if probe_gitea_salvage_path_no_follow(
|
|
probe_root,
|
|
forbidden,
|
|
trusted_device,
|
|
mountpoints,
|
|
f"repository forbidden path {relative_path}",
|
|
) is not None:
|
|
displayed = (probe_root / forbidden).relative_to(path).as_posix()
|
|
die(
|
|
"Gitea salvage forbidden Git material present: "
|
|
f"{relative_path}/{displayed}"
|
|
)
|
|
|
|
object_files = 0
|
|
object_bytes = 0
|
|
oid_lengths = set()
|
|
pack_members = {}
|
|
pack_bitmaps = set()
|
|
excluded_derived_files = []
|
|
excluded_quarantine_directories = []
|
|
for root, directories, files in os.walk(
|
|
objects,
|
|
topdown=True,
|
|
onerror=gitea_salvage_walk_error(
|
|
f"repository objects {relative_path}"
|
|
),
|
|
followlinks=False,
|
|
):
|
|
root_path = Path(root)
|
|
root_stat = root_path.lstat()
|
|
validate_gitea_salvage_internal_entry(
|
|
root_path,
|
|
root_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISDIR,
|
|
f"repository object directory {relative_path}",
|
|
)
|
|
directories.sort()
|
|
files.sort()
|
|
for name in tuple(directories):
|
|
child = root_path / name
|
|
child_stat = child.lstat()
|
|
child_relative = child.relative_to(objects).as_posix()
|
|
validate_gitea_salvage_internal_entry(
|
|
child,
|
|
child_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISDIR,
|
|
f"repository object directory {relative_path}",
|
|
)
|
|
if (
|
|
root_path == objects
|
|
and re.fullmatch(
|
|
r"tmp_objdir-incoming-[A-Za-z0-9]{6}",
|
|
name,
|
|
)
|
|
):
|
|
if (
|
|
stat.S_IMODE(child_stat.st_mode) != 0o755
|
|
or child_stat.st_uid != 1000
|
|
or child_stat.st_gid != 1000
|
|
or child_stat.st_nlink != 1
|
|
or gitea_salvage_file_has_nocow(child)
|
|
):
|
|
die(
|
|
"Gitea salvage receive quarantine directory is unsafe: "
|
|
f"{relative_path}/objects/{child_relative}"
|
|
)
|
|
excluded_quarantine_directories.append(
|
|
{
|
|
"kind": "receive-pack-quarantine",
|
|
"lstat": {
|
|
"gid": child_stat.st_gid,
|
|
"mode": f"{stat.S_IMODE(child_stat.st_mode):04o}",
|
|
"nlink": child_stat.st_nlink,
|
|
"size": child_stat.st_size,
|
|
"uid": child_stat.st_uid,
|
|
},
|
|
"path": f"objects/{child_relative}",
|
|
}
|
|
)
|
|
directories.remove(name)
|
|
continue
|
|
if (
|
|
child_relative not in {"info", "pack"}
|
|
and re.fullmatch(r"[a-f0-9]{2}", child_relative) is None
|
|
):
|
|
die(
|
|
"Gitea salvage unexpected object directory: "
|
|
f"{relative_path}/objects/{child_relative}"
|
|
)
|
|
for name in files:
|
|
child = root_path / name
|
|
child_stat = child.lstat()
|
|
validate_gitea_salvage_internal_entry(
|
|
child,
|
|
child_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISREG,
|
|
f"repository object file {relative_path}",
|
|
)
|
|
if (
|
|
child_stat.st_nlink != 1
|
|
or gitea_salvage_file_has_nocow(child)
|
|
):
|
|
die(f"Gitea salvage object file is unsafe: {child}")
|
|
relative = child.relative_to(objects).as_posix()
|
|
if relative == "info/packs":
|
|
if child_stat.st_size > GITEA_SALVAGE_DERIVED_INFO_PACKS_MAX_BYTES:
|
|
die(
|
|
"Gitea salvage derived object cache is oversized: "
|
|
f"{relative_path}/objects/{relative}"
|
|
)
|
|
excluded_derived_files.append(
|
|
{
|
|
"bytes": child_stat.st_size,
|
|
"kind": "dumb-http-pack-list",
|
|
"path": "objects/info/packs",
|
|
}
|
|
)
|
|
continue
|
|
if relative == "info/commit-graph":
|
|
if child_stat.st_size > GITEA_SALVAGE_DERIVED_COMMIT_GRAPH_MAX_BYTES:
|
|
die(
|
|
"Gitea salvage derived object cache is oversized: "
|
|
f"{relative_path}/objects/{relative}"
|
|
)
|
|
excluded_derived_files.append(
|
|
{
|
|
"bytes": child_stat.st_size,
|
|
"kind": "commit-graph",
|
|
"path": "objects/info/commit-graph",
|
|
}
|
|
)
|
|
continue
|
|
bitmap = re.fullmatch(
|
|
r"pack/pack-([a-f0-9]{40}|[a-f0-9]{64})\.bitmap",
|
|
relative,
|
|
)
|
|
if bitmap:
|
|
if child_stat.st_size > GITEA_SALVAGE_DERIVED_PACK_BITMAP_MAX_BYTES:
|
|
die(
|
|
"Gitea salvage derived object cache is oversized: "
|
|
f"{relative_path}/objects/{relative}"
|
|
)
|
|
digest = bitmap.group(1)
|
|
pack_bitmaps.add(digest)
|
|
excluded_derived_files.append(
|
|
{
|
|
"bytes": child_stat.st_size,
|
|
"kind": "pack-bitmap",
|
|
"path": f"objects/{relative}",
|
|
}
|
|
)
|
|
continue
|
|
loose = re.fullmatch(r"([a-f0-9]{2})/([a-f0-9]{38}|[a-f0-9]{62})", relative)
|
|
pack = re.fullmatch(
|
|
r"pack/pack-([a-f0-9]{40}|[a-f0-9]{64})\."
|
|
r"(pack|idx)",
|
|
relative,
|
|
)
|
|
if loose:
|
|
oid_lengths.add(len(loose.group(1) + loose.group(2)))
|
|
elif pack:
|
|
oid_lengths.add(len(pack.group(1)))
|
|
pack_members.setdefault(pack.group(1), set()).add(pack.group(2))
|
|
elif re.fullmatch(
|
|
r"pack/pack-([a-f0-9]{40}|[a-f0-9]{64})\.promisor",
|
|
relative,
|
|
):
|
|
die(
|
|
"Gitea salvage promisor object material is forbidden: "
|
|
f"{relative_path}/objects/{relative}"
|
|
)
|
|
else:
|
|
die(
|
|
"Gitea salvage unexpected object material: "
|
|
f"{relative_path}/objects/{relative}"
|
|
)
|
|
object_files += 1
|
|
object_bytes += child_stat.st_size
|
|
orphan_pack_bitmaps = sorted(
|
|
digest
|
|
for digest in pack_bitmaps
|
|
if pack_members.get(digest) != {"pack", "idx"}
|
|
)
|
|
if orphan_pack_bitmaps:
|
|
die(
|
|
"Gitea salvage pack bitmap lacks a complete pack/index pair: "
|
|
f"{relative_path}/{orphan_pack_bitmaps[0]}"
|
|
)
|
|
incomplete_packs = sorted(
|
|
digest
|
|
for digest, members in pack_members.items()
|
|
if members != {"pack", "idx"}
|
|
)
|
|
if incomplete_packs:
|
|
die(
|
|
"Gitea salvage incomplete pack/index pair: "
|
|
f"{relative_path}/{incomplete_packs[0]}"
|
|
)
|
|
if len(oid_lengths) > 1 or oid_lengths - {40, 64}:
|
|
die(f"Gitea salvage mixed/invalid object format: {relative_path}")
|
|
oid_length = next(iter(oid_lengths), 40)
|
|
oid_pattern = re.compile(rf"[a-f0-9]{{{oid_length}}}")
|
|
|
|
refs = {}
|
|
packed_refs = path / "packed-refs"
|
|
try:
|
|
packed_refs_stat = packed_refs.lstat()
|
|
except FileNotFoundError:
|
|
packed_refs_stat = None
|
|
if packed_refs_stat is not None:
|
|
validate_gitea_salvage_internal_entry(
|
|
packed_refs,
|
|
packed_refs_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISREG,
|
|
f"repository packed refs {relative_path}",
|
|
)
|
|
packed = read_gitea_salvage_ref_file(packed_refs, f"{relative_path} packed-refs")
|
|
previous_ref = None
|
|
for line in packed.splitlines():
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if line.startswith("^"):
|
|
if previous_ref is None or oid_pattern.fullmatch(line[1:]) is None:
|
|
die(f"Gitea salvage packed-refs peeled line is invalid: {relative_path}")
|
|
continue
|
|
fields = line.split(" ")
|
|
if (
|
|
len(fields) != 2
|
|
or oid_pattern.fullmatch(fields[0]) is None
|
|
or not gitea_salvage_refname_is_safe(fields[1])
|
|
or fields[1] in refs
|
|
):
|
|
die(f"Gitea salvage packed ref is invalid: {relative_path}")
|
|
refs[fields[1]] = fields[0]
|
|
previous_ref = fields[1]
|
|
if loose_root_stat is not None:
|
|
validate_gitea_salvage_internal_entry(
|
|
loose_refs_root,
|
|
loose_root_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISDIR,
|
|
f"repository loose refs root {relative_path}",
|
|
)
|
|
for root, directories, files in os.walk(
|
|
loose_refs_root,
|
|
topdown=True,
|
|
onerror=gitea_salvage_walk_error(
|
|
f"repository refs {relative_path}"
|
|
),
|
|
followlinks=False,
|
|
):
|
|
root_path = Path(root)
|
|
root_stat = root_path.lstat()
|
|
validate_gitea_salvage_internal_entry(
|
|
root_path,
|
|
root_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISDIR,
|
|
f"repository loose ref directory {relative_path}",
|
|
)
|
|
directories.sort()
|
|
files.sort()
|
|
for name in directories:
|
|
directory_path = root_path / name
|
|
child_stat = directory_path.lstat()
|
|
validate_gitea_salvage_internal_entry(
|
|
directory_path,
|
|
child_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISDIR,
|
|
f"repository loose ref directory {relative_path}",
|
|
)
|
|
for name in files:
|
|
ref_path = root_path / name
|
|
ref_path_stat = ref_path.lstat()
|
|
validate_gitea_salvage_internal_entry(
|
|
ref_path,
|
|
ref_path_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISREG,
|
|
f"repository loose ref file {relative_path}",
|
|
)
|
|
refname = ref_path.relative_to(path).as_posix()
|
|
value = read_gitea_salvage_ref_file(
|
|
ref_path,
|
|
f"{relative_path} loose ref",
|
|
).strip()
|
|
if (
|
|
not gitea_salvage_refname_is_safe(refname)
|
|
or oid_pattern.fullmatch(value) is None
|
|
):
|
|
die(f"Gitea salvage loose ref is invalid: {relative_path}/{refname}")
|
|
refs[refname] = value
|
|
head_path = path / "HEAD"
|
|
try:
|
|
head_stat = head_path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Gitea salvage {relative_path} HEAD is unreadable")
|
|
validate_gitea_salvage_internal_entry(
|
|
head_path,
|
|
head_stat,
|
|
trusted_device,
|
|
mountpoints,
|
|
stat.S_ISREG,
|
|
f"repository HEAD {relative_path}",
|
|
)
|
|
head_raw = read_gitea_salvage_ref_file(head_path, f"{relative_path} HEAD")
|
|
head_match = re.fullmatch(r"ref: (refs/[A-Za-z0-9._/-]+)\n?", head_raw)
|
|
if head_match is None or not gitea_salvage_refname_is_safe(head_match.group(1)):
|
|
die(f"Gitea salvage HEAD is not a safe symbolic ref: {relative_path}")
|
|
manifest_refs = [
|
|
{"name": name, "oid": oid}
|
|
for name, oid in sorted(refs.items())
|
|
]
|
|
return {
|
|
"relative_path": relative_path,
|
|
"head": head_match.group(1),
|
|
"object_format": "sha1" if oid_length == 40 else "sha256",
|
|
"object_files": object_files,
|
|
"object_bytes": object_bytes,
|
|
"excluded_derived_files": sorted(
|
|
excluded_derived_files,
|
|
key=lambda item: item["path"],
|
|
),
|
|
"excluded_derived_bytes": sum(
|
|
item["bytes"] for item in excluded_derived_files
|
|
),
|
|
"excluded_quarantine_directories": sorted(
|
|
excluded_quarantine_directories,
|
|
key=lambda item: item["path"],
|
|
),
|
|
"refs": manifest_refs,
|
|
}
|
|
|
|
|
|
def inventory_gitea_salvage_repository_refs(decisions):
|
|
repositories = []
|
|
mountpoints = gitea_salvage_mountpoints()
|
|
repository_root_stat = GITEA_SALVAGE_SNAPSHOT_REPOSITORIES.lstat()
|
|
trusted_device = repository_root_stat.st_dev
|
|
for row in decisions["kept_repositories"]:
|
|
source = GITEA_SALVAGE_SNAPSHOT_REPOSITORIES / row["repo_relative_path"]
|
|
validate_gitea_salvage_path_chain(
|
|
GITEA_SALVAGE_SNAPSHOT_REPOSITORIES,
|
|
source,
|
|
stat.S_ISDIR,
|
|
f"repository source {row['repo_relative_path']}",
|
|
trusted_device=trusted_device,
|
|
mountpoints=mountpoints,
|
|
)
|
|
inventory = inventory_gitea_salvage_bare_repository(
|
|
source,
|
|
row["repo_relative_path"],
|
|
trusted_device,
|
|
mountpoints,
|
|
)
|
|
if inventory is None:
|
|
die(f"Gitea salvage kept repository source is missing: {row['repo_relative_path']}")
|
|
inventory["old_repo_id"] = int(row["repo_id"])
|
|
inventory["owner"] = row["owner"]
|
|
inventory["slug"] = row["slug"]
|
|
inventory["wiki"] = False
|
|
repositories.append(inventory)
|
|
wiki_source = GITEA_SALVAGE_SNAPSHOT_REPOSITORIES / row["wiki_relative_path"]
|
|
wiki_path = validate_gitea_salvage_path_chain(
|
|
GITEA_SALVAGE_SNAPSHOT_REPOSITORIES,
|
|
wiki_source,
|
|
stat.S_ISDIR,
|
|
f"wiki source {row['wiki_relative_path']}",
|
|
allow_missing_final=True,
|
|
trusted_device=trusted_device,
|
|
mountpoints=mountpoints,
|
|
)
|
|
if wiki_path is None:
|
|
continue
|
|
wiki = inventory_gitea_salvage_bare_repository(
|
|
wiki_source,
|
|
row["wiki_relative_path"],
|
|
trusted_device,
|
|
mountpoints,
|
|
)
|
|
if wiki is None:
|
|
die(f"Gitea salvage wiki source disappeared: {row['wiki_relative_path']}")
|
|
wiki["old_repo_id"] = int(row["repo_id"])
|
|
wiki["owner"] = row["owner"]
|
|
wiki["slug"] = row["slug"]
|
|
wiki["wiki"] = True
|
|
repositories.append(wiki)
|
|
manifest = {
|
|
"schema": "nodedc.gitea.salvage-refs/v2",
|
|
"snapshot_uuid": GITEA_SALVAGE_SNAPSHOT_UUID,
|
|
"database_sha256": GITEA_SALVAGE_SNAPSHOT_DATABASE_SHA256,
|
|
"repositories": repositories,
|
|
}
|
|
canonical = canonical_gitea_salvage_evidence(
|
|
manifest,
|
|
"reference manifest",
|
|
)
|
|
return {
|
|
"manifest": manifest,
|
|
**canonical,
|
|
"repository_stores": len(repositories),
|
|
"object_files": sum(item["object_files"] for item in repositories),
|
|
"object_bytes": sum(item["object_bytes"] for item in repositories),
|
|
"excluded_derived_files": sum(
|
|
len(item["excluded_derived_files"]) for item in repositories
|
|
),
|
|
"excluded_derived_bytes": sum(
|
|
item["excluded_derived_bytes"] for item in repositories
|
|
),
|
|
"excluded_quarantine_directories": sum(
|
|
len(item["excluded_quarantine_directories"])
|
|
for item in repositories
|
|
),
|
|
"refs": sum(len(item["refs"]) for item in repositories),
|
|
}
|
|
|
|
|
|
def validate_gitea_salvage_disposition_evidence(
|
|
disposition,
|
|
refs,
|
|
unsupported,
|
|
topics,
|
|
):
|
|
source = disposition["sourceEvidence"]
|
|
report = unsupported["report"]
|
|
topic_evidence = topics["evidence"]
|
|
if (
|
|
refs.get("sha256") != source["referenceManifestSha256"]
|
|
or refs.get("bytes") != source["referenceManifestBytes"]
|
|
or unsupported.get("sha256")
|
|
!= source["unsupportedRepositoryReportSha256"]
|
|
or unsupported.get("bytes")
|
|
!= source["unsupportedRepositoryReportBytes"]
|
|
or report.get("schema_catalog_sha256")
|
|
!= source["unsupportedSchemaCatalogSha256"]
|
|
or report.get("database_sha256")
|
|
!= source["databaseSha256"]
|
|
or report.get("decision_manifest_sha256")
|
|
!= source["identityDecisionManifestSha256"]
|
|
or report.get("snapshot_uuid") != source["snapshotUuid"]
|
|
or report.get("schema_missing") != []
|
|
or report.get("schema_mismatch") != []
|
|
or report.get("anomalies") != []
|
|
or not isinstance(report.get("kept_repository_ids"), list)
|
|
or len(report["kept_repository_ids"])
|
|
!= disposition["scope"]["keptRepositories"]
|
|
or len(report.get("per_repository", []))
|
|
!= disposition["scope"]["keptRepositories"]
|
|
):
|
|
die("Gitea salvage disposition source evidence drift")
|
|
|
|
state_policy = disposition["repositoryStatePolicy"]
|
|
if report.get("nonzero_categories") != state_policy[
|
|
"sourceNonzeroCategories"
|
|
]:
|
|
die("Gitea salvage nonzero-category evidence drift")
|
|
expected_direct_counts = {
|
|
row["label"]: row["sourceCount"]
|
|
for row in state_policy["directRelations"]
|
|
}
|
|
direct_counts = (report.get("aggregates") or {}).get(
|
|
"direct_relation_counts"
|
|
)
|
|
if direct_counts != expected_direct_counts:
|
|
die("Gitea salvage direct-relation evidence drift")
|
|
|
|
expected_unit_counts = {
|
|
str(row["type"]): row["sourceCount"]
|
|
for row in state_policy["units"]["rows"]
|
|
if row["sourceCount"]
|
|
}
|
|
actual_unit_counts = {}
|
|
seen_repo_ids = set()
|
|
for repository in report["per_repository"]:
|
|
old_repo_id = repository.get("old_repo_id")
|
|
unit_types = repository.get("repo_unit_types")
|
|
if (
|
|
not isinstance(old_repo_id, int)
|
|
or old_repo_id <= 0
|
|
or old_repo_id in seen_repo_ids
|
|
or not isinstance(unit_types, dict)
|
|
):
|
|
die("Gitea salvage repo-unit evidence row is invalid")
|
|
seen_repo_ids.add(old_repo_id)
|
|
for unit_type, count in unit_types.items():
|
|
if (
|
|
unit_type not in expected_unit_counts
|
|
or not isinstance(count, int)
|
|
or isinstance(count, bool)
|
|
or count != 1
|
|
):
|
|
die("Gitea salvage repo-unit evidence drift")
|
|
actual_unit_counts[unit_type] = (
|
|
actual_unit_counts.get(unit_type, 0) + count
|
|
)
|
|
if actual_unit_counts != expected_unit_counts:
|
|
die("Gitea salvage repo-unit evidence drift")
|
|
|
|
expected_metadata = {
|
|
row["name"]: row["sourceRepositories"]
|
|
for row in state_policy["textMetadata"]
|
|
}
|
|
if (
|
|
(report.get("aggregates") or {}).get(
|
|
"repository_metadata_presence"
|
|
)
|
|
!= expected_metadata
|
|
):
|
|
die("Gitea salvage repository metadata evidence drift")
|
|
expected_schema_only_tables = sorted(
|
|
table
|
|
for group in state_policy["schemaOnlyDependencyGroups"]
|
|
for table in group["tables"]
|
|
)
|
|
if (
|
|
sorted((report.get("coverage") or {}).get(
|
|
"schema_only_unreviewed_tables",
|
|
[],
|
|
))
|
|
!= expected_schema_only_tables
|
|
):
|
|
die("Gitea salvage schema-only coverage evidence drift")
|
|
|
|
topic_policy = state_policy["topics"]
|
|
topic_rows = topic_evidence.get("repositories")
|
|
topic_row_ids = []
|
|
topic_row_arrays = 0
|
|
topic_row_nulls = 0
|
|
topic_row_material = 0
|
|
topic_row_topics = 0
|
|
if not isinstance(topic_rows, list):
|
|
die("Gitea salvage semantic-topics evidence rows are invalid")
|
|
for row in topic_rows:
|
|
if (
|
|
not isinstance(row, dict)
|
|
or set(row)
|
|
!= {"encoding", "material", "old_repo_id", "topic_count"}
|
|
or row.get("encoding") not in {"json-array", "json-null"}
|
|
or not isinstance(row.get("material"), bool)
|
|
or not isinstance(row.get("old_repo_id"), int)
|
|
or isinstance(row.get("old_repo_id"), bool)
|
|
or row["old_repo_id"] <= 0
|
|
or not isinstance(row.get("topic_count"), int)
|
|
or isinstance(row.get("topic_count"), bool)
|
|
or not 0 <= row["topic_count"] <= 256
|
|
or row["material"] != bool(row["topic_count"])
|
|
or (
|
|
row["encoding"] == "json-null"
|
|
and row["topic_count"] != 0
|
|
)
|
|
):
|
|
die("Gitea salvage semantic-topics evidence row is invalid")
|
|
topic_row_ids.append(row["old_repo_id"])
|
|
topic_row_arrays += int(row["encoding"] == "json-array")
|
|
topic_row_nulls += int(row["encoding"] == "json-null")
|
|
topic_row_material += int(row["material"])
|
|
topic_row_topics += row["topic_count"]
|
|
if (
|
|
topic_evidence.get("schema")
|
|
!= "nodedc.gitea.salvage-semantic-topics/v2"
|
|
or topic_row_ids != report["kept_repository_ids"]
|
|
or topic_evidence.get("database_sha256") != source["databaseSha256"]
|
|
or topic_evidence.get("decision_manifest_sha256")
|
|
!= source["identityDecisionManifestSha256"]
|
|
or topic_evidence.get("snapshot_uuid") != source["snapshotUuid"]
|
|
or len(topic_evidence.get("repositories", []))
|
|
!= disposition["scope"]["keptRepositories"]
|
|
or topic_evidence.get("serialized_arrays")
|
|
!= topic_policy["expectedSerializedArrays"]
|
|
or topic_row_arrays != topic_evidence.get("serialized_arrays")
|
|
or topic_evidence.get("serialized_nulls")
|
|
!= topic_policy["expectedSerializedNulls"]
|
|
or topic_row_nulls != topic_evidence.get("serialized_nulls")
|
|
or topic_evidence.get("material_repositories")
|
|
!= topic_policy["expectedMaterialRepositories"]
|
|
or topic_row_material != topic_evidence.get("material_repositories")
|
|
or topic_evidence.get("topics") != topic_policy["expectedTopics"]
|
|
or topic_row_topics != topic_evidence.get("topics")
|
|
or direct_counts.get("topics")
|
|
!= topic_policy["expectedRelationalRows"]
|
|
):
|
|
die("Gitea salvage semantic-topics evidence drift")
|
|
|
|
actual_decisions = []
|
|
stores = refs.get("manifest", {}).get("repositories")
|
|
if not isinstance(stores, list):
|
|
die("Gitea salvage reference manifest shape mismatch")
|
|
live_head_targets = 0
|
|
missing_head_targets = []
|
|
for repository in stores:
|
|
if (
|
|
not isinstance(repository, dict)
|
|
or not isinstance(repository.get("old_repo_id"), int)
|
|
or not isinstance(repository.get("relative_path"), str)
|
|
or not isinstance(repository.get("wiki"), bool)
|
|
or repository.get("object_format") != "sha1"
|
|
or not isinstance(repository.get("head"), str)
|
|
or not repository["head"].startswith("refs/heads/")
|
|
or not isinstance(repository.get("refs"), list)
|
|
):
|
|
die("Gitea salvage reference store evidence is invalid")
|
|
live_names = set()
|
|
for ref in repository["refs"]:
|
|
name = ref.get("name")
|
|
oid = ref.get("oid")
|
|
if repository["wiki"] and isinstance(name, str) and name.startswith(
|
|
"refs/heads/"
|
|
):
|
|
reference_disposition = "LIVE_RESTORE"
|
|
elif (
|
|
not repository["wiki"]
|
|
and isinstance(name, str)
|
|
and (
|
|
name.startswith("refs/heads/")
|
|
or name.startswith("refs/tags/")
|
|
)
|
|
):
|
|
reference_disposition = "LIVE_RESTORE"
|
|
elif (
|
|
not repository["wiki"]
|
|
and isinstance(name, str)
|
|
and (
|
|
name.startswith("refs/pull/")
|
|
or name.startswith("refs/remotes/")
|
|
)
|
|
):
|
|
reference_disposition = "SEALED_ARCHIVE_ONLY"
|
|
else:
|
|
die("Gitea salvage reference namespace is outside disposition")
|
|
if reference_disposition == "LIVE_RESTORE":
|
|
live_names.add(name)
|
|
actual_decisions.append(
|
|
{
|
|
"disposition": reference_disposition,
|
|
"name": name,
|
|
"oid": oid,
|
|
"oldRepositoryId": repository["old_repo_id"],
|
|
"repositoryPath": repository["relative_path"],
|
|
"wiki": repository["wiki"],
|
|
}
|
|
)
|
|
if repository["head"] in live_names:
|
|
live_head_targets += 1
|
|
else:
|
|
missing_head_targets.append(
|
|
{
|
|
"head": repository["head"],
|
|
"oldRepositoryId": repository["old_repo_id"],
|
|
"repositoryPath": repository["relative_path"],
|
|
"wiki": repository["wiki"],
|
|
}
|
|
)
|
|
actual_decisions.sort(
|
|
key=lambda row: (
|
|
row["oldRepositoryId"],
|
|
int(row["wiki"]),
|
|
row["repositoryPath"],
|
|
row["name"],
|
|
row["oid"],
|
|
)
|
|
)
|
|
reference_policy = disposition["referencePolicy"]
|
|
head_invariants = reference_policy["headInvariants"]
|
|
if (
|
|
len(stores) != head_invariants["stores"]
|
|
or refs.get("refs") != len(actual_decisions)
|
|
or actual_decisions != reference_policy["exactDecisions"]
|
|
or live_head_targets != head_invariants["targetsPresentAmongLiveRefs"]
|
|
or sorted(
|
|
missing_head_targets,
|
|
key=lambda row: (
|
|
row["oldRepositoryId"],
|
|
int(row["wiki"]),
|
|
row["repositoryPath"],
|
|
row["head"],
|
|
),
|
|
)
|
|
!= head_invariants["allowedMissingTargets"]
|
|
):
|
|
die("Gitea salvage exact reference disposition evidence mismatch")
|
|
|
|
live_refs = sum(
|
|
row["disposition"] == "LIVE_RESTORE" for row in actual_decisions
|
|
)
|
|
archive_refs = sum(
|
|
row["disposition"] == "SEALED_ARCHIVE_ONLY"
|
|
for row in actual_decisions
|
|
)
|
|
if (
|
|
live_refs != reference_policy["liveRestore"]["totalRefs"]
|
|
or archive_refs != reference_policy["archiveOnly"]["totalRefs"]
|
|
or live_refs + archive_refs
|
|
!= reference_policy["forensicScope"]["allDiscoveredRefs"]
|
|
):
|
|
die("Gitea salvage reference disposition totals mismatch")
|
|
|
|
return {
|
|
"archive_only_refs": archive_refs,
|
|
"blockers": list(GITEA_SALVAGE_DISPOSITION_REMAINING_BLOCKERS),
|
|
"forensic_refs": len(actual_decisions),
|
|
"live_refs": live_refs,
|
|
"sha256": GITEA_SALVAGE_DISPOSITION_SHA256,
|
|
"topics": topics,
|
|
}
|
|
|
|
|
|
def validate_gitea_salvage_closure_evidence(disposition, closure):
|
|
report = closure.get("report")
|
|
if not isinstance(report, dict):
|
|
die("Gitea salvage closure evidence report is missing")
|
|
source = disposition["sourceEvidence"]
|
|
report_source = report.get("source_evidence")
|
|
issue_pr_subrelations = report.get("issue_pr_subrelations")
|
|
if not isinstance(issue_pr_subrelations, dict):
|
|
die("Gitea salvage issue/PR subrelation evidence is missing")
|
|
conditional_hold_blockers = issue_pr_subrelations.get(
|
|
"conditional_hold_blockers"
|
|
)
|
|
expected_report_blockers = [
|
|
"attachment-physical-verifier-pending",
|
|
"closure-report-review-pin-pending",
|
|
"collaboration-kept-user-mapping-verifier-pending",
|
|
"issue-pr-metadata-sanitized-archive-verifier-pending",
|
|
"package-action-physical-closure-verifier-pending",
|
|
"target-unit-policy-acceptance-pending",
|
|
]
|
|
if isinstance(conditional_hold_blockers, list):
|
|
expected_report_blockers.extend(conditional_hold_blockers)
|
|
expected_report_blockers.sort()
|
|
if (
|
|
report.get("schema") != disposition["closureReport"]["schema"]
|
|
or report.get("database_sha256") != source["databaseSha256"]
|
|
or report.get("decision_manifest_sha256")
|
|
!= source["identityDecisionManifestSha256"]
|
|
or report.get("snapshot_uuid") != source["snapshotUuid"]
|
|
or report.get("schema_catalog_sha256")
|
|
!= source["unsupportedSchemaCatalogSha256"]
|
|
or not isinstance(report_source, dict)
|
|
or report_source.get("predecessor_artifact_sha256")
|
|
!= disposition["predecessor"]["artifactSha256"]
|
|
or report_source.get("predecessor_disposition_sha256")
|
|
!= disposition["predecessor"]["dispositionSha256"]
|
|
or report_source.get("reference_manifest_sha256")
|
|
!= source["referenceManifestSha256"]
|
|
or report_source.get("semantic_topics_sha256")
|
|
!= source["semanticTopicsSha256"]
|
|
or report_source.get("unsupported_report_sha256")
|
|
!= source["unsupportedRepositoryReportSha256"]
|
|
or report_source.get("unsupported_schema_sha256")
|
|
!= source["unsupportedSchemaCatalogSha256"]
|
|
or report.get("scope")
|
|
!= {
|
|
"all_repositories": 2058,
|
|
"all_users": 972,
|
|
"deleted_repositories": 2013,
|
|
"deleted_users": 962,
|
|
"kept_repositories": 45,
|
|
"kept_users": 10,
|
|
}
|
|
or len(report.get("kept_repository_ids", [])) != 45
|
|
or len(report.get("per_repository", [])) != 45
|
|
or report.get("remaining_blockers") != expected_report_blockers
|
|
):
|
|
die("Gitea salvage closure source evidence drift")
|
|
|
|
require_exact_json_keys(
|
|
issue_pr_subrelations,
|
|
{
|
|
"aggregates",
|
|
"conditional_hold_blockers",
|
|
"extra_schema_coverage",
|
|
"holds",
|
|
"per_repository",
|
|
"schema",
|
|
},
|
|
"Gitea salvage issue/PR subrelation evidence",
|
|
)
|
|
if issue_pr_subrelations["schema"] != (
|
|
"nodedc.gitea.salvage-issue-pr-subrelation-closure/v1"
|
|
):
|
|
die("Gitea salvage issue/PR subrelation schema drift")
|
|
|
|
def validate_nonnegative_integer_map(value, keys, label):
|
|
if not isinstance(value, dict) or set(value) != set(keys):
|
|
die(f"Gitea salvage {label} shape is invalid")
|
|
if any(
|
|
not isinstance(item, int)
|
|
or isinstance(item, bool)
|
|
or item < 0
|
|
or item > GITEA_SALVAGE_UNSUPPORTED_SIZE_TOTAL_MAX_BYTES
|
|
for item in value.values()
|
|
):
|
|
die(f"Gitea salvage {label} value is invalid")
|
|
|
|
class_keys = ("deleted", "kept", "system-or-external")
|
|
target_class_keys = ("deleted", "global", "kept")
|
|
external_fields = (
|
|
"id_without_name",
|
|
"name_bytes",
|
|
"name_without_id",
|
|
"rows_with_id",
|
|
"rows_with_name",
|
|
)
|
|
per_subrelations = issue_pr_subrelations.get("per_repository")
|
|
if (
|
|
not isinstance(per_subrelations, list)
|
|
or len(per_subrelations) != 45
|
|
or [row.get("old_repo_id") for row in per_subrelations]
|
|
!= report["kept_repository_ids"]
|
|
):
|
|
die("Gitea salvage issue/PR per-repository subrelations are invalid")
|
|
for row in per_subrelations:
|
|
if not isinstance(row, dict):
|
|
die("Gitea salvage issue/PR per-repository row is invalid")
|
|
require_exact_json_keys(
|
|
row,
|
|
{
|
|
"actor_classes",
|
|
"counts",
|
|
"external_author_provenance",
|
|
"null_encodings",
|
|
"old_repo_id",
|
|
"target_repository_classes",
|
|
},
|
|
"Gitea salvage issue/PR per-repository row",
|
|
)
|
|
validate_nonnegative_integer_map(
|
|
row["counts"],
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_COUNTS,
|
|
"issue/PR subrelation count",
|
|
)
|
|
validate_nonnegative_integer_map(
|
|
row["null_encodings"],
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_NULLABLE_COLUMNS,
|
|
"issue/PR null encoding",
|
|
)
|
|
actor_classes = row["actor_classes"]
|
|
if not isinstance(actor_classes, dict) or set(actor_classes) != set(
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_ACTORS
|
|
):
|
|
die("Gitea salvage issue/PR actor-class shape is invalid")
|
|
for label in actor_classes:
|
|
validate_nonnegative_integer_map(
|
|
actor_classes[label],
|
|
class_keys,
|
|
"issue/PR actor class",
|
|
)
|
|
if sum(actor_classes[label].values()) != row["counts"][label]:
|
|
die("Gitea salvage issue/PR actor-class count drift")
|
|
target_classes = row["target_repository_classes"]
|
|
if not isinstance(target_classes, dict) or set(target_classes) != set(
|
|
GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_REPOSITORIES
|
|
):
|
|
die("Gitea salvage issue/PR repository-class shape is invalid")
|
|
for label in target_classes:
|
|
validate_nonnegative_integer_map(
|
|
target_classes[label],
|
|
target_class_keys,
|
|
"issue/PR repository class",
|
|
)
|
|
if sum(target_classes[label].values()) != row["counts"][label]:
|
|
die("Gitea salvage issue/PR repository-class count drift")
|
|
external = row["external_author_provenance"]
|
|
if not isinstance(external, dict) or set(external) != set(
|
|
GITEA_SALVAGE_CLOSURE_EXTERNAL_AUTHOR_SOURCES
|
|
):
|
|
die("Gitea salvage external-author source shape is invalid")
|
|
for source in external:
|
|
validate_nonnegative_integer_map(
|
|
external[source],
|
|
external_fields,
|
|
"external-author provenance",
|
|
)
|
|
if (
|
|
external[source]["id_without_name"]
|
|
> external[source]["rows_with_id"]
|
|
or external[source]["name_without_id"]
|
|
> external[source]["rows_with_name"]
|
|
):
|
|
die("Gitea salvage external-author provenance is inconsistent")
|
|
|
|
expected_aggregates = {
|
|
"actor_classes": {
|
|
label: {
|
|
actor_class: sum(
|
|
row["actor_classes"][label][actor_class]
|
|
for row in per_subrelations
|
|
)
|
|
for actor_class in class_keys
|
|
}
|
|
for label in GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_ACTORS
|
|
},
|
|
"counts": {
|
|
label: sum(row["counts"][label] for row in per_subrelations)
|
|
for label in GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_COUNTS
|
|
},
|
|
"external_author_provenance": {
|
|
source: {
|
|
field: sum(
|
|
row["external_author_provenance"][source][field]
|
|
for row in per_subrelations
|
|
)
|
|
for field in external_fields
|
|
}
|
|
for source in GITEA_SALVAGE_CLOSURE_EXTERNAL_AUTHOR_SOURCES
|
|
},
|
|
"null_encodings": {
|
|
column: sum(
|
|
row["null_encodings"][column]
|
|
for row in per_subrelations
|
|
)
|
|
for column in GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_NULLABLE_COLUMNS
|
|
},
|
|
"target_repository_classes": {
|
|
label: {
|
|
target_class: sum(
|
|
row["target_repository_classes"][label][target_class]
|
|
for row in per_subrelations
|
|
)
|
|
for target_class in target_class_keys
|
|
}
|
|
for label in GITEA_SALVAGE_CLOSURE_ISSUE_SUBRELATION_REPOSITORIES
|
|
},
|
|
}
|
|
if issue_pr_subrelations.get("aggregates") != expected_aggregates:
|
|
die("Gitea salvage issue/PR subrelation aggregate drift")
|
|
|
|
extra_schema = issue_pr_subrelations.get("extra_schema_coverage")
|
|
if (
|
|
not isinstance(extra_schema, list)
|
|
or len(extra_schema) != 1
|
|
or not isinstance(extra_schema[0], dict)
|
|
or extra_schema[0].get("table") != "team"
|
|
or extra_schema[0].get("ordinary_main_table") is not True
|
|
or not {"id", "org_id"}.issubset(
|
|
{
|
|
column.get("name")
|
|
for column in extra_schema[0].get("columns", [])
|
|
if isinstance(column, dict)
|
|
}
|
|
)
|
|
):
|
|
die("Gitea salvage issue/PR extra schema coverage is invalid")
|
|
|
|
holds = issue_pr_subrelations.get("holds")
|
|
if not isinstance(holds, list) or len(holds) > (
|
|
GITEA_SALVAGE_CLOSURE_MAX_ROWS_PER_RELATION
|
|
):
|
|
die("Gitea salvage issue/PR hold inventory is invalid")
|
|
seen_holds = set()
|
|
for row in holds:
|
|
if not isinstance(row, dict):
|
|
die("Gitea salvage issue/PR hold row is invalid")
|
|
require_exact_json_keys(
|
|
row,
|
|
{
|
|
"kind",
|
|
"old_org_identity_class",
|
|
"old_org_id",
|
|
"old_repo_id",
|
|
"old_row_id",
|
|
"old_team_id",
|
|
"source_table",
|
|
},
|
|
"Gitea salvage issue/PR hold row",
|
|
)
|
|
expected_source = {
|
|
"comment-assignee-team-mapping": "comment",
|
|
"review-reviewer-team-mapping": "review",
|
|
}.get(row["kind"])
|
|
identity = (
|
|
row["source_table"],
|
|
row["old_row_id"],
|
|
row["old_team_id"],
|
|
)
|
|
if (
|
|
expected_source is None
|
|
or row["source_table"] != expected_source
|
|
or row["old_org_identity_class"] not in {"deleted", "kept"}
|
|
or row["old_repo_id"] not in report["kept_repository_ids"]
|
|
or identity in seen_holds
|
|
or any(
|
|
not isinstance(row[key], int)
|
|
or isinstance(row[key], bool)
|
|
or row[key] <= 0
|
|
for key in (
|
|
"old_org_id",
|
|
"old_repo_id",
|
|
"old_row_id",
|
|
"old_team_id",
|
|
)
|
|
)
|
|
):
|
|
die("Gitea salvage issue/PR hold row is invalid")
|
|
seen_holds.add(identity)
|
|
expected_conditional = ["issue-pr-team-mapping-hold"] if holds else []
|
|
if conditional_hold_blockers != expected_conditional:
|
|
die("Gitea salvage issue/PR conditional hold blocker drift")
|
|
if len(holds) != (
|
|
expected_aggregates["counts"]["comment_assignee_team"]
|
|
+ expected_aggregates["counts"]["review_reviewer_team"]
|
|
):
|
|
die("Gitea salvage issue/PR team hold count drift")
|
|
report_aggregates = report.get("aggregates")
|
|
history_aggregate = (
|
|
report_aggregates.get("issue_content_histories")
|
|
if isinstance(report_aggregates, dict)
|
|
else None
|
|
)
|
|
review_aggregate = (
|
|
report_aggregates.get("reviews")
|
|
if isinstance(report_aggregates, dict)
|
|
else None
|
|
)
|
|
pull_state_totals = report.get("pull_state_totals")
|
|
if (
|
|
not isinstance(history_aggregate, dict)
|
|
or not isinstance(review_aggregate, dict)
|
|
or not isinstance(pull_state_totals, dict)
|
|
or not isinstance(history_aggregate.get("rows"), int)
|
|
or isinstance(history_aggregate.get("rows"), bool)
|
|
or not isinstance(review_aggregate.get("rows"), int)
|
|
or isinstance(review_aggregate.get("rows"), bool)
|
|
or not isinstance(pull_state_totals.get("merged"), int)
|
|
or isinstance(pull_state_totals.get("merged"), bool)
|
|
or (
|
|
expected_aggregates["counts"]["content_history_comment"]
|
|
+ expected_aggregates["counts"]["content_history_issue"]
|
|
!= history_aggregate["rows"]
|
|
)
|
|
or expected_aggregates["counts"]["pull_merger"]
|
|
!= pull_state_totals["merged"]
|
|
or expected_aggregates["counts"]["review_reviewer_team"]
|
|
> review_aggregate["rows"]
|
|
):
|
|
die("Gitea salvage issue/PR subrelation coverage invariant failed")
|
|
|
|
actor_relations = report.get("actor_relations")
|
|
if not isinstance(actor_relations, list) or len(actor_relations) > 128:
|
|
die("Gitea salvage closure actor-relation evidence is invalid")
|
|
relation_identities = set()
|
|
for row in actor_relations:
|
|
if not isinstance(row, dict):
|
|
die("Gitea salvage closure actor-relation row is invalid")
|
|
require_exact_json_keys(
|
|
row,
|
|
{
|
|
"actor_class",
|
|
"disposition",
|
|
"legacy_mode",
|
|
"old_relation_id",
|
|
"old_repo_id",
|
|
"old_user_id",
|
|
"relation",
|
|
},
|
|
"Gitea salvage closure actor-relation row",
|
|
)
|
|
identity = (row["relation"], row["old_repo_id"], row["old_user_id"])
|
|
expected = (
|
|
"DROP_CACHE_RECOMPUTE"
|
|
if row["relation"] == "access_cache"
|
|
else "RECREATE_KEPT_ACTOR_AFTER_ID_MAP"
|
|
if row["relation"] == "collaborations"
|
|
and row["actor_class"] == "kept"
|
|
else "DROP_DELETED_ACTOR"
|
|
if row["relation"] == "collaborations"
|
|
and row["actor_class"] == "deleted"
|
|
else None
|
|
)
|
|
if (
|
|
identity in relation_identities
|
|
or row["relation"] not in {"access_cache", "collaborations"}
|
|
or row["actor_class"] not in {"deleted", "kept"}
|
|
or expected is None
|
|
or row["disposition"] != expected
|
|
or not isinstance(row["legacy_mode"], int)
|
|
or isinstance(row["legacy_mode"], bool)
|
|
or not 0 <= row["legacy_mode"] <= 5
|
|
):
|
|
die("Gitea salvage closure actor-relation disposition is invalid")
|
|
relation_identities.add(identity)
|
|
|
|
attachment_summary = report.get("attachment_summary")
|
|
attachment_manifest = report.get("attachment_manifest")
|
|
if (
|
|
not isinstance(attachment_summary, dict)
|
|
or not isinstance(attachment_manifest, list)
|
|
or any(not isinstance(row, dict) for row in attachment_manifest)
|
|
or attachment_summary.get("rows") != len(attachment_manifest)
|
|
or attachment_summary.get("unique_uuids") != len(attachment_manifest)
|
|
or attachment_summary.get("physical_presence") != "not-inventoried"
|
|
or attachment_summary.get("declared_logical_bytes")
|
|
!= sum(row.get("declared_size", -1) for row in attachment_manifest)
|
|
):
|
|
die("Gitea salvage closure attachment evidence is invalid")
|
|
for row in attachment_manifest:
|
|
if (
|
|
not isinstance(row, dict)
|
|
or row.get("content_hash") != "unavailable-in-schema"
|
|
or row.get("disposition")
|
|
!= "PHYSICAL_VERIFY_THEN_SANITIZED_ARCHIVE"
|
|
or row.get("link_class")
|
|
not in {"comment", "issue", "multi-link", "release", "unlinked"}
|
|
or not isinstance(row.get("declared_size"), int)
|
|
or isinstance(row.get("declared_size"), bool)
|
|
or row["declared_size"] < 0
|
|
or re.fullmatch(
|
|
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
|
|
r"[0-9a-f]{4}-[0-9a-f]{12}",
|
|
str(row.get("uuid")),
|
|
)
|
|
is None
|
|
):
|
|
die("Gitea salvage closure attachment manifest is invalid")
|
|
|
|
if (
|
|
report.get("unit_type_counts")
|
|
!= {
|
|
"1": 45,
|
|
"2": 45,
|
|
"3": 45,
|
|
"4": 45,
|
|
"5": 45,
|
|
"6": 0,
|
|
"7": 0,
|
|
"8": 45,
|
|
"9": 45,
|
|
"10": 0,
|
|
}
|
|
or (report.get("privacy_contract") or {}).get("payload_values_selected")
|
|
is not False
|
|
or (report.get("target_policy") or {}).get("actions")
|
|
!= "DISABLED_DROP_ALL_LEGACY_ROWS_AND_PHYSICAL_STATE"
|
|
or (report.get("target_policy") or {}).get("packages")
|
|
!= "DISABLED_DROP_ALL_LEGACY_ROWS_AND_PHYSICAL_STATE"
|
|
or (report.get("target_policy") or {}).get(
|
|
"secrets_integrations_credentials"
|
|
)
|
|
!= "IMPORT_ZERO"
|
|
):
|
|
die("Gitea salvage closure target disposition drift")
|
|
return {
|
|
"blockers": list(GITEA_SALVAGE_CLOSURE_REMAINING_BLOCKERS)
|
|
+ expected_conditional,
|
|
"bytes": closure["bytes"],
|
|
"json": closure["json"],
|
|
"sha256": closure["sha256"],
|
|
}
|
|
|
|
def preflight_gitea_incident_salvage(payload_dir, enforce_apply=False):
|
|
decisions = validate_gitea_incident_salvage_payload(
|
|
payload_dir,
|
|
GITEA_SALVAGE_ENTRIES,
|
|
)
|
|
try:
|
|
GITEA_ROOT.lstat()
|
|
except FileNotFoundError:
|
|
pass
|
|
else:
|
|
die("Gitea salvage candidate root must be absent")
|
|
if gitea_compose_project_container_ids():
|
|
die("Gitea salvage Compose project already has containers")
|
|
maintenance = docker_named_container_inspect_fail_closed(
|
|
GITEA_SALVAGE_MAINTENANCE_CONTAINER,
|
|
"Gitea salvage maintenance container inspect",
|
|
)
|
|
if maintenance is not None:
|
|
die("Gitea salvage maintenance container already exists")
|
|
snapshot = validate_gitea_salvage_snapshot_boundary()
|
|
database = bind_gitea_salvage_decisions_to_snapshot(decisions)
|
|
refs = inventory_gitea_salvage_repository_refs(decisions)
|
|
disposition = validate_gitea_salvage_disposition_evidence(
|
|
decisions["disposition"],
|
|
refs,
|
|
database["unsupported"],
|
|
database["topics"],
|
|
)
|
|
closure_disposition = validate_gitea_salvage_closure_evidence(
|
|
decisions["closure_disposition"],
|
|
database["closure"],
|
|
)
|
|
blockers = sorted(
|
|
set(disposition["blockers"] + closure_disposition["blockers"])
|
|
)
|
|
legacy = None
|
|
if (
|
|
GITEA_SALVAGE_EXPECTED_LEGACY_IMAGE is None
|
|
or GITEA_SALVAGE_EXPECTED_LEGACY_IMAGE_ID is None
|
|
):
|
|
blockers.append("legacy-container-identity-not-reviewed")
|
|
else:
|
|
legacy = validate_gitea_salvage_legacy_container()
|
|
|
|
common = {
|
|
"mode": "clean-state-exact-repository-material-only",
|
|
"snapshot": snapshot,
|
|
"database": database,
|
|
"disposition": disposition,
|
|
"closure_disposition": closure_disposition,
|
|
"legacy": legacy,
|
|
"refs": refs,
|
|
"blockers": blockers,
|
|
}
|
|
if not enforce_apply:
|
|
return common
|
|
die(
|
|
"Gitea salvage activation is frozen before candidate root creation: "
|
|
+ ",".join(blockers)
|
|
)
|
|
|
|
|
|
def ensure_engine_node_intelligence_secret():
|
|
try:
|
|
runtime_stat = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR.lstat()
|
|
except FileNotFoundError:
|
|
ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR.mkdir(parents=False, exist_ok=False)
|
|
runtime_stat = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR.lstat()
|
|
if stat.S_ISLNK(runtime_stat.st_mode) or not stat.S_ISDIR(runtime_stat.st_mode):
|
|
die("Engine node-intelligence runtime directory is unsafe")
|
|
os.chown(
|
|
ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR,
|
|
0,
|
|
ENGINE_NODE_INTELLIGENCE_RUNTIME_GID,
|
|
)
|
|
ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR.chmod(0o710)
|
|
|
|
try:
|
|
secret_stat = ENGINE_NODE_INTELLIGENCE_SECRET_FILE.lstat()
|
|
except FileNotFoundError:
|
|
value = secrets.token_urlsafe(48)
|
|
temporary = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR / (
|
|
f".{ENGINE_NODE_INTELLIGENCE_SECRET_FILE.name}."
|
|
f"{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(
|
|
str(temporary),
|
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
0o440,
|
|
)
|
|
os.write(descriptor, f"{value}\n".encode("ascii"))
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, 0, ENGINE_NODE_INTELLIGENCE_RUNTIME_GID)
|
|
os.fchmod(descriptor, 0o440)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary, ENGINE_NODE_INTELLIGENCE_SECRET_FILE)
|
|
fsync_directory(ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
return "created"
|
|
|
|
if (
|
|
stat.S_ISLNK(secret_stat.st_mode)
|
|
or not stat.S_ISREG(secret_stat.st_mode)
|
|
or secret_stat.st_uid != 0
|
|
or secret_stat.st_gid != ENGINE_NODE_INTELLIGENCE_RUNTIME_GID
|
|
or stat.S_IMODE(secret_stat.st_mode) != 0o440
|
|
or secret_stat.st_size < 49
|
|
or secret_stat.st_size > 512
|
|
):
|
|
die("Engine node-intelligence secret file is unsafe")
|
|
try:
|
|
value = ENGINE_NODE_INTELLIGENCE_SECRET_FILE.read_text(
|
|
encoding="ascii"
|
|
).strip()
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine node-intelligence secret file is unreadable")
|
|
if not ENGINE_NODE_INTELLIGENCE_SECRET_RE.fullmatch(value):
|
|
die("Engine node-intelligence secret file has invalid format")
|
|
return "reused"
|
|
|
|
|
|
def inspect_engine_node_intelligence_image(descriptor, required=True):
|
|
result = subprocess.run(
|
|
[str(DOCKER), "image", "inspect", ENGINE_NODE_INTELLIGENCE_IMAGE],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
if not required:
|
|
return None
|
|
die("Engine node-intelligence image is missing")
|
|
try:
|
|
images = json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
die("Engine node-intelligence image inspect returned invalid JSON")
|
|
if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict):
|
|
die("Engine node-intelligence image inspect shape mismatch")
|
|
image = images[0]
|
|
image_id = image.get("Id")
|
|
config = image.get("Config") or {}
|
|
labels = config.get("Labels") or {}
|
|
repo_tags = image.get("RepoTags") or []
|
|
if (
|
|
not isinstance(image_id, str)
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id)
|
|
or image.get("Architecture") != descriptor["image"]["architecture"]
|
|
or image.get("Os") != descriptor["image"]["os"]
|
|
or ENGINE_NODE_INTELLIGENCE_IMAGE not in repo_tags
|
|
or not isinstance(labels, dict)
|
|
or labels.get("org.opencontainers.image.revision")
|
|
!= ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT
|
|
or config.get("Entrypoint") != ["/usr/local/bin/docker-entrypoint.sh"]
|
|
or config.get("Cmd") != ["node", "dist/mcp/index.js"]
|
|
):
|
|
die("Engine node-intelligence image identity mismatch")
|
|
env = config.get("Env") or []
|
|
if any(
|
|
str(value).startswith(("AUTH_TOKEN=", "N8N_API_URL=", "N8N_API_KEY="))
|
|
for value in env
|
|
):
|
|
die("Engine node-intelligence image contains runtime authority")
|
|
return image
|
|
|
|
|
|
def install_engine_node_intelligence_image(descriptor):
|
|
if descriptor.get("action") != "activate":
|
|
die("Engine node-intelligence image install requires activation")
|
|
source_archive = (
|
|
component_root("engine") / ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL
|
|
)
|
|
validate_engine_node_intelligence_image_archive(source_archive, descriptor)
|
|
|
|
try:
|
|
releases_stat = ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.lstat()
|
|
except FileNotFoundError:
|
|
ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.mkdir(parents=False, exist_ok=False)
|
|
releases_stat = ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.lstat()
|
|
if stat.S_ISLNK(releases_stat.st_mode) or not stat.S_ISDIR(releases_stat.st_mode):
|
|
die("Engine node-intelligence releases directory is unsafe")
|
|
os.chown(ENGINE_NODE_INTELLIGENCE_RELEASES_DIR, 0, 0)
|
|
ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.chmod(0o500)
|
|
|
|
release_dir = (
|
|
ENGINE_NODE_INTELLIGENCE_RELEASES_DIR / descriptor["releaseId"]
|
|
)
|
|
try:
|
|
release_stat = release_dir.lstat()
|
|
except FileNotFoundError:
|
|
ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.chmod(0o700)
|
|
try:
|
|
release_dir.mkdir(mode=0o500, exist_ok=False)
|
|
finally:
|
|
ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.chmod(0o500)
|
|
release_stat = release_dir.lstat()
|
|
if (
|
|
stat.S_ISLNK(release_stat.st_mode)
|
|
or not stat.S_ISDIR(release_stat.st_mode)
|
|
or release_stat.st_uid != 0
|
|
):
|
|
die("Engine node-intelligence release directory is unsafe")
|
|
os.chown(release_dir, 0, 0)
|
|
release_dir.chmod(0o500)
|
|
|
|
release_archive = release_dir / "engine-node-intelligence.tar"
|
|
if release_archive.exists() or release_archive.is_symlink():
|
|
release_archive_stat = release_archive.lstat()
|
|
if (
|
|
stat.S_ISLNK(release_archive_stat.st_mode)
|
|
or not stat.S_ISREG(release_archive_stat.st_mode)
|
|
or release_archive_stat.st_uid != 0
|
|
or release_archive_stat.st_gid != 0
|
|
or stat.S_IMODE(release_archive_stat.st_mode) != 0o400
|
|
or sha256_file(release_archive) != descriptor["image"]["archiveSha256"]
|
|
):
|
|
die("Engine node-intelligence sealed release collision")
|
|
else:
|
|
release_dir.chmod(0o700)
|
|
temporary = release_dir / (
|
|
f".engine-node-intelligence.tar.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
try:
|
|
with source_archive.open("rb") as source, temporary.open("xb") as output:
|
|
shutil.copyfileobj(source, output, 1024 * 1024)
|
|
output.flush()
|
|
os.fsync(output.fileno())
|
|
os.chown(temporary, 0, 0)
|
|
temporary.chmod(0o400)
|
|
if sha256_file(temporary) != descriptor["image"]["archiveSha256"]:
|
|
die("Engine node-intelligence sealed release copy mismatch")
|
|
os.replace(temporary, release_archive)
|
|
fsync_directory(release_dir)
|
|
finally:
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
release_dir.chmod(0o500)
|
|
|
|
load = subprocess.run(
|
|
[str(DOCKER), "image", "load", "--input", str(release_archive)],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=600,
|
|
)
|
|
if load.returncode != 0:
|
|
die("Engine node-intelligence offline image load failed")
|
|
image = inspect_engine_node_intelligence_image(descriptor)
|
|
return image["Id"]
|
|
|
|
|
|
def engine_backend_container_id():
|
|
result = subprocess.run(
|
|
[*compose_base_cmd("engine"), "ps", "-q", "nodedc-backend"],
|
|
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("Engine backend topology mismatch")
|
|
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):
|
|
if (container_id is None) == (image_ref is None):
|
|
die(f"{label} probe target mismatch")
|
|
if container_id is not None:
|
|
cmd = [str(DOCKER), "exec", container_id, *arguments]
|
|
else:
|
|
cmd = [
|
|
str(DOCKER), "run", "--rm", "--network", "none",
|
|
"--tmpfs", "/tmp:mode=1777", "--env", "HOME=/tmp",
|
|
image_ref, *arguments,
|
|
]
|
|
result = subprocess.run(
|
|
cmd,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=90,
|
|
)
|
|
value = result.stdout.strip()
|
|
if (result.returncode != 0
|
|
or result.stderr.strip()
|
|
or not value
|
|
or len(value) > 512
|
|
or any(ord(character) < 0x20 and character not in "\t" for character in value)):
|
|
die(f"Engine backend {label} probe failed")
|
|
return value
|
|
|
|
|
|
def engine_backend_tool_versions(container_id=None, image_ref=None):
|
|
versions = {
|
|
"node": run_engine_backend_probe(("node", "--version"), "node", container_id, image_ref),
|
|
"sqlite": run_engine_backend_probe(("sqlite3", "--version"), "sqlite", container_id, image_ref),
|
|
"docker": run_engine_backend_probe(("docker", "--version"), "docker", container_id, image_ref),
|
|
"compose": run_engine_backend_probe(
|
|
("docker", "compose", "version"), "Docker Compose", container_id, image_ref
|
|
),
|
|
}
|
|
if (not re.fullmatch(r"v20\.[0-9]+\.[0-9]+", versions["node"])
|
|
or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:\s+.*)?", versions["sqlite"])
|
|
or not re.fullmatch(r"Docker version [0-9]+\.[0-9]+\.[0-9]+, build [A-Za-z0-9._-]+", versions["docker"])
|
|
or not re.fullmatch(r"Docker Compose version v?[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9._-]+)?", versions["compose"])):
|
|
die("Engine backend runtime tool version format mismatch")
|
|
return versions
|
|
|
|
|
|
def validate_engine_backend_mounts(container, node_modules_read_only):
|
|
expected = {
|
|
"/app": ("bind", True),
|
|
"/app/deploy/docker-compose.yml": ("bind", False),
|
|
"/seed-api": ("bind", False),
|
|
"/seed-data": ("bind", False),
|
|
"/app/node_modules": ("bind", not node_modules_read_only),
|
|
"/app/server/data": ("bind", True),
|
|
"/app/server/storage": ("bind", True),
|
|
"/app/server/logs": ("bind", True),
|
|
"/var/run/docker.sock": ("bind", True),
|
|
}
|
|
mounts = container.get("Mounts")
|
|
if not isinstance(mounts, list):
|
|
die("Engine backend mount inventory is missing")
|
|
actual = {}
|
|
node_modules_source = None
|
|
for mount in mounts:
|
|
if not isinstance(mount, dict) or not isinstance(mount.get("Destination"), str):
|
|
die("Engine backend mount inventory is invalid")
|
|
destination = mount["Destination"]
|
|
if destination in actual:
|
|
die(f"Engine backend duplicate mount destination: {destination}")
|
|
actual[destination] = (mount.get("Type"), bool(mount.get("RW")))
|
|
if destination == "/app/node_modules":
|
|
node_modules_source = mount.get("Source")
|
|
if actual != expected:
|
|
die("Engine backend mount barrier mismatch")
|
|
if (node_modules_source != str(ENGINE_CREDENTIAL_BACKEND_NODE_MODULES_DIR)
|
|
or not ENGINE_CREDENTIAL_BACKEND_NODE_MODULES_DIR.is_absolute()):
|
|
die("Engine backend node_modules mount source mismatch")
|
|
|
|
|
|
def validate_engine_backend_mounts_for_installed_runtime(
|
|
container,
|
|
node_modules_read_only,
|
|
):
|
|
publish_override = (
|
|
component_root("engine")
|
|
/ ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL
|
|
)
|
|
publish_installed = publish_override.exists() or publish_override.is_symlink()
|
|
if not publish_installed:
|
|
# The legacy credential runtime remains byte-for-byte authoritative
|
|
# until the exact Publish overlay has been installed.
|
|
return validate_engine_backend_mounts(container, node_modules_read_only)
|
|
|
|
if publish_override.is_symlink() or not publish_override.is_file():
|
|
die("installed Engine data product publish grant override is unsafe")
|
|
try:
|
|
publish_override_text = publish_override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("installed Engine data product publish grant override cannot be read")
|
|
if publish_override_text != expected_engine_data_product_publish_grant_override():
|
|
die("installed Engine data product publish grant override drift detected")
|
|
|
|
expected_publish_mounts = {
|
|
ENGINE_PUBLISH_GRANT_CONTAINER_PATH: (
|
|
"bind",
|
|
True,
|
|
str(ENGINE_PUBLISH_GRANT_STATE_PATH),
|
|
),
|
|
ENGINE_EDP_PRIVATE_KEY_CONTAINER_PATH: (
|
|
"bind",
|
|
False,
|
|
str(ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE),
|
|
),
|
|
}
|
|
read_override = component_root("engine") / ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL
|
|
read_installed = read_override.exists() or read_override.is_symlink()
|
|
if read_installed:
|
|
if read_override.is_symlink() or not read_override.is_file():
|
|
die("installed Engine data product read grant override is unsafe")
|
|
try:
|
|
read_override_text = read_override.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("installed Engine data product read grant override cannot be read")
|
|
if read_override_text != expected_engine_data_product_read_grant_override():
|
|
die("installed Engine data product read grant override drift detected")
|
|
expected_publish_mounts[ENGINE_READ_GRANT_CONTAINER_PATH] = (
|
|
"bind",
|
|
True,
|
|
str(ENGINE_READ_GRANT_STATE_PATH),
|
|
)
|
|
mounts = container.get("Mounts")
|
|
if not isinstance(mounts, list):
|
|
die("Engine backend mount inventory is missing")
|
|
baseline_mounts = []
|
|
observed_publish_mounts = set()
|
|
for mount in mounts:
|
|
if not isinstance(mount, dict) or not isinstance(mount.get("Destination"), str):
|
|
die("Engine backend mount inventory is invalid")
|
|
destination = mount["Destination"]
|
|
expected = expected_publish_mounts.get(destination)
|
|
if expected is None:
|
|
baseline_mounts.append(mount)
|
|
continue
|
|
if destination in observed_publish_mounts:
|
|
die(f"Engine backend duplicate mount destination: {destination}")
|
|
actual = (
|
|
mount.get("Type"),
|
|
bool(mount.get("RW")),
|
|
mount.get("Source"),
|
|
)
|
|
if actual != expected:
|
|
die(f"Engine backend Publish mount barrier mismatch: {destination}")
|
|
observed_publish_mounts.add(destination)
|
|
if observed_publish_mounts != set(expected_publish_mounts):
|
|
die("Engine backend Publish mount inventory mismatch")
|
|
|
|
projected = dict(container)
|
|
projected["Mounts"] = baseline_mounts
|
|
return validate_engine_backend_mounts(projected, node_modules_read_only)
|
|
|
|
|
|
def validate_engine_backend_node_intelligence_mounts_for_installed_runtime(
|
|
container,
|
|
node_modules_read_only,
|
|
expected_gateway_sha256=None,
|
|
):
|
|
descriptor = current_engine_node_intelligence_descriptor()
|
|
active = descriptor is not None and descriptor["action"] == "activate"
|
|
if not active:
|
|
# The pre-existing runtime guard remains authoritative when this
|
|
# independent overlay is inactive. Any stray secret mount therefore
|
|
# still fails its exact baseline inventory.
|
|
return validate_engine_backend_mounts_for_installed_runtime(
|
|
container,
|
|
node_modules_read_only,
|
|
)
|
|
|
|
if expected_gateway_sha256 is None:
|
|
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")
|
|
if not isinstance(mounts, list):
|
|
die("Engine backend mount inventory is missing")
|
|
baseline_mounts = []
|
|
observed = 0
|
|
for mount in mounts:
|
|
if not isinstance(mount, dict) or not isinstance(mount.get("Destination"), str):
|
|
die("Engine backend mount inventory is invalid")
|
|
if mount["Destination"] != ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH:
|
|
baseline_mounts.append(mount)
|
|
continue
|
|
observed += 1
|
|
actual = (
|
|
mount.get("Type"),
|
|
bool(mount.get("RW")),
|
|
mount.get("Source"),
|
|
)
|
|
expected = (
|
|
"bind",
|
|
False,
|
|
str(ENGINE_NODE_INTELLIGENCE_SECRET_FILE),
|
|
)
|
|
if observed != 1 or actual != expected:
|
|
die("Engine backend node-intelligence mount barrier mismatch")
|
|
if observed != 1:
|
|
die("Engine backend node-intelligence mount inventory mismatch")
|
|
|
|
# Remove only the independently proven additive mount, then delegate the
|
|
# remaining inventory to the existing Publish-aware guard and ultimately
|
|
# the byte-protected legacy baseline validator.
|
|
projected = dict(container)
|
|
projected["Mounts"] = baseline_mounts
|
|
return validate_engine_backend_mounts_for_installed_runtime(
|
|
projected,
|
|
node_modules_read_only,
|
|
)
|
|
|
|
|
|
def validate_engine_backend_dependencies(container_id):
|
|
package_path = component_root("engine") / "nodedc-source/package.json"
|
|
try:
|
|
package_stat = package_path.lstat()
|
|
except FileNotFoundError:
|
|
die("Engine backend package.json is missing")
|
|
if stat.S_ISLNK(package_stat.st_mode) or not stat.S_ISREG(package_stat.st_mode):
|
|
die("Engine backend package.json is unsafe")
|
|
package = read_strict_json(package_path, "Engine backend package.json", max_bytes=2 * 1024 * 1024)
|
|
dependencies = package.get("dependencies")
|
|
if not isinstance(dependencies, dict) or not dependencies:
|
|
die("Engine backend dependency manifest is invalid")
|
|
names = sorted(dependencies)
|
|
if any(not re.fullmatch(r"(?:@[a-z0-9._-]+/)?[a-z0-9._-]+", name) for name in names):
|
|
die("Engine backend dependency name is unsafe")
|
|
script = (
|
|
"const names=" + json.dumps(names, separators=(",", ":"))
|
|
+ ";for(const name of names)require.resolve(name,{paths:['/app']});"
|
|
+ "process.stdout.write('dependencies-ok')"
|
|
)
|
|
result = subprocess.run(
|
|
[str(DOCKER), "exec", "--workdir", "/app", container_id, "node", "-e", script],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
)
|
|
if result.returncode != 0 or result.stderr.strip() or result.stdout.strip() != "dependencies-ok":
|
|
die("Engine backend mounted dependency barrier mismatch")
|
|
|
|
|
|
def engine_backend_node_modules_tree_sha256(container_id):
|
|
script = """
|
|
const crypto=require('crypto');
|
|
const fs=require('fs');
|
|
const path=require('path');
|
|
const root='/app/node_modules';
|
|
const hash=crypto.createHash('sha256');
|
|
const lock=JSON.parse(fs.readFileSync('/app/package-lock.json','utf8'));
|
|
if(lock.lockfileVersion!==3||!lock.packages||typeof lock.packages!=='object')throw new Error('lock-invalid');
|
|
const expected=new Map(Object.entries(lock.packages).filter(([key,value])=>key.startsWith('node_modules/')&&value&&typeof value==='object'));
|
|
function secure(stat,label,{symlink=false}={}){
|
|
if(stat.uid!==0||stat.gid!==0)throw new Error('owner:'+label);
|
|
if(!symlink&&(stat.mode&0o022)!==0)throw new Error('writable:'+label);
|
|
}
|
|
secure(fs.lstatSync(root),'root');
|
|
function packageVersion(packageRoot,key){
|
|
const manifest=JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'),'utf8'));
|
|
const wanted=expected.get(key);
|
|
if(!wanted||String(manifest.version||'')!==String(wanted.version||''))throw new Error('package-version:'+key);
|
|
}
|
|
function inspectPackageDirectory(nodeModulesRoot,relativePrefix){
|
|
const names=fs.readdirSync(nodeModulesRoot).sort((a,b)=>Buffer.from(a).compare(Buffer.from(b)));
|
|
const roots=[];
|
|
for(const name of names){
|
|
if(name==='.bin'||name==='.package-lock.json')continue;
|
|
const candidate=path.join(nodeModulesRoot,name);
|
|
const stat=fs.lstatSync(candidate);
|
|
if(name.startsWith('@')){
|
|
if(!stat.isDirectory())throw new Error('scope-not-directory');
|
|
for(const child of fs.readdirSync(candidate).sort((a,b)=>Buffer.from(a).compare(Buffer.from(b)))){
|
|
roots.push([path.join(candidate,child),relativePrefix+name+'/'+child]);
|
|
}
|
|
}else{
|
|
roots.push([candidate,relativePrefix+name]);
|
|
}
|
|
}
|
|
for(const [packageRoot,key] of roots){
|
|
const stat=fs.lstatSync(packageRoot);
|
|
if(!stat.isDirectory()||stat.isSymbolicLink())throw new Error('package-boundary:'+key);
|
|
packageVersion(packageRoot,key);
|
|
const nested=path.join(packageRoot,'node_modules');
|
|
if(fs.existsSync(nested))inspectPackageDirectory(nested,key+'/node_modules/');
|
|
}
|
|
}
|
|
inspectPackageDirectory(root,'node_modules/');
|
|
for(const [key,value] of expected){
|
|
const target=path.join('/app',key);
|
|
if(!fs.existsSync(target)&&value.optional!==true)throw new Error('package-missing:'+key);
|
|
if(fs.existsSync(target))packageVersion(target,key);
|
|
}
|
|
function visit(relative){
|
|
const absolute=path.join(root,relative);
|
|
const names=fs.readdirSync(absolute).sort((a,b)=>Buffer.from(a).compare(Buffer.from(b)));
|
|
for(const name of names){
|
|
if(name.includes('\\0')||name==='.'||name==='..')throw new Error('unsafe-name');
|
|
const child=relative?relative+'/'+name:name;
|
|
const target=path.join(root,child);
|
|
const stat=fs.lstatSync(target);
|
|
const mode=(stat.mode&0o7777).toString(8);
|
|
if(stat.isDirectory()){
|
|
secure(stat,child);
|
|
hash.update('d\\0'+child+'\\0'+mode+'\\0');
|
|
visit(child);
|
|
}else if(stat.isFile()){
|
|
secure(stat,child);
|
|
hash.update('f\\0'+child+'\\0'+mode+'\\0'+stat.size+'\\0');
|
|
hash.update(fs.readFileSync(target));
|
|
}else if(stat.isSymbolicLink()){
|
|
secure(stat,child,{symlink:true});
|
|
const link=fs.readlinkSync(target);
|
|
if(path.isAbsolute(link))throw new Error('absolute-link:'+child);
|
|
const resolved=path.resolve(path.dirname(target),link);
|
|
if(resolved!==root&&!resolved.startsWith(root+'/'))throw new Error('escaping-link:'+child);
|
|
const real=fs.realpathSync(target);
|
|
if(real!==root&&!real.startsWith(root+'/'))throw new Error('escaping-real-link:'+child);
|
|
hash.update('l\\0'+child+'\\0'+mode+'\\0'+link+'\\0');
|
|
}else{
|
|
throw new Error('special-file');
|
|
}
|
|
}
|
|
}
|
|
visit('');
|
|
process.stdout.write('sha256:'+hash.digest('hex'));
|
|
""".strip()
|
|
result = subprocess.run(
|
|
[str(DOCKER), "exec", "--workdir", "/app", container_id, "node", "-e", script],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300,
|
|
)
|
|
value = result.stdout.strip()
|
|
if (result.returncode != 0
|
|
or result.stderr.strip()
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", value)):
|
|
die("Engine backend node_modules tree proof failed")
|
|
return value
|
|
|
|
|
|
def validate_runtime_owned_file(path, mode, label, min_bytes=1, max_bytes=1024 * 1024 * 1024):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"{label} is missing")
|
|
if (stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_uid != 0
|
|
or path_stat.st_gid != 0
|
|
or stat.S_IMODE(path_stat.st_mode) != mode
|
|
or path_stat.st_size < min_bytes
|
|
or path_stat.st_size > max_bytes):
|
|
die(f"{label} is unsafe")
|
|
return path_stat
|
|
|
|
|
|
def validate_engine_backend_runtime_override_file():
|
|
validate_runtime_owned_file(
|
|
ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE,
|
|
0o600,
|
|
"Engine backend immutable runtime override",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
try:
|
|
value = ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine backend immutable runtime override is unreadable")
|
|
if value != expected_engine_credential_backend_override():
|
|
die("Engine backend immutable runtime override drift detected")
|
|
return sha256_file(ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE)
|
|
|
|
|
|
def validate_engine_backend_activation_marker():
|
|
validate_runtime_owned_file(
|
|
ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE,
|
|
0o600,
|
|
"Engine backend immutable runtime activation marker",
|
|
max_bytes=128,
|
|
)
|
|
validate_engine_backend_runtime_override_file()
|
|
metadata = read_engine_backend_runtime_metadata()
|
|
validate_runtime_owned_file(
|
|
ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE,
|
|
0o600,
|
|
"Engine backend canonical rootfs",
|
|
max_bytes=MAX_PAYLOAD_BYTES,
|
|
)
|
|
expected = f"sha256:{sha256_file(ENGINE_CREDENTIAL_BACKEND_METADATA_FILE)}\n"
|
|
try:
|
|
actual = ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.read_text(encoding="ascii")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine backend immutable runtime activation marker is unreadable")
|
|
if (actual != expected
|
|
or sha256_file(ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE) != metadata["rootfsSha256"]):
|
|
die("Engine backend immutable runtime activation marker mismatch")
|
|
inspect_engine_backend_derived_image(metadata)
|
|
return metadata
|
|
|
|
|
|
def quarantine_engine_backend_partial_runtime():
|
|
runtime_exists = (
|
|
ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR.exists()
|
|
or ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR.is_symlink()
|
|
)
|
|
if not runtime_exists:
|
|
return "absent"
|
|
ensure_root_runtime_directory(ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR)
|
|
present = [
|
|
path for path in (
|
|
ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE,
|
|
ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE,
|
|
ENGINE_CREDENTIAL_BACKEND_METADATA_FILE,
|
|
ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE,
|
|
)
|
|
if path.exists() or path.is_symlink()
|
|
]
|
|
if not present:
|
|
return "empty"
|
|
failed_root = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "failed"
|
|
ensure_root_runtime_directory(failed_root)
|
|
quarantine = failed_root / f"{stamp()}-{os.getpid()}-{time.time_ns()}"
|
|
quarantine.mkdir(mode=0o700)
|
|
os.chown(quarantine, 0, 0)
|
|
# Move the activation marker first. Once this succeeds, no subsequent
|
|
# Engine Compose command can consume the incomplete override.
|
|
ordered = sorted(
|
|
present,
|
|
key=lambda path: path != ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE,
|
|
)
|
|
for path in ordered:
|
|
os.replace(path, quarantine / path.name)
|
|
fsync_directory(ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR)
|
|
fsync_directory(quarantine)
|
|
return "quarantined"
|
|
|
|
|
|
def read_engine_backend_runtime_metadata():
|
|
validate_runtime_owned_file(
|
|
ENGINE_CREDENTIAL_BACKEND_METADATA_FILE,
|
|
0o600,
|
|
"Engine backend immutable runtime metadata",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
value = read_strict_json(
|
|
ENGINE_CREDENTIAL_BACKEND_METADATA_FILE,
|
|
"Engine backend immutable runtime metadata",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
expected_keys = {
|
|
"schemaVersion", "release", "sourceContainerId", "sourceImageId",
|
|
"derivedImageId", "rootfsSha256", "packageLockSha256",
|
|
"nodeModulesTreeSha256", "overrideSha256", "tools",
|
|
}
|
|
if (not isinstance(value, dict)
|
|
or set(value) != expected_keys
|
|
or value.get("schemaVersion") != "nodedc.engine-backend-immutable-runtime/v1"
|
|
or value.get("release") != "credential-sink-20260716-001"
|
|
or not re.fullmatch(r"[a-f0-9]{12,64}", str(value.get("sourceContainerId")))
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", str(value.get("sourceImageId")))
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", str(value.get("derivedImageId")))
|
|
or not re.fullmatch(r"[a-f0-9]{64}", str(value.get("rootfsSha256")))
|
|
or value.get("packageLockSha256") != ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", str(value.get("nodeModulesTreeSha256")))
|
|
or not re.fullmatch(r"[a-f0-9]{64}", str(value.get("overrideSha256")))
|
|
or not isinstance(value.get("tools"), dict)
|
|
or set(value["tools"]) != {"node", "sqlite", "docker", "compose"}):
|
|
die("Engine backend immutable runtime metadata mismatch")
|
|
return value
|
|
|
|
|
|
def inspect_engine_backend_derived_image(
|
|
expected_metadata=None,
|
|
image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE,
|
|
expected_source_image_id=None,
|
|
expected_rootfs_sha256=None,
|
|
):
|
|
images = docker_json(
|
|
["image", "inspect", image_ref],
|
|
"Engine backend immutable image inspect",
|
|
)
|
|
if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict):
|
|
die("Engine backend immutable image inspect shape mismatch")
|
|
image = images[0]
|
|
image_id = image.get("Id")
|
|
labels = (image.get("Config") or {}).get("Labels") or {}
|
|
if (not isinstance(image_id, str)
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id)
|
|
or image.get("Architecture") != "amd64"
|
|
or image.get("Os") != "linux"
|
|
or not isinstance(labels, dict)
|
|
or labels.get("org.nodedc.release") != "credential-sink-20260716-001"
|
|
or labels.get("org.nodedc.package-lock-sha256") != ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256
|
|
or not re.fullmatch(r"sha256:[a-f0-9]{64}", str(labels.get("org.nodedc.source-image-id")))
|
|
or not re.fullmatch(r"[a-f0-9]{64}", str(labels.get("org.nodedc.rootfs-sha256")))):
|
|
die("Engine backend immutable image identity mismatch")
|
|
if (expected_source_image_id is not None
|
|
and labels["org.nodedc.source-image-id"] != expected_source_image_id):
|
|
die("Engine backend immutable image source collision")
|
|
if (expected_rootfs_sha256 is not None
|
|
and labels["org.nodedc.rootfs-sha256"] != expected_rootfs_sha256):
|
|
die("Engine backend immutable image rootfs collision")
|
|
if expected_metadata is not None and (
|
|
image_id != expected_metadata["derivedImageId"]
|
|
or labels["org.nodedc.source-image-id"] != expected_metadata["sourceImageId"]
|
|
or labels["org.nodedc.rootfs-sha256"] != expected_metadata["rootfsSha256"]
|
|
):
|
|
die("Engine backend immutable image metadata drift detected")
|
|
return image
|
|
|
|
|
|
def ensure_engine_backend_release_image(
|
|
build_context,
|
|
source_image_id,
|
|
rootfs_sha256,
|
|
expected_tools,
|
|
):
|
|
target_id = inspect_optional_local_image(
|
|
ENGINE_CREDENTIAL_BACKEND_IMAGE,
|
|
"Engine backend immutable release image",
|
|
)
|
|
if target_id is not None:
|
|
image = inspect_engine_backend_derived_image(
|
|
image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE,
|
|
expected_source_image_id=source_image_id,
|
|
expected_rootfs_sha256=rootfs_sha256,
|
|
)
|
|
if (image.get("Id") != target_id
|
|
or engine_backend_tool_versions(
|
|
image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE,
|
|
) != expected_tools):
|
|
die("Engine backend immutable release image collision")
|
|
return image
|
|
|
|
build_ref = f"nodedc/deploy-build:engine-backend-{rootfs_sha256[:16]}"
|
|
build_id = inspect_optional_local_image(
|
|
build_ref,
|
|
"Engine backend immutable build image",
|
|
)
|
|
if build_id is None:
|
|
build = subprocess.run(
|
|
[
|
|
str(DOCKER), "build", "--no-cache", "--network", "none", "--pull=false",
|
|
"-f", "Dockerfile", "-t", build_ref, ".",
|
|
],
|
|
cwd=str(build_context),
|
|
check=False,
|
|
timeout=600,
|
|
)
|
|
if build.returncode != 0:
|
|
die("Engine backend immutable image build failed")
|
|
build_id = inspect_optional_local_image(
|
|
build_ref,
|
|
"Engine backend immutable built image",
|
|
)
|
|
if build_id is None:
|
|
die("Engine backend immutable built image is missing")
|
|
|
|
build_image = inspect_engine_backend_derived_image(
|
|
image_ref=build_ref,
|
|
expected_source_image_id=source_image_id,
|
|
expected_rootfs_sha256=rootfs_sha256,
|
|
)
|
|
if (build_image.get("Id") != build_id
|
|
or engine_backend_tool_versions(image_ref=build_ref) != expected_tools):
|
|
die("Engine backend immutable build image collision")
|
|
|
|
# The deploy lock serializes canonical applies. Recheck the release tag
|
|
# immediately before assigning it so an existing versioned identity is
|
|
# never intentionally moved to a different image.
|
|
target_id = inspect_optional_local_image(
|
|
ENGINE_CREDENTIAL_BACKEND_IMAGE,
|
|
"Engine backend immutable release image recheck",
|
|
)
|
|
if target_id is not None:
|
|
image = inspect_engine_backend_derived_image(
|
|
image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE,
|
|
expected_source_image_id=source_image_id,
|
|
expected_rootfs_sha256=rootfs_sha256,
|
|
)
|
|
if image.get("Id") != build_id:
|
|
die("Engine backend immutable release image collision")
|
|
return image
|
|
tag = subprocess.run(
|
|
[str(DOCKER), "image", "tag", build_id, ENGINE_CREDENTIAL_BACKEND_IMAGE],
|
|
check=False,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
if tag.returncode != 0:
|
|
die("Engine backend immutable release image tag failed")
|
|
image = inspect_engine_backend_derived_image(
|
|
image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE,
|
|
expected_source_image_id=source_image_id,
|
|
expected_rootfs_sha256=rootfs_sha256,
|
|
)
|
|
if (image.get("Id") != build_id
|
|
or engine_backend_tool_versions(
|
|
image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE,
|
|
) != expected_tools):
|
|
die("Engine backend immutable release image verification failed")
|
|
return image
|
|
|
|
|
|
def preflight_engine_credential_backend_runtime(
|
|
expected_node_intelligence_gateway_sha256=None,
|
|
):
|
|
base_image_id = inspect_local_image(
|
|
ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE,
|
|
"Engine backend base image",
|
|
)
|
|
package_lock = component_root("engine") / "nodedc-source/package-lock.json"
|
|
try:
|
|
lock_stat = package_lock.lstat()
|
|
except FileNotFoundError:
|
|
die("Engine backend package-lock.json is missing")
|
|
if (stat.S_ISLNK(lock_stat.st_mode)
|
|
or not stat.S_ISREG(lock_stat.st_mode)
|
|
or sha256_file(package_lock) != ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256):
|
|
die("Engine backend package-lock barrier mismatch")
|
|
try:
|
|
node_modules_stat = ENGINE_CREDENTIAL_BACKEND_NODE_MODULES_DIR.lstat()
|
|
except FileNotFoundError:
|
|
die("Engine backend node_modules host source is missing")
|
|
if (stat.S_ISLNK(node_modules_stat.st_mode)
|
|
or not stat.S_ISDIR(node_modules_stat.st_mode)
|
|
or node_modules_stat.st_uid != 0
|
|
or node_modules_stat.st_gid != 0
|
|
or node_modules_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)):
|
|
die("Engine backend node_modules host source is unsafe")
|
|
|
|
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")
|
|
if not isinstance(containers, list) or len(containers) != 1 or not isinstance(containers[0], dict):
|
|
die("Engine backend container inspect shape mismatch")
|
|
container = containers[0]
|
|
state = container.get("State") or {}
|
|
started_at = state.get("StartedAt")
|
|
initial_restart_count = int(container.get("RestartCount") or 0)
|
|
if (state.get("Status") != "running"
|
|
or (state.get("Health") or {}).get("Status") != "healthy"
|
|
or not isinstance(started_at, str)
|
|
or not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:.]+Z", started_at)):
|
|
die("Engine backend source container is not stable and healthy")
|
|
config_image = (container.get("Config") or {}).get("Image")
|
|
current_image_id = container.get("Image")
|
|
validate_engine_backend_node_intelligence_mounts_for_installed_runtime(
|
|
container,
|
|
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:
|
|
runtime_presence = {
|
|
"activation": ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.exists()
|
|
or ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.is_symlink(),
|
|
"override": ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE.exists()
|
|
or ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE.is_symlink(),
|
|
"metadata": ENGINE_CREDENTIAL_BACKEND_METADATA_FILE.exists()
|
|
or ENGINE_CREDENTIAL_BACKEND_METADATA_FILE.is_symlink(),
|
|
"rootfs": ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE.exists()
|
|
or ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE.is_symlink(),
|
|
}
|
|
if all(runtime_presence.values()):
|
|
validate_engine_backend_activation_marker()
|
|
override_sha256 = validate_engine_backend_runtime_override_file()
|
|
metadata = read_engine_backend_runtime_metadata()
|
|
rootfs_stat = validate_runtime_owned_file(
|
|
ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE,
|
|
0o600,
|
|
"Engine backend canonical rootfs",
|
|
max_bytes=MAX_PAYLOAD_BYTES,
|
|
)
|
|
image = inspect_engine_backend_derived_image(metadata)
|
|
if (rootfs_stat.st_size <= 0
|
|
or sha256_file(ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE) != metadata["rootfsSha256"]
|
|
or override_sha256 != metadata["overrideSha256"]
|
|
or metadata["sourceImageId"] != base_image_id
|
|
or metadata["derivedImageId"] != image.get("Id")):
|
|
die("Engine backend prepared immutable runtime drift detected")
|
|
mode = "verified-prepared-not-active"
|
|
elif any(runtime_presence.values()):
|
|
for name, path in (
|
|
("activation", ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE),
|
|
("override", ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE),
|
|
("metadata", ENGINE_CREDENTIAL_BACKEND_METADATA_FILE),
|
|
("rootfs", ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE),
|
|
):
|
|
if runtime_presence[name]:
|
|
validate_runtime_owned_file(
|
|
path,
|
|
0o600,
|
|
f"Engine backend partial runtime {name}",
|
|
max_bytes=MAX_PAYLOAD_BYTES,
|
|
)
|
|
mode = "recoverable-prepared-partial"
|
|
else:
|
|
mode = "verified-running-base"
|
|
source_image_id = base_image_id
|
|
elif config_image == ENGINE_CREDENTIAL_BACKEND_IMAGE:
|
|
override_sha256 = validate_engine_backend_runtime_override_file()
|
|
metadata = read_engine_backend_runtime_metadata()
|
|
image = inspect_engine_backend_derived_image(metadata)
|
|
if current_image_id != image.get("Id") or override_sha256 != metadata["overrideSha256"]:
|
|
die("Engine backend immutable runtime activation drift detected")
|
|
mode = "verified-derived-retry"
|
|
source_image_id = metadata["sourceImageId"]
|
|
else:
|
|
die("Engine backend current image barrier mismatch")
|
|
tools = engine_backend_tool_versions(container_id=container_id)
|
|
validate_engine_backend_dependencies(container_id)
|
|
node_modules_tree_sha256 = engine_backend_node_modules_tree_sha256(container_id)
|
|
if mode in ("verified-derived-retry", "verified-prepared-not-active"):
|
|
if (node_modules_tree_sha256 != metadata["nodeModulesTreeSha256"]
|
|
or tools != metadata["tools"]):
|
|
die("Engine backend prepared runtime source proof drift detected")
|
|
stable = docker_json(["container", "inspect", container_id], "Engine backend stability inspect")
|
|
stable_container = stable[0] if isinstance(stable, list) and len(stable) == 1 else None
|
|
stable_state = stable_container.get("State") if isinstance(stable_container, dict) else None
|
|
if (not isinstance(stable_state, dict)
|
|
or stable_state.get("Status") != "running"
|
|
or (stable_state.get("Health") or {}).get("Status") != "healthy"
|
|
or stable_state.get("StartedAt") != started_at
|
|
or int(stable_container.get("RestartCount") or 0) != initial_restart_count):
|
|
die("Engine backend restarted during immutable runtime preflight")
|
|
return {
|
|
"base_image_id": base_image_id,
|
|
"container_id": container_id,
|
|
"current_image_id": current_image_id,
|
|
"mode": mode,
|
|
"source_image_id": source_image_id,
|
|
"node_modules_tree_sha256": node_modules_tree_sha256,
|
|
"tools": tools,
|
|
}
|
|
|
|
|
|
def normalize_rootfs_member_name(name):
|
|
if not isinstance(name, str) or not name or name.startswith("/") or "\\" in name:
|
|
die("Engine backend export member path is unsafe")
|
|
while name.startswith("./"):
|
|
name = name[2:]
|
|
parts = name.split("/")
|
|
if not name or any(not part or part in (".", "..") for part in parts):
|
|
die("Engine backend export member path is unsafe")
|
|
if any(part.startswith("._") or part == "__MACOSX" for part in parts):
|
|
die("Engine backend export contains AppleDouble metadata")
|
|
return "/".join(parts)
|
|
|
|
|
|
def engine_backend_rootfs_member_allowed(name, is_directory):
|
|
excluded = (
|
|
"etc/hosts", "etc/hostname", "etc/resolv.conf", "etc/mtab",
|
|
"etc/environment", "etc/profile", "etc/profile.d", "etc/ssl/private",
|
|
"etc/ssh/ssh_host", "etc/machine-id", "etc/docker", "etc/containerd",
|
|
"usr/local/etc", "usr/local/lib/node_modules",
|
|
)
|
|
if any(name == value or name.startswith(f"{value}/") for value in excluded):
|
|
return False
|
|
basename = name.rsplit("/", 1)[-1].lower()
|
|
if (basename in (".env", "id_rsa", "id_ed25519")
|
|
or basename.startswith(".env.")
|
|
or basename.endswith((".p12", ".pfx"))):
|
|
die(f"Engine backend export secret-like path rejected: {name}")
|
|
if any(name == root or name.startswith(f"{root}/") for root in ("bin", "sbin", "lib", "usr", "etc")):
|
|
return True
|
|
if name in ("var", "var/lib"):
|
|
return is_directory
|
|
return name == "var/lib/apk" or name.startswith("var/lib/apk/")
|
|
|
|
|
|
def resolve_rootfs_link(name, linkname):
|
|
if not isinstance(linkname, str) or not linkname or "\\" in linkname:
|
|
die(f"Engine backend export link target is unsafe: {name}")
|
|
parts = [] if linkname.startswith("/") else name.split("/")[:-1]
|
|
for part in linkname.lstrip("/").split("/"):
|
|
if part in ("", "."):
|
|
continue
|
|
if part == "..":
|
|
if not parts:
|
|
die(f"Engine backend export link escapes root: {name}")
|
|
parts.pop()
|
|
else:
|
|
parts.append(part)
|
|
target = "/".join(parts)
|
|
if not target or not engine_backend_rootfs_member_allowed(target, False):
|
|
die(f"Engine backend export link leaves safe roots: {name}")
|
|
return linkname
|
|
|
|
|
|
ENGINE_BACKEND_ROOTFS_OMITTED_TOOLCHAIN_LINKS = {
|
|
"usr/local/bin/corepack": re.compile(r"^\.\./lib/node_modules/corepack/dist/corepack\.js$"),
|
|
"usr/local/bin/npm": re.compile(r"^\.\./lib/node_modules/npm/bin/npm-cli\.js$"),
|
|
"usr/local/bin/npx": re.compile(r"^\.\./lib/node_modules/npm/bin/npx-cli\.js$"),
|
|
"usr/local/bin/yarn": re.compile(r"^/opt/yarn-v[0-9]+\.[0-9]+\.[0-9]+/bin/yarn$"),
|
|
"usr/local/bin/yarnpkg": re.compile(r"^/opt/yarn-v[0-9]+\.[0-9]+\.[0-9]+/bin/yarnpkg$"),
|
|
}
|
|
|
|
|
|
def engine_backend_rootfs_toolchain_link_is_omitted(name, linkname):
|
|
expected = ENGINE_BACKEND_ROOTFS_OMITTED_TOOLCHAIN_LINKS.get(name)
|
|
if expected is None:
|
|
return False
|
|
if not isinstance(linkname, str) or not expected.fullmatch(linkname):
|
|
die(f"Engine backend export omitted toolchain link target mismatch: {name}")
|
|
return True
|
|
|
|
|
|
ENGINE_BACKEND_ROOTFS_SECRET_PATTERNS = (
|
|
re.compile(br"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----"),
|
|
re.compile(br"ndc_(?:edpwb|edprb|fndbg|edppr)_[A-Za-z0-9_-]{8,}"),
|
|
re.compile(br"AKIA[0-9A-Z]{16}"),
|
|
re.compile(br"gh[pousr]_[A-Za-z0-9]{20,}"),
|
|
re.compile(br"xox[baprs]-[A-Za-z0-9-]{20,}"),
|
|
)
|
|
|
|
|
|
def spool_and_scan_rootfs_file(source, name, expected_size):
|
|
if expected_size < 0 or expected_size > MAX_FILE_BYTES:
|
|
die(f"Engine backend export member size rejected: {name}")
|
|
temporary = tempfile.SpooledTemporaryFile(max_size=8 * 1024 * 1024)
|
|
total = 0
|
|
carry = b""
|
|
while True:
|
|
chunk = source.read(1024 * 1024)
|
|
if not chunk:
|
|
break
|
|
total += len(chunk)
|
|
if total > MAX_FILE_BYTES:
|
|
temporary.close()
|
|
die(f"Engine backend export member too large: {name}")
|
|
sample = carry + chunk
|
|
if any(pattern.search(sample) for pattern in ENGINE_BACKEND_ROOTFS_SECRET_PATTERNS):
|
|
temporary.close()
|
|
die(f"Engine backend export secret material rejected: {name}")
|
|
carry = sample[-256:]
|
|
temporary.write(chunk)
|
|
if total != expected_size:
|
|
temporary.close()
|
|
die(f"Engine backend export member size mismatch: {name}")
|
|
temporary.seek(0)
|
|
return temporary
|
|
|
|
|
|
def build_canonical_engine_backend_rootfs(source_tar, destination_tar):
|
|
total_bytes = 0
|
|
with tarfile.open(source_tar, "r:*") as source:
|
|
members = []
|
|
seen = set()
|
|
source_members = {}
|
|
for member in source.getmembers():
|
|
name = normalize_rootfs_member_name(member.name)
|
|
if name in seen:
|
|
die(f"Engine backend export duplicate member: {name}")
|
|
seen.add(name)
|
|
source_members[name] = member
|
|
if not engine_backend_rootfs_member_allowed(name, member.isdir()):
|
|
continue
|
|
if not (member.isdir() or member.isfile() or member.issym() or member.islnk()):
|
|
die(f"Engine backend export special member rejected: {name}")
|
|
if (member.issym()
|
|
and engine_backend_rootfs_toolchain_link_is_omitted(name, member.linkname)):
|
|
continue
|
|
members.append((name, member))
|
|
members.sort(key=lambda item: item[0])
|
|
|
|
with destination_tar.open("xb") as raw_output:
|
|
with tarfile.open(fileobj=raw_output, mode="w", format=tarfile.GNU_FORMAT) as output:
|
|
for name, member in members:
|
|
info = tarfile.TarInfo(name)
|
|
info.uid = member.uid
|
|
info.gid = member.gid
|
|
info.uname = ""
|
|
info.gname = ""
|
|
info.mode = member.mode & 0o7777
|
|
info.mtime = 0
|
|
if member.isdir():
|
|
info.type = tarfile.DIRTYPE
|
|
info.size = 0
|
|
output.addfile(info)
|
|
continue
|
|
if member.issym():
|
|
info.type = tarfile.SYMTYPE
|
|
info.linkname = resolve_rootfs_link(name, member.linkname)
|
|
info.size = 0
|
|
output.addfile(info)
|
|
continue
|
|
if member.islnk():
|
|
target_name = normalize_rootfs_member_name(member.linkname.lstrip("/"))
|
|
target = source_members.get(target_name)
|
|
if (target is None
|
|
or not target.isfile()
|
|
or not engine_backend_rootfs_member_allowed(target_name, False)):
|
|
die(f"Engine backend export hardlink target mismatch: {name}")
|
|
expected_size = target.size
|
|
source_member = target
|
|
else:
|
|
expected_size = member.size
|
|
source_member = member
|
|
source_file = source.extractfile(source_member)
|
|
if source_file is None:
|
|
die(f"Engine backend export member is unreadable: {name}")
|
|
with source_file:
|
|
staged = spool_and_scan_rootfs_file(source_file, name, expected_size)
|
|
with staged:
|
|
info.type = tarfile.REGTYPE
|
|
info.size = expected_size
|
|
output.addfile(info, staged)
|
|
total_bytes += expected_size
|
|
if total_bytes > MAX_PAYLOAD_BYTES:
|
|
die("Engine backend canonical rootfs exceeds size limit")
|
|
destination_tar.chmod(0o600)
|
|
return sha256_file(destination_tar)
|
|
|
|
|
|
def atomic_write_root_file(path, data, mode=0o600):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
directory_stat = path.parent.lstat()
|
|
if (stat.S_ISLNK(directory_stat.st_mode)
|
|
or not stat.S_ISDIR(directory_stat.st_mode)
|
|
or directory_stat.st_uid != 0
|
|
or directory_stat.st_gid != 0
|
|
or stat.S_IMODE(directory_stat.st_mode) != 0o700):
|
|
die(f"runtime directory is unsafe: {path.parent}")
|
|
temporary = path.with_name(f".{path.name}.{os.getpid()}.{time.time_ns()}.tmp")
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode)
|
|
os.write(descriptor, data)
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, 0, 0)
|
|
os.fchmod(descriptor, mode)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary, path)
|
|
fsync_directory(path.parent)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
|
|
|
|
def ensure_root_runtime_directory(path):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
path.mkdir(parents=True, exist_ok=False)
|
|
path_stat = path.lstat()
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISDIR(path_stat.st_mode):
|
|
die(f"runtime directory is unsafe: {path}")
|
|
os.chown(path, 0, 0)
|
|
path.chmod(0o700)
|
|
|
|
|
|
def prepare_engine_credential_backend_runtime():
|
|
preflight = preflight_engine_credential_backend_runtime()
|
|
installed_template = component_root("engine") / ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL
|
|
try:
|
|
template_stat = installed_template.lstat()
|
|
template = installed_template.read_text(encoding="utf-8")
|
|
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
die("installed Engine backend immutable runtime template is unreadable")
|
|
if (stat.S_ISLNK(template_stat.st_mode)
|
|
or not stat.S_ISREG(template_stat.st_mode)
|
|
or template != expected_engine_credential_backend_override()):
|
|
die("installed Engine backend immutable runtime template mismatch")
|
|
|
|
ensure_root_runtime_directory(ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR)
|
|
if preflight["mode"] in ("verified-derived-retry", "verified-prepared-not-active"):
|
|
validate_engine_backend_activation_marker()
|
|
validate_engine_backend_runtime_override_file()
|
|
metadata = read_engine_backend_runtime_metadata()
|
|
image = inspect_engine_backend_derived_image(metadata)
|
|
if engine_backend_tool_versions(image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE) != metadata["tools"]:
|
|
die("Engine backend immutable runtime tool drift detected")
|
|
return metadata
|
|
if preflight["mode"] == "recoverable-prepared-partial":
|
|
quarantine_engine_backend_partial_runtime()
|
|
return prepare_engine_credential_backend_runtime()
|
|
|
|
with tempfile.TemporaryDirectory(prefix="engine-backend-runtime-", dir=TMP_DIR) as temporary_name:
|
|
temporary = Path(temporary_name)
|
|
raw_export = temporary / "container-export.tar"
|
|
canonical_rootfs = temporary / "canonical-rootfs.tar"
|
|
export = subprocess.run(
|
|
[str(DOCKER), "container", "export", "--output", str(raw_export), preflight["container_id"]],
|
|
check=False,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=300,
|
|
)
|
|
if export.returncode != 0:
|
|
die("Engine backend container export failed")
|
|
rootfs_sha256 = build_canonical_engine_backend_rootfs(raw_export, canonical_rootfs)
|
|
raw_export.unlink()
|
|
dockerfile = "\n".join((
|
|
"FROM scratch",
|
|
"ADD canonical-rootfs.tar /",
|
|
"LABEL org.nodedc.release=\"credential-sink-20260716-001\"",
|
|
f"LABEL org.nodedc.source-image-id=\"{preflight['source_image_id']}\"",
|
|
f"LABEL org.nodedc.rootfs-sha256=\"{rootfs_sha256}\"",
|
|
f"LABEL org.nodedc.package-lock-sha256=\"{ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256}\"",
|
|
"WORKDIR /app",
|
|
"CMD [\"/usr/local/bin/node\",\"server/index.js\"]",
|
|
"",
|
|
))
|
|
(temporary / "Dockerfile").write_text(dockerfile, encoding="utf-8")
|
|
(temporary / "Dockerfile").chmod(0o600)
|
|
image = ensure_engine_backend_release_image(
|
|
temporary,
|
|
preflight["source_image_id"],
|
|
rootfs_sha256,
|
|
preflight["tools"],
|
|
)
|
|
tools = preflight["tools"]
|
|
|
|
os.chown(canonical_rootfs, 0, 0)
|
|
canonical_rootfs.chmod(0o600)
|
|
os.replace(canonical_rootfs, ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE)
|
|
fsync_directory(ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR)
|
|
|
|
atomic_write_root_file(
|
|
ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE,
|
|
template.encode("utf-8"),
|
|
)
|
|
override_sha256 = validate_engine_backend_runtime_override_file()
|
|
metadata = {
|
|
"schemaVersion": "nodedc.engine-backend-immutable-runtime/v1",
|
|
"release": "credential-sink-20260716-001",
|
|
"sourceContainerId": preflight["container_id"],
|
|
"sourceImageId": preflight["source_image_id"],
|
|
"derivedImageId": image["Id"],
|
|
"rootfsSha256": rootfs_sha256,
|
|
"packageLockSha256": ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256,
|
|
"nodeModulesTreeSha256": preflight["node_modules_tree_sha256"],
|
|
"overrideSha256": override_sha256,
|
|
"tools": tools,
|
|
}
|
|
atomic_write_root_file(
|
|
ENGINE_CREDENTIAL_BACKEND_METADATA_FILE,
|
|
(json.dumps(metadata, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8"),
|
|
)
|
|
read_engine_backend_runtime_metadata()
|
|
atomic_write_root_file(
|
|
ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE,
|
|
f"sha256:{sha256_file(ENGINE_CREDENTIAL_BACKEND_METADATA_FILE)}\n".encode("ascii"),
|
|
)
|
|
validate_engine_backend_activation_marker()
|
|
return metadata
|
|
|
|
|
|
def preflight_engine_credential_sink_ready():
|
|
url = "http://127.0.0.1:3001/internal/engine-credential-sink/v1/health"
|
|
try:
|
|
request = urllib.request.Request(url, headers={"Cache-Control": "no-store"})
|
|
with NO_REDIRECT_OPENER.open(request, timeout=10) as response:
|
|
if response.status != 200:
|
|
die(f"Engine credential sink preflight failed: HTTP {response.status}")
|
|
body = response.read(16 * 1024)
|
|
except urllib.error.HTTPError as exc:
|
|
die(f"Engine credential sink preflight failed: HTTP {exc.code}")
|
|
except Exception as exc:
|
|
die(f"Engine credential sink preflight failed: {type(exc).__name__}")
|
|
try:
|
|
value = json.loads(body)
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
die("Engine credential sink preflight returned invalid JSON")
|
|
if (value.get("ok") is not True
|
|
or value.get("ready") is not True
|
|
or value.get("service") != "NDC Engine Credential Sink"
|
|
or value.get("issuer") != {
|
|
"serviceId": "platform.engine-credential-provisioner",
|
|
"keyId": ENGINE_CREDENTIAL_PROVISIONER_KEY_ID,
|
|
}):
|
|
die("Engine credential sink preflight identity/readiness mismatch")
|
|
|
|
|
|
def engine_n8n_container_ids():
|
|
result = subprocess.run(
|
|
[*compose_base_cmd("engine"), "ps", "-q", "n8n"],
|
|
cwd=str(component_compose_root("engine")),
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
die("Engine n8n container lookup failed")
|
|
container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
if len(container_ids) != 1:
|
|
die(f"Engine n8n topology mismatch: expected=1 actual={len(container_ids)}")
|
|
return container_ids
|
|
|
|
|
|
def inspect_engine_n8n_base_image():
|
|
images = docker_json(["image", "inspect", ENGINE_N8N_BASE_IMAGE], "Engine n8n base image inspect")
|
|
if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict):
|
|
die("Engine n8n base image inspect shape mismatch")
|
|
image = images[0]
|
|
image_id = image.get("Id")
|
|
if not isinstance(image_id, str) or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id):
|
|
die("Engine n8n base image id is invalid")
|
|
if image.get("Architecture") != ENGINE_N8N_BASE_ARCHITECTURE or image.get("Os") != "linux":
|
|
die("Engine n8n base image platform mismatch")
|
|
return {
|
|
"id": image_id,
|
|
"architecture": image.get("Architecture"),
|
|
"repo_digests": tuple(image.get("RepoDigests") or ()),
|
|
}
|
|
|
|
|
|
def inspect_container(container_id):
|
|
containers = docker_json(["inspect", container_id], "Engine n8n container inspect")
|
|
if not isinstance(containers, list) or len(containers) != 1 or not isinstance(containers[0], dict):
|
|
die("Engine n8n container inspect shape mismatch")
|
|
return containers[0]
|
|
|
|
|
|
def engine_n8n_version(container_id):
|
|
result = subprocess.run(
|
|
[str(DOCKER), "exec", container_id, "n8n", "--version"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
version = result.stdout.strip()
|
|
if result.returncode != 0 or version != ENGINE_N8N_VERSION:
|
|
die("Engine n8n runtime version mismatch")
|
|
return version
|
|
|
|
|
|
def engine_n8n_package_loader_probe_script():
|
|
# LoadNodesAndCredentials.init() in n8n 2.3.2 initializes NODE_PATH before
|
|
# it loads community packages. A standalone `node -e` probe must reproduce
|
|
# that image-owned module-resolution step or peer imports such as
|
|
# n8n-workflow fail even though the real n8n loader succeeds.
|
|
return (
|
|
"process.env.NODE_PATH='"
|
|
+ ENGINE_N8N_NODE_MODULES_PATH
|
|
+ "';require('module').Module._initPaths();"
|
|
"const {PackageDirectoryLoader}=require('"
|
|
+ ENGINE_N8N_NODE_MODULES_PATH
|
|
+ "/n8n-core');"
|
|
"(async()=>{const loader=new PackageDirectoryLoader('"
|
|
+ ENGINE_N8N_RUNTIME_PACKAGE_PATH
|
|
+ "');await loader.loadAll();process.stdout.write(JSON.stringify({packageName:loader.packageName,"
|
|
"nodes:loader.types.nodes.map(x=>x.name),credentials:loader.types.credentials.map(x=>x.name)}))})()"
|
|
".catch(error=>{process.stderr.write(String(error&&error.code||'PROBE_ERROR'));process.exit(1)});"
|
|
)
|
|
|
|
|
|
def engine_n8n_private_loader_catalog(container_id):
|
|
container = inspect_container(container_id)
|
|
package_mounts = [
|
|
mount for mount in (container.get("Mounts") or ())
|
|
if mount.get("Destination") == ENGINE_N8N_RUNTIME_PACKAGE_PATH
|
|
]
|
|
if not package_mounts:
|
|
return {"node_types": [], "credential_types": []}
|
|
if len(package_mounts) != 1 or package_mounts[0].get("RW") is not False:
|
|
die("Engine n8n package-loader probe found an unsafe package mount")
|
|
script = engine_n8n_package_loader_probe_script()
|
|
result = subprocess.run(
|
|
[str(DOCKER), "exec", container_id, "node", "-e", script],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
)
|
|
if result.returncode != 0:
|
|
raw_category = (result.stderr or "").strip().upper()
|
|
category = raw_category if re.fullmatch(r"[A-Z0-9_]{1,64}", raw_category) else "PROBE_ERROR"
|
|
die(f"Engine n8n package-loader probe failed: category={category}")
|
|
try:
|
|
catalog = json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
die("Engine n8n package-loader probe returned invalid JSON")
|
|
require_exact_json_keys(catalog, ("packageName", "nodes", "credentials"), "Engine n8n package-loader probe")
|
|
if catalog.get("packageName") != "n8n-nodes-ndc":
|
|
die("Engine n8n package-loader probe package identity mismatch")
|
|
nodes = catalog.get("nodes")
|
|
credentials = catalog.get("credentials")
|
|
if not isinstance(nodes, list) or not all(isinstance(value, str) for value in nodes):
|
|
die("Engine n8n package-loader probe node set is invalid")
|
|
if not isinstance(credentials, list) or not all(isinstance(value, str) for value in credentials):
|
|
die("Engine n8n package-loader probe credential set is invalid")
|
|
return {
|
|
"node_types": [f"n8n-nodes-ndc.{value}" for value in nodes],
|
|
"credential_types": credentials,
|
|
}
|
|
|
|
|
|
def engine_n8n_current_state():
|
|
descriptor = current_engine_n8n_transition_descriptor()
|
|
if descriptor and descriptor["action"] == "activate":
|
|
return descriptor["releaseId"], descriptor
|
|
return "verified_inactive", descriptor
|
|
|
|
|
|
def validate_engine_n8n_base_compose_source():
|
|
compose_path = component_root("engine") / "docker-compose.yml"
|
|
try:
|
|
compose = compose_path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
die("Engine base Compose cannot be read")
|
|
services = re.findall(r"(?m)^ ([A-Za-z0-9_-]+):\s*$", compose)
|
|
if tuple(services) != ENGINE_BASE_COMPOSE_SERVICES:
|
|
die("Engine base Compose exact service topology mismatch")
|
|
match = re.search(r"(?ms)^ n8n:\s*\n(?P<body>.*?)(?=^ [A-Za-z0-9_-]+:\s*$)", compose)
|
|
if not match:
|
|
die("Engine base Compose n8n service cannot be isolated")
|
|
n8n_service = match.group("body")
|
|
expected_image = "image: docker.n8n.io/n8nio/n8n:${N8N_IMAGE_TAG:-2.3.2}"
|
|
if expected_image not in n8n_service:
|
|
die("Engine base Compose n8n version mismatch")
|
|
required_fragments = (
|
|
"./n8n-data:/home/node/.n8n",
|
|
"./nodedc-source/services/n8n/runtime-plugin:/opt/nodedc/runtime-plugin:ro",
|
|
"n8n-postgres:",
|
|
)
|
|
if any(fragment not in n8n_service for fragment in required_fragments):
|
|
die("Engine base Compose n8n persistence/runtime dependency mismatch")
|
|
forbidden_fragments = (
|
|
"build:",
|
|
"N8N_CUSTOM_EXTENSIONS",
|
|
"CUSTOM.",
|
|
ENGINE_N8N_RUNTIME_PACKAGE_PATH,
|
|
"N8N_USER_FOLDER",
|
|
"N8N_COMMUNITY_PACKAGES_ENABLED",
|
|
"N8N_COMMUNITY_PACKAGES_PREVENT_LOADING",
|
|
"N8N_REINSTALL_MISSING_PACKAGES",
|
|
)
|
|
if any(fragment in n8n_service for fragment in forbidden_fragments):
|
|
die("Engine base Compose contains a non-baseline n8n activation setting")
|
|
|
|
|
|
def preflight_engine_n8n_transition(descriptor, enforce_expected_current):
|
|
validate_engine_n8n_base_compose_source()
|
|
if descriptor["action"] == "activate":
|
|
validate_engine_n8n_staged_release(descriptor)
|
|
base_image = inspect_engine_n8n_base_image()
|
|
container_id = engine_n8n_container_ids()[0]
|
|
container = inspect_container(container_id)
|
|
state = container.get("State") or {}
|
|
if state.get("Status") != "running":
|
|
die("Engine n8n baseline container is not running")
|
|
if container.get("Image") != base_image["id"]:
|
|
die("Engine n8n running image does not match the local 2.3.2 base image")
|
|
version = engine_n8n_version(container_id)
|
|
current_state, current_descriptor = engine_n8n_current_state()
|
|
state_matches = current_state == descriptor["expectedCurrent"]
|
|
current_catalog = engine_n8n_private_loader_catalog(container_id)
|
|
current_types = current_catalog["node_types"]
|
|
current_credentials = current_catalog["credential_types"]
|
|
expected_current_types = list(ENGINE_N8N_NODE_TYPES) if current_state != "verified_inactive" else []
|
|
expected_current_credentials = (
|
|
list(engine_n8n_credential_types_for_release(current_state))
|
|
if current_state != "verified_inactive"
|
|
else []
|
|
)
|
|
if current_types != expected_current_types:
|
|
die("Engine n8n current live private-node set does not match its transition state")
|
|
if current_credentials != expected_current_credentials:
|
|
die("Engine n8n current live private-credential set does not match its transition state")
|
|
catalog_descriptor = (
|
|
current_descriptor
|
|
if current_state != "verified_inactive" and current_descriptor is not None
|
|
else inactive_engine_n8n_descriptor(descriptor)
|
|
)
|
|
validate_engine_n8n_catalog_payload(component_root("engine"), catalog_descriptor)
|
|
if enforce_expected_current and not state_matches:
|
|
die(
|
|
"Engine n8n transition stale current state: "
|
|
f"expected={descriptor['expectedCurrent']} actual={current_state}"
|
|
)
|
|
return {
|
|
"base_image_id": base_image["id"],
|
|
"base_image_architecture": base_image["architecture"],
|
|
"base_image_repo_digests": base_image["repo_digests"],
|
|
"container_id": container_id,
|
|
"current_state": current_state,
|
|
"current_types": current_types,
|
|
"current_credentials": current_credentials,
|
|
"n8n_version": version,
|
|
"state_matches": state_matches,
|
|
}
|
|
|
|
|
|
def add_engine_n8n_sealed_member_paths(expected_paths, member_name):
|
|
parts = PurePosixPath(member_name).parts
|
|
if not parts or parts[0] != "package":
|
|
die("Engine sealed n8n package member root mismatch")
|
|
for depth in range(1, len(parts) + 1):
|
|
expected_paths.add(PurePosixPath(*parts[:depth]).as_posix())
|
|
|
|
|
|
def validate_engine_n8n_sealed_release(release_dir, staged_release_dir, descriptor):
|
|
if release_dir.is_symlink() or not release_dir.is_dir():
|
|
die("Engine sealed n8n release is missing or unsafe")
|
|
for filename in ("package.tgz", "release.json", "rollback.json"):
|
|
installed = release_dir / filename
|
|
staged = staged_release_dir / filename
|
|
if installed.is_symlink() or not installed.is_file():
|
|
die(f"Engine sealed n8n release member is unsafe: {filename}")
|
|
if sha256_file(installed) != sha256_file(staged):
|
|
die(f"Engine sealed n8n release member mismatch: {filename}")
|
|
package_root = release_dir / "package"
|
|
if package_root.is_symlink() or not package_root.is_dir():
|
|
die("Engine sealed n8n package tree is missing or unsafe")
|
|
expected_paths = {"package", "package.tgz", "release.json", "rollback.json"}
|
|
with tarfile.open(staged_release_dir / "package.tgz", "r:gz") as archive:
|
|
for member in archive:
|
|
# npm package tarballs do not have to carry explicit directory
|
|
# members. Extraction still creates those parent directories, so
|
|
# they are part of the exact sealed tree rather than unexpected
|
|
# content.
|
|
add_engine_n8n_sealed_member_paths(expected_paths, member.name)
|
|
target = release_dir.joinpath(*PurePosixPath(member.name).parts)
|
|
target_stat = target.lstat()
|
|
if stat.S_ISLNK(target_stat.st_mode):
|
|
die(f"Engine sealed n8n package symlink rejected: {member.name}")
|
|
if member.isdir():
|
|
if not stat.S_ISDIR(target_stat.st_mode):
|
|
die(f"Engine sealed n8n package directory mismatch: {member.name}")
|
|
else:
|
|
if not stat.S_ISREG(target_stat.st_mode) or target_stat.st_size != member.size:
|
|
die(f"Engine sealed n8n package file mismatch: {member.name}")
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die(f"Engine sealed n8n package file unreadable: {member.name}")
|
|
import hashlib
|
|
expected_sha = hashlib.sha256(source.read()).hexdigest()
|
|
if sha256_file(target) != expected_sha:
|
|
die(f"Engine sealed n8n package content mismatch: {member.name}")
|
|
actual_paths = {
|
|
path.relative_to(release_dir).as_posix()
|
|
for path in release_dir.rglob("*")
|
|
}
|
|
if actual_paths != expected_paths:
|
|
die("Engine sealed n8n release tree exact member set mismatch")
|
|
for path in [release_dir, *release_dir.rglob("*")]:
|
|
path_stat = path.lstat()
|
|
if path_stat.st_uid != 0 or path_stat.st_gid != 0:
|
|
die(f"Engine sealed n8n release ownership mismatch: {path}")
|
|
expected_mode = 0o555 if stat.S_ISDIR(path_stat.st_mode) else 0o444
|
|
if stat.S_IMODE(path_stat.st_mode) != expected_mode:
|
|
die(f"Engine sealed n8n release mode mismatch: {path}")
|
|
package_json = read_strict_json(package_root / "package.json", "Engine sealed n8n package.json")
|
|
if package_json.get("name") != "n8n-nodes-ndc" or package_json.get("version") != descriptor["packageVersion"]:
|
|
die("Engine sealed n8n package identity mismatch")
|
|
|
|
|
|
def ensure_engine_n8n_sealed_release(descriptor, current_stamp):
|
|
staged_release_dir = validate_engine_n8n_staged_release(descriptor)
|
|
release_parent = ENGINE_N8N_SEALED_RELEASES_ROOT / "releases" / "n8n-nodes-ndc"
|
|
release_parent.mkdir(parents=True, exist_ok=True)
|
|
for path in (
|
|
ENGINE_N8N_SEALED_RELEASES_ROOT,
|
|
ENGINE_N8N_SEALED_RELEASES_ROOT / "releases",
|
|
release_parent,
|
|
):
|
|
path_stat = path.lstat()
|
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISDIR(path_stat.st_mode):
|
|
die(f"Engine sealed n8n release parent is unsafe: {path}")
|
|
os.chown(path, 0, 0)
|
|
path.chmod(0o755)
|
|
release_dir = release_parent / descriptor["releaseId"]
|
|
if release_dir.exists() or release_dir.is_symlink():
|
|
validate_engine_n8n_sealed_release(release_dir, staged_release_dir, descriptor)
|
|
return release_dir
|
|
|
|
next_dir = release_parent / f".{descriptor['releaseId']}.next-{current_stamp}"
|
|
if next_dir.exists() or next_dir.is_symlink():
|
|
die(f"Engine sealed n8n release staging path already exists: {next_dir}")
|
|
next_dir.mkdir(mode=0o700)
|
|
for filename in ("package.tgz", "release.json", "rollback.json"):
|
|
shutil.copy2(staged_release_dir / filename, next_dir / filename)
|
|
with tarfile.open(staged_release_dir / "package.tgz", "r:gz") as archive:
|
|
for member in archive:
|
|
relative = PurePosixPath(member.name)
|
|
target = next_dir.joinpath(*relative.parts)
|
|
if member.isdir():
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
continue
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die(f"Engine sealed n8n package member unreadable: {member.name}")
|
|
with target.open("xb") as output:
|
|
shutil.copyfileobj(source, output)
|
|
for path in [next_dir, *next_dir.rglob("*")]:
|
|
path_stat = path.lstat()
|
|
if stat.S_ISLNK(path_stat.st_mode):
|
|
die(f"Engine sealed n8n release symlink rejected: {path}")
|
|
os.chown(path, 0, 0)
|
|
if stat.S_ISDIR(path_stat.st_mode):
|
|
path.chmod(0o555)
|
|
elif stat.S_ISREG(path_stat.st_mode):
|
|
path.chmod(0o444)
|
|
else:
|
|
die(f"Engine sealed n8n release special member rejected: {path}")
|
|
if release_dir.exists() or release_dir.is_symlink():
|
|
die("Engine sealed n8n release appeared concurrently")
|
|
next_dir.rename(release_dir)
|
|
validate_engine_n8n_sealed_release(release_dir, staged_release_dir, descriptor)
|
|
return release_dir
|
|
|
|
|
|
def wait_engine_n8n_readiness(container_id):
|
|
script = (
|
|
"const http=require('http');const req=http.get('http://127.0.0.1:5678/healthz/readiness',"
|
|
"r=>{r.resume();process.exit(r.statusCode===200?0:1)});req.on('error',()=>process.exit(1));"
|
|
"req.setTimeout(4000,()=>{req.destroy();process.exit(1)});"
|
|
)
|
|
for _attempt in range(1, 61):
|
|
result = subprocess.run(
|
|
[str(DOCKER), "exec", container_id, "node", "-e", script],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
if result.returncode == 0:
|
|
return
|
|
time.sleep(5)
|
|
die("Engine n8n readiness check failed")
|
|
|
|
|
|
def accept_engine_n8n_runtime(descriptor):
|
|
base_image = inspect_engine_n8n_base_image()
|
|
container_id = engine_n8n_container_ids()[0]
|
|
container = inspect_container(container_id)
|
|
state = container.get("State") or {}
|
|
if state.get("Status") != "running" or container.get("Image") != base_image["id"]:
|
|
die("Engine n8n runtime image/state acceptance failed")
|
|
wait_engine_n8n_readiness(container_id)
|
|
engine_n8n_version(container_id)
|
|
config = container.get("Config") or {}
|
|
env = set(config.get("Env") or ())
|
|
mounts = container.get("Mounts") or ()
|
|
package_mounts = [mount for mount in mounts if mount.get("Destination") == ENGINE_N8N_RUNTIME_PACKAGE_PATH]
|
|
if descriptor["action"] == "activate":
|
|
expected_source = f"/volume2/nodedc-demo/{descriptor['sealedReleaseRelativePath']}"
|
|
if len(package_mounts) != 1:
|
|
die("Engine n8n package mount count mismatch")
|
|
package_mount = package_mounts[0]
|
|
if package_mount.get("Source") != expected_source or package_mount.get("RW") is not False:
|
|
die("Engine n8n sealed package mount mismatch")
|
|
required_env = {
|
|
"N8N_USER_FOLDER=/home/node",
|
|
"N8N_COMMUNITY_PACKAGES_ENABLED=true",
|
|
"N8N_COMMUNITY_PACKAGES_PREVENT_LOADING=false",
|
|
"N8N_REINSTALL_MISSING_PACKAGES=false",
|
|
}
|
|
if not required_env.issubset(env):
|
|
die("Engine n8n package loader environment mismatch")
|
|
labels = config.get("Labels") or {}
|
|
if labels.get("nodedc.n8n-private-extension.release") != descriptor["releaseId"]:
|
|
die("Engine n8n release label mismatch")
|
|
if labels.get("nodedc.n8n-private-extension.package-sha256") != descriptor["packageSha256"]:
|
|
die("Engine n8n package label mismatch")
|
|
else:
|
|
if package_mounts:
|
|
die("Engine n8n inactive baseline still has the private package mount")
|
|
inactive_forbidden = (
|
|
"N8N_USER_FOLDER=",
|
|
"N8N_COMMUNITY_PACKAGES_ENABLED=",
|
|
"N8N_COMMUNITY_PACKAGES_PREVENT_LOADING=",
|
|
"N8N_REINSTALL_MISSING_PACKAGES=",
|
|
)
|
|
if any(item.startswith(inactive_forbidden) for item in env):
|
|
die("Engine n8n inactive baseline still has private package loader flags")
|
|
if any(item.startswith("N8N_CUSTOM_EXTENSIONS=") or item.startswith("CUSTOM.") for item in env):
|
|
die("Engine n8n custom extension environment is forbidden")
|
|
|
|
started_at = str(state.get("StartedAt") or "")
|
|
logs = subprocess.run(
|
|
[str(DOCKER), "logs", "--since", started_at, container_id],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
if logs.returncode != 0:
|
|
die("Engine n8n loader log inspection failed")
|
|
scoped_logs = f"{logs.stdout}\n{logs.stderr}".lower()
|
|
error_patterns = (
|
|
r"n8n-nodes-ndc.{0,400}(?:cannot find|error|failed|duplicate|eacces|syntaxerror)",
|
|
r"(?:cannot find|error loading|failed to load|duplicate|eacces|syntaxerror).{0,400}n8n-nodes-ndc",
|
|
)
|
|
if any(re.search(pattern, scoped_logs, re.DOTALL) for pattern in error_patterns):
|
|
die("Engine n8n private extension loader log acceptance failed")
|
|
|
|
expected_types = list(ENGINE_N8N_NODE_TYPES) if descriptor["action"] == "activate" else []
|
|
expected_credentials = (
|
|
list(engine_n8n_credential_types_for_release(descriptor["releaseId"]))
|
|
if descriptor["action"] == "activate"
|
|
else []
|
|
)
|
|
loader_catalog = engine_n8n_private_loader_catalog(container_id)
|
|
if loader_catalog["node_types"] != expected_types:
|
|
die("Engine n8n live package-loader node catalog acceptance failed")
|
|
if loader_catalog["credential_types"] != expected_credentials:
|
|
die("Engine n8n live package-loader credential catalog acceptance failed")
|
|
validate_engine_n8n_catalog_payload(component_root("engine"), descriptor)
|
|
first_restart_count = int(container.get("RestartCount") or 0)
|
|
time.sleep(2)
|
|
stable_container = inspect_container(container_id)
|
|
if stable_container.get("State", {}).get("Status") != "running":
|
|
die("Engine n8n container did not remain running")
|
|
if int(stable_container.get("RestartCount") or 0) != first_restart_count:
|
|
die("Engine n8n container restarted during acceptance")
|
|
return {
|
|
"container_id": container_id,
|
|
"image_id": base_image["id"],
|
|
"node_types": expected_types,
|
|
"credential_types": list(descriptor["expectedCredentialTypes"]),
|
|
}
|
|
|
|
|
|
MAP_ACCESS_CONFIG = Path("/etc/ssh/sshd_config")
|
|
MAP_ACCESS_SSHD = Path("/usr/bin/sshd")
|
|
MAP_ACCESS_PID = Path("/var/run/sshd.pid")
|
|
MAP_ACCESS_BASE_SHA = "adf3da038acb21e30a1b79089f53917e7e82b469183889567b7d22f7b83163ba"
|
|
MAP_ACCESS_APPEND = (
|
|
b"\n# BEGIN NODEDC MISSION CORE MAP ACCESS v1\n"
|
|
b"Match User dctouch\n"
|
|
b" AllowTcpForwarding local\n"
|
|
b" PermitOpen 127.0.0.1:18103\n"
|
|
b"Match all\n"
|
|
b"# END NODEDC MISSION CORE MAP ACCESS v1\n"
|
|
)
|
|
|
|
|
|
def read_map_access_descriptor(root, entries):
|
|
if tuple(entries) != ("access.json",):
|
|
die("Map access file selection mismatch")
|
|
value = read_strict_json(root / "access.json", "Map access descriptor", max_bytes=1024)
|
|
if (set(value) != {"schemaVersion", "state"}
|
|
or value["schemaVersion"] != "nodedc.mission-core-map-access.v1"
|
|
or value["state"] not in ("enabled", "disabled")):
|
|
die("Map access descriptor mismatch")
|
|
return value["state"]
|
|
|
|
|
|
def map_access_policy(raw, desired):
|
|
base = raw[:-len(MAP_ACCESS_APPEND)] if raw.endswith(MAP_ACCESS_APPEND) else raw
|
|
if hashlib.sha256(base).hexdigest() != MAP_ACCESS_BASE_SHA:
|
|
die("Map access SSH policy predecessor drift")
|
|
return base + MAP_ACCESS_APPEND if desired == "enabled" else base
|
|
|
|
|
|
def map_access_read_config():
|
|
metadata = MAP_ACCESS_CONFIG.lstat()
|
|
if (not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0
|
|
or metadata.st_mode & 0o022 or metadata.st_size > 65536):
|
|
die("Map access SSH configuration metadata rejected")
|
|
return MAP_ACCESS_CONFIG.read_bytes()
|
|
|
|
|
|
def map_access_effective(config, user, address):
|
|
result = subprocess.run(
|
|
[str(MAP_ACCESS_SSHD), "-T", "-f", str(config), "-C",
|
|
f"user={user},host=localhost,addr={address}"],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
if result.returncode != 0:
|
|
die("Map access SSH effective-policy validation failed")
|
|
return dict(line.split(" ", 1) for line in result.stdout.splitlines() if " " in line)
|
|
|
|
|
|
def map_access_validate_candidate(before, candidate):
|
|
check = subprocess.run([str(MAP_ACCESS_SSHD), "-t", "-f", str(candidate)],
|
|
capture_output=True, timeout=10)
|
|
if check.returncode != 0:
|
|
die("Map access SSH syntax validation failed")
|
|
enabled = candidate.read_bytes().endswith(MAP_ACCESS_APPEND)
|
|
with tempfile.NamedTemporaryFile(dir=TMP_DIR) as original:
|
|
original.write(before); original.flush()
|
|
for address in ("127.0.0.1", "100.114.248.4"):
|
|
for user in ("dctouch", "root", "admin", "anonymous"):
|
|
old = map_access_effective(original.name, user, address)
|
|
new = map_access_effective(candidate, user, address)
|
|
if user == "dctouch":
|
|
old["allowtcpforwarding"] = "local" if enabled else "no"
|
|
old["permitopen"] = "127.0.0.1:18103" if enabled else "any"
|
|
if old != new:
|
|
die("Map access would change unrelated SSH authority")
|
|
|
|
|
|
def preflight_map_access(root, entries):
|
|
desired = read_map_access_descriptor(root, entries)
|
|
before = map_access_read_config()
|
|
after = map_access_policy(before, desired)
|
|
with tempfile.NamedTemporaryFile(dir=TMP_DIR) as candidate:
|
|
candidate.write(after); candidate.flush()
|
|
map_access_validate_candidate(before, Path(candidate.name))
|
|
return before, after
|
|
|
|
|
|
def map_access_replace(raw):
|
|
# Same-filesystem atomic replace; host policy is never accepted from an artifact.
|
|
original = MAP_ACCESS_CONFIG.stat()
|
|
fd, name = tempfile.mkstemp(prefix=".nodedc-map-", dir=MAP_ACCESS_CONFIG.parent)
|
|
try:
|
|
with os.fdopen(fd, "wb") as stream:
|
|
os.fchown(stream.fileno(), 0, original.st_gid)
|
|
os.fchmod(stream.fileno(), stat.S_IMODE(original.st_mode))
|
|
stream.write(raw); stream.flush(); os.fsync(stream.fileno())
|
|
os.replace(name, MAP_ACCESS_CONFIG)
|
|
directory_fd = os.open(str(MAP_ACCESS_CONFIG.parent), os.O_RDONLY)
|
|
try:
|
|
os.fsync(directory_fd)
|
|
finally:
|
|
os.close(directory_fd)
|
|
finally:
|
|
if os.path.exists(name):
|
|
os.unlink(name)
|
|
|
|
|
|
def map_access_reload():
|
|
metadata = MAP_ACCESS_PID.lstat()
|
|
if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0 or metadata.st_mode & 0o022:
|
|
die("Map access SSH PID metadata rejected")
|
|
pid = int(MAP_ACCESS_PID.read_text().strip())
|
|
if pid <= 1 or Path(f"/proc/{pid}/exe").resolve() != MAP_ACCESS_SSHD.resolve():
|
|
die("Map access SSH master identity mismatch")
|
|
os.kill(pid, signal.SIGHUP)
|
|
deadline = time.monotonic() + 10
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
with socket.create_connection(("127.0.0.1", 22), timeout=2) as connection:
|
|
connection.settimeout(2)
|
|
if connection.recv(255).startswith(b"SSH-2.0-"):
|
|
return
|
|
except OSError:
|
|
pass
|
|
time.sleep(0.25)
|
|
die("Map access SSH listener did not recover")
|
|
|
|
|
|
def backup_map_access(backup_dir):
|
|
raw = map_access_read_config()
|
|
map_access_policy(raw, "enabled")
|
|
path = backup_dir / "sshd-config-before"
|
|
with path.open("xb") as output:
|
|
os.fchmod(output.fileno(), 0o600)
|
|
output.write(raw); output.flush(); os.fsync(output.fileno())
|
|
|
|
|
|
def apply_map_access(root, entries):
|
|
before, after = preflight_map_access(root, entries)
|
|
if after != before:
|
|
map_access_replace(after)
|
|
map_access_reload()
|
|
|
|
|
|
def accept_map_access(root, entries):
|
|
before, after = preflight_map_access(root, entries)
|
|
if before != after:
|
|
die("Map access installed policy differs from the requested state")
|
|
healthcheck_url({
|
|
"url": "http://127.0.0.1:18103/healthz",
|
|
"headers": {"x-nodedc-user-id": "mission-core-loopback-operator"},
|
|
"expected_json": {"ok": True, "service": "nodedc-map-gateway"},
|
|
})
|
|
|
|
|
|
def rollback_map_access(root, backup_dir, entries, current_stamp):
|
|
original = backup_dir / "sshd-config-before"
|
|
metadata = original.lstat()
|
|
if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0 or metadata.st_mode & 0o077:
|
|
die("Map access backup metadata rejected")
|
|
raw = original.read_bytes()
|
|
map_access_policy(raw, "enabled")
|
|
current = map_access_read_config()
|
|
map_access_policy(current, "enabled")
|
|
map_access_validate_candidate(current, original)
|
|
if raw != current:
|
|
map_access_replace(raw)
|
|
map_access_reload()
|
|
restore_overlay_source(root, backup_dir, entries, current_stamp)
|
|
if map_access_read_config() != raw:
|
|
die("Map access rollback policy verification failed")
|
|
|
|
|
|
def plan_artifact(artifact):
|
|
validate_artifact_location(artifact)
|
|
ensure_layout()
|
|
|
|
sha = sha256_file(artifact)
|
|
publish_grant_preflight = None
|
|
node_intelligence_preflight = None
|
|
mcp_control_plane_preflight = None
|
|
mcp_ontology_sdk_preflight = None
|
|
mcp_autonomy_provider_v5_preflight = None
|
|
l2_closed_loop_preflight = None
|
|
device_plane_foundation_recovery_preflight = None
|
|
device_plane_network_publication_preflight = None
|
|
device_plane_b2_ingress_preflight = None
|
|
device_plane_b2_recovery_preflight = None
|
|
device_plane_manager_activation_preflight = None
|
|
device_plane_edge_core_channel_preflight = None
|
|
device_plane_control_core_release_preflight = None
|
|
device_plane_manager_reconciliation_preflight = None
|
|
device_plane_manager_v2_reconciliation_preflight = None
|
|
device_plane_control_core_v3_reconciliation_preflight = None
|
|
device_plane_control_core_incident_audit_preflight = None
|
|
device_plane_control_core_migration_replay_audit_preflight = None
|
|
device_plane_control_core_migration_replay_recovery_preflight = None
|
|
device_plane_control_core_migration_replay_checkpoint_recovery_preflight = None
|
|
device_plane_backhaul_preflight = None
|
|
device_plane_backhaul_vps_enrollment_preflight = None
|
|
device_plane_runtime_before = None
|
|
composite_provider_v4_preflight = None
|
|
provider_rotating_slot_preflight = None
|
|
provider_authority_diagnostics_preflight = None
|
|
depttrans_zone_authority_v1_preflight = None
|
|
provider_target_host_policy_preflight = None
|
|
mcp_execution_profile_decoder_preflight = None
|
|
mcp_telemetry_catalog_preflight = None
|
|
mcp_execution_plan_materialization_preflight = None
|
|
mcp_execution_plan_telemetry_runtime_preflight = None
|
|
mcp_execution_plan_module_ownership_preflight = None
|
|
mcp_normalized_identity_search_preflight = None
|
|
mcp_l1_credential_reuse_preflight = None
|
|
mcp_l1_credential_provenance_preflight = None
|
|
mcp_execution_plan_sandbox_runtime_preflight = None
|
|
mcp_gelios_items_envelope_preflight = None
|
|
mcp_registered_execution_profiles_preflight = None
|
|
mcp_gelios_units_items_preflight = None
|
|
provider_catalog_preflight = None
|
|
device_plane_postgres_preflight = None
|
|
device_plane_foundation_recovery_preflight = None
|
|
gitea_preflight = None
|
|
gitea_salvage_preflight = None
|
|
with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp:
|
|
manifest, entries, payload_dir = load_artifact(artifact, Path(tmp))
|
|
if is_gitea_fresh_install_slice(manifest["component"], entries):
|
|
validate_gitea_compose_schema(payload_dir / GITEA_COMPOSE_REL)
|
|
if is_gitea_incident_salvage_slice(manifest["component"], entries):
|
|
validate_gitea_compose_schema(payload_dir / GITEA_COMPOSE_REL)
|
|
if manifest["component"] == "mission-core-map-access":
|
|
preflight_map_access(payload_dir, entries)
|
|
reject_failed_artifact_replay(manifest, sha)
|
|
reject_terminal_engine_l2_failed_artifact(manifest, sha)
|
|
reject_terminal_device_plane_foundation_artifact(manifest, sha)
|
|
reject_terminal_device_plane_manager_artifact(
|
|
manifest,
|
|
sha,
|
|
entries,
|
|
)
|
|
reject_terminal_device_plane_backhaul_artifact(manifest, sha)
|
|
transition_descriptor = None
|
|
transition_preflight = None
|
|
if is_engine_n8n_transition(manifest["component"], entries):
|
|
transition_descriptor = read_engine_n8n_transition_descriptor(
|
|
payload_dir / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL
|
|
)
|
|
transition_preflight = preflight_engine_n8n_transition(
|
|
transition_descriptor,
|
|
enforce_expected_current=False,
|
|
)
|
|
if is_engine_data_product_publish_grant_slice(manifest["component"], entries):
|
|
publish_grant_preflight = (
|
|
preflight_engine_data_product_publish_grant_predecessor(payload_dir)
|
|
)
|
|
if is_engine_node_intelligence_transition(manifest["component"], entries):
|
|
node_intelligence_preflight = preflight_engine_node_intelligence_predecessor(
|
|
payload_dir
|
|
)
|
|
if is_engine_mcp_control_plane_slice(manifest["component"], entries):
|
|
mcp_control_plane_preflight = preflight_engine_mcp_control_plane_predecessor(
|
|
payload_dir
|
|
)
|
|
if is_engine_mcp_ontology_sdk_slice(manifest["component"], entries):
|
|
mcp_ontology_sdk_preflight = preflight_engine_mcp_ontology_sdk_predecessor(
|
|
payload_dir
|
|
)
|
|
if is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries):
|
|
mcp_autonomy_provider_v5_preflight = (
|
|
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):
|
|
composite_provider_v4_preflight = preflight_engine_composite_provider_v4_predecessor()
|
|
if is_engine_provider_rotating_slot_slice(manifest["component"], entries):
|
|
provider_rotating_slot_preflight = preflight_engine_provider_rotating_slot_predecessor()
|
|
if is_engine_provider_authority_diagnostics_slice(manifest["component"], entries):
|
|
provider_authority_diagnostics_preflight = (
|
|
preflight_engine_provider_authority_diagnostics_predecessor()
|
|
)
|
|
if is_engine_depttrans_zone_authority_v1_slice(manifest["component"], entries):
|
|
depttrans_zone_authority_v1_preflight = (
|
|
preflight_engine_depttrans_zone_authority_v1_predecessor()
|
|
)
|
|
if is_engine_provider_target_host_policy_slice(manifest["component"], entries):
|
|
provider_target_host_policy_preflight = (
|
|
preflight_engine_provider_target_host_policy_predecessor()
|
|
)
|
|
if is_engine_mcp_execution_profile_decoder_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_execution_profile_decoder_preflight = (
|
|
preflight_engine_mcp_execution_profile_decoder_predecessor()
|
|
)
|
|
if is_engine_mcp_telemetry_catalog_slice(manifest["component"], entries):
|
|
mcp_telemetry_catalog_preflight = (
|
|
preflight_engine_mcp_telemetry_catalog_predecessor()
|
|
)
|
|
if is_engine_mcp_execution_plan_materialization_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_execution_plan_materialization_preflight = (
|
|
preflight_engine_mcp_execution_plan_materialization_predecessor()
|
|
)
|
|
if is_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_execution_plan_telemetry_runtime_preflight = (
|
|
preflight_engine_mcp_execution_plan_telemetry_runtime_predecessor()
|
|
)
|
|
if is_engine_mcp_execution_plan_module_ownership_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_execution_plan_module_ownership_preflight = (
|
|
preflight_engine_mcp_execution_plan_module_ownership_predecessor()
|
|
)
|
|
if is_engine_mcp_normalized_identity_search_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_normalized_identity_search_preflight = (
|
|
preflight_engine_mcp_normalized_identity_search_predecessor()
|
|
)
|
|
if is_engine_mcp_l1_credential_reuse_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_l1_credential_reuse_preflight = (
|
|
preflight_engine_mcp_l1_credential_reuse_predecessor()
|
|
)
|
|
if is_engine_mcp_l1_credential_provenance_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_l1_credential_provenance_preflight = (
|
|
preflight_engine_mcp_l1_credential_provenance_predecessor()
|
|
)
|
|
if is_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_execution_plan_sandbox_runtime_preflight = (
|
|
preflight_engine_mcp_execution_plan_sandbox_runtime_predecessor()
|
|
)
|
|
if is_engine_mcp_gelios_items_envelope_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_gelios_items_envelope_preflight = (
|
|
preflight_engine_mcp_gelios_items_envelope_predecessor()
|
|
)
|
|
if is_engine_mcp_registered_execution_profiles_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_registered_execution_profiles_preflight = (
|
|
preflight_engine_mcp_registered_execution_profiles_predecessor()
|
|
)
|
|
if is_engine_mcp_gelios_units_items_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
mcp_gelios_units_items_preflight = (
|
|
preflight_engine_mcp_gelios_units_items_predecessor()
|
|
)
|
|
if is_engine_provider_security_catalog_slice(manifest["component"], entries):
|
|
provider_catalog_preflight = preflight_engine_provider_security_catalog_predecessor()
|
|
if is_device_plane_postgres_bootstrap_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_postgres_preflight = (
|
|
preflight_device_plane_postgres_bootstrap()
|
|
)
|
|
if is_device_plane_foundation_recovery_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_foundation_recovery_preflight = (
|
|
validate_device_plane_foundation_recovery_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_foundation_network_publication_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_network_publication_preflight = (
|
|
validate_device_plane_foundation_network_publication_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_b2_discovery_ingress_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_b2_ingress_preflight = (
|
|
validate_device_plane_b2_discovery_ingress_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_b2_recovery_preflight = (
|
|
validate_device_plane_b2_discovery_rollback_recovery_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_manager_control_plane_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_manager_activation_preflight = (
|
|
validate_device_plane_manager_activation_predecessor(
|
|
payload_dir,
|
|
preflight_phase="plan",
|
|
)
|
|
)
|
|
if is_device_plane_edge_core_channel_bootstrap_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_edge_core_channel_preflight = (
|
|
validate_device_plane_edge_core_channel_bootstrap_predecessor(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_control_core_release_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_control_core_release_preflight = (
|
|
validate_device_plane_control_core_release_predecessor(
|
|
payload_dir,
|
|
preflight_phase="plan",
|
|
)
|
|
)
|
|
if is_device_plane_manager_reconciliation_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_manager_reconciliation_preflight = (
|
|
validate_device_plane_manager_reconciliation_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_manager_v2_reconciliation_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_manager_v2_reconciliation_preflight = (
|
|
validate_device_plane_manager_v2_reconciliation_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_control_core_v3_reconciliation_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_control_core_v3_reconciliation_preflight = (
|
|
validate_device_plane_control_core_v3_reconciliation_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_control_core_incident_audit_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
incident = validate_device_plane_control_core_incident_audit_evidence(
|
|
payload_dir
|
|
)
|
|
incident["diagnostics"] = (
|
|
collect_device_plane_control_core_incident_audit()
|
|
)
|
|
device_plane_control_core_incident_audit_preflight = incident
|
|
if is_device_plane_control_core_migration_replay_audit_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
if (
|
|
manifest["id"]
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_AUDIT_PATCH_ID
|
|
):
|
|
die("Device Control Core migration replay audit patch id mismatch")
|
|
device_plane_control_core_migration_replay_audit_preflight = (
|
|
validate_device_plane_control_core_migration_replay_audit_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_control_core_migration_replay_recovery_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
if (
|
|
manifest["id"]
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
):
|
|
die("Device Control Core migration recovery patch id mismatch")
|
|
device_plane_control_core_migration_replay_recovery_preflight = (
|
|
validate_device_plane_control_core_migration_replay_recovery_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
if (
|
|
manifest["id"]
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_PATCH_ID
|
|
):
|
|
die(
|
|
"Device Control Core migration replay checkpoint recovery "
|
|
"patch id mismatch"
|
|
)
|
|
device_plane_control_core_migration_replay_checkpoint_recovery_preflight = (
|
|
validate_device_plane_control_core_migration_replay_checkpoint_recovery_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_backhaul_target_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_backhaul_preflight = (
|
|
validate_device_plane_backhaul_target_evidence(payload_dir)
|
|
)
|
|
if is_device_plane_backhaul_vps_enrollment_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
device_plane_backhaul_vps_enrollment_preflight = (
|
|
validate_device_plane_backhaul_vps_enrollment_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_gitea_fresh_install_slice(manifest["component"], entries):
|
|
gitea_preflight = preflight_gitea_fresh_install()
|
|
if is_gitea_incident_salvage_slice(manifest["component"], entries):
|
|
gitea_salvage_preflight = preflight_gitea_incident_salvage(
|
|
payload_dir,
|
|
enforce_apply=False,
|
|
)
|
|
|
|
component = manifest["component"]
|
|
root = component_root(component)
|
|
compose_root = component_compose_root(component)
|
|
services = component_services(component, entries)
|
|
builds = component_builds(component, entries)
|
|
touches_publish_grant = is_engine_data_product_publish_grant_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_composite_provider_v4 = is_engine_composite_provider_v4_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_provider_rotating_slot = is_engine_provider_rotating_slot_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_provider_authority_diagnostics = is_engine_provider_authority_diagnostics_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_depttrans_zone_authority_v1 = is_engine_depttrans_zone_authority_v1_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_provider_target_host_policy = is_engine_provider_target_host_policy_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_mcp_execution_profile_decoder = (
|
|
is_engine_mcp_execution_profile_decoder_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_telemetry_catalog = is_engine_mcp_telemetry_catalog_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_mcp_execution_plan_materialization = (
|
|
is_engine_mcp_execution_plan_materialization_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_execution_plan_telemetry_runtime = (
|
|
is_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_execution_plan_module_ownership = (
|
|
is_engine_mcp_execution_plan_module_ownership_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_normalized_identity_search = (
|
|
is_engine_mcp_normalized_identity_search_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_l1_credential_reuse = (
|
|
is_engine_mcp_l1_credential_reuse_slice(component, entries)
|
|
)
|
|
touches_mcp_l1_credential_provenance = (
|
|
is_engine_mcp_l1_credential_provenance_slice(component, entries)
|
|
)
|
|
touches_mcp_execution_plan_sandbox_runtime = (
|
|
is_engine_mcp_execution_plan_sandbox_runtime_slice(component, entries)
|
|
)
|
|
touches_mcp_gelios_items_envelope = (
|
|
is_engine_mcp_gelios_items_envelope_slice(component, entries)
|
|
)
|
|
touches_mcp_registered_execution_profiles = (
|
|
is_engine_mcp_registered_execution_profiles_slice(component, entries)
|
|
)
|
|
touches_mcp_gelios_units_items = (
|
|
is_engine_mcp_gelios_units_items_slice(component, entries)
|
|
)
|
|
touches_agent_grant_migration = is_engine_agent_full_grant_migration_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
publish_grant_predecessor_sha256 = None
|
|
agent_grant_migration_predecessor_sha256 = None
|
|
credential_backend_preflight = None
|
|
if (
|
|
touches_engine_credential_sink(component, entries)
|
|
or touches_publish_grant
|
|
or touches_composite_provider_v4
|
|
or touches_provider_rotating_slot
|
|
or touches_provider_authority_diagnostics
|
|
or touches_depttrans_zone_authority_v1
|
|
or touches_provider_target_host_policy
|
|
or touches_mcp_execution_profile_decoder
|
|
or touches_mcp_telemetry_catalog
|
|
or touches_mcp_execution_plan_materialization
|
|
or touches_mcp_execution_plan_telemetry_runtime
|
|
or touches_mcp_execution_plan_module_ownership
|
|
or touches_mcp_normalized_identity_search
|
|
or touches_mcp_l1_credential_reuse
|
|
or touches_mcp_l1_credential_provenance
|
|
or touches_mcp_execution_plan_sandbox_runtime
|
|
or touches_mcp_gelios_items_envelope
|
|
or touches_mcp_registered_execution_profiles
|
|
or touches_mcp_gelios_units_items
|
|
or touches_agent_grant_migration
|
|
):
|
|
credential_backend_preflight = preflight_engine_credential_backend_runtime()
|
|
if touches_publish_grant:
|
|
if publish_grant_preflight is None:
|
|
die("Engine data product publish grant preflight is missing")
|
|
publish_grant_predecessor_sha256 = publish_grant_preflight["compose_sha256"]
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine data product publish grant requires the active immutable credential backend")
|
|
if touches_composite_provider_v4:
|
|
if composite_provider_v4_preflight is None:
|
|
die("Engine composite provider v4 preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine composite provider v4 requires the active immutable credential backend")
|
|
if touches_provider_rotating_slot:
|
|
if provider_rotating_slot_preflight is None:
|
|
die("Engine provider rotating slot preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine provider rotating slot requires the active immutable credential backend")
|
|
if touches_provider_authority_diagnostics:
|
|
if provider_authority_diagnostics_preflight is None:
|
|
die("Engine provider authority diagnostics preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine provider authority diagnostics requires the active immutable credential backend")
|
|
if touches_depttrans_zone_authority_v1:
|
|
if depttrans_zone_authority_v1_preflight is None:
|
|
die("Engine Depttrans zone authority v1 preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine Depttrans zone authority v1 requires the active immutable credential backend")
|
|
if touches_provider_target_host_policy:
|
|
if provider_target_host_policy_preflight is None:
|
|
die("Engine provider target host policy preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine provider target host policy requires the active immutable credential backend")
|
|
if touches_mcp_execution_profile_decoder:
|
|
if mcp_execution_profile_decoder_preflight is None:
|
|
die("Engine MCP execution profile decoder preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution profile decoder requires the active "
|
|
"immutable credential backend"
|
|
)
|
|
if touches_mcp_telemetry_catalog:
|
|
if mcp_telemetry_catalog_preflight is None:
|
|
die("Engine MCP telemetry catalog preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP telemetry catalog requires the active immutable "
|
|
"credential backend"
|
|
)
|
|
if touches_mcp_execution_plan_materialization:
|
|
if mcp_execution_plan_materialization_preflight is None:
|
|
die("Engine MCP execution plan materialization preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution plan materialization requires the active "
|
|
"immutable credential backend"
|
|
)
|
|
if touches_mcp_execution_plan_telemetry_runtime:
|
|
if mcp_execution_plan_telemetry_runtime_preflight is None:
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime preflight is "
|
|
"missing"
|
|
)
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime requires the "
|
|
"active immutable credential backend"
|
|
)
|
|
if touches_mcp_execution_plan_module_ownership:
|
|
if mcp_execution_plan_module_ownership_preflight is None:
|
|
die(
|
|
"Engine MCP execution plan module ownership preflight is "
|
|
"missing"
|
|
)
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution plan module ownership requires the "
|
|
"active immutable credential backend"
|
|
)
|
|
if touches_mcp_normalized_identity_search:
|
|
if mcp_normalized_identity_search_preflight is None:
|
|
die("Engine MCP normalized identity search preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP normalized identity search requires the active "
|
|
"immutable credential backend"
|
|
)
|
|
if touches_mcp_l1_credential_reuse:
|
|
if mcp_l1_credential_reuse_preflight is None:
|
|
die("Engine MCP L1 credential reuse preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP L1 credential reuse requires the active immutable "
|
|
"credential backend"
|
|
)
|
|
if touches_mcp_l1_credential_provenance:
|
|
if mcp_l1_credential_provenance_preflight is None:
|
|
die("Engine MCP L1 credential provenance preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP L1 credential provenance requires the active "
|
|
"immutable credential backend"
|
|
)
|
|
if touches_mcp_execution_plan_sandbox_runtime:
|
|
if mcp_execution_plan_sandbox_runtime_preflight is None:
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime preflight is "
|
|
"missing"
|
|
)
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime requires the "
|
|
"active immutable credential backend"
|
|
)
|
|
if touches_mcp_gelios_items_envelope:
|
|
if mcp_gelios_items_envelope_preflight is None:
|
|
die("Engine MCP Gelios items envelope preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP Gelios items envelope requires the active "
|
|
"immutable credential backend"
|
|
)
|
|
if touches_mcp_registered_execution_profiles:
|
|
if mcp_registered_execution_profiles_preflight is None:
|
|
die(
|
|
"Engine MCP registered execution profiles preflight is missing"
|
|
)
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP registered execution profiles requires the active "
|
|
"immutable credential backend"
|
|
)
|
|
if touches_mcp_gelios_units_items:
|
|
if mcp_gelios_units_items_preflight is None:
|
|
die("Engine MCP Gelios units items preflight is missing")
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die(
|
|
"Engine MCP Gelios units items requires the active immutable "
|
|
"credential backend"
|
|
)
|
|
if touches_agent_grant_migration:
|
|
agent_grant_migration_predecessor_sha256 = (
|
|
preflight_engine_agent_full_grant_migration_predecessor()
|
|
)
|
|
if credential_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine agent full grant migration requires the active immutable credential backend")
|
|
print("== plan ==")
|
|
print(f"artifact={artifact.name}")
|
|
print(f"sha256={sha}")
|
|
print(f"id={manifest['id']}")
|
|
print(f"component={component}")
|
|
print(f"type={manifest['type']}")
|
|
print(f"payload_root={root}")
|
|
if component == "mission-core-map-access":
|
|
print("access=local-forward:dctouch:127.0.0.1:18103")
|
|
print("runtime=sshd-scoped-reload;docker=untouched;cache=preserved")
|
|
print("policy_predecessor=exact;rollback=automatic")
|
|
print(f"compose_root={compose_root}")
|
|
compose_project = component_compose_project(component)
|
|
if compose_project:
|
|
print(f"compose_project={compose_project}")
|
|
for build_root, build_args in builds:
|
|
print(f"build_root={build_root}")
|
|
print(f"build={' '.join((str(DOCKER),) + tuple(build_args))}")
|
|
if is_device_plane_control_core_incident_audit_slice(component, entries):
|
|
print("services=none")
|
|
else:
|
|
print(f"services={' '.join(services)}")
|
|
if gitea_preflight:
|
|
print("gitea_transition=fresh-install-only")
|
|
print(f"gitea_root={GITEA_ROOT}:required-absent")
|
|
print(f"gitea_image={GITEA_IMAGE}")
|
|
print(f"gitea_image_id={gitea_preflight['image_id']}")
|
|
print("gitea_platform=linux/amd64")
|
|
print("gitea_pull=never")
|
|
print(f"gitea_docker_version={gitea_preflight['docker_version']}")
|
|
print(
|
|
"gitea_compose_version="
|
|
f"{gitea_preflight['compose_version']}"
|
|
)
|
|
print("gitea_database=fresh-sqlite-users-0-repositories-0")
|
|
print("gitea_lfs=disabled-pending-reviewed-restore-transition")
|
|
print("gitea_transport=unix:/run/gitea/gitea.sock")
|
|
print(f"gitea_socket_host={GITEA_SOCKET_FILE}")
|
|
print("gitea_ssh=disabled-no-published-port")
|
|
print("gitea_network_mode=none")
|
|
print("gitea_docker_ports=none")
|
|
print(f"gitea_secret=runner-managed:{GITEA_SECRET_KEY_FILE}")
|
|
print(f"gitea_secret=runner-managed:{GITEA_INTERNAL_TOKEN_FILE}")
|
|
print("gitea_legacy_root=unread-unmounted-untouched")
|
|
print("gitea_legacy_container=never-started")
|
|
print(
|
|
"gitea_reverse_proxy="
|
|
f"{gitea_preflight['reverse_proxy']['upstream']}"
|
|
)
|
|
print(
|
|
"gitea_nginx_uds_bridge="
|
|
f"{gitea_preflight['nginx_bridge']['sha256']}"
|
|
)
|
|
print("gitea_firewall=legacy-isolated-loopback-3000-ready")
|
|
print(
|
|
"gitea_rollback=stop-remove-candidate-preserve-runtime-state-"
|
|
"quarantine-source"
|
|
)
|
|
if gitea_salvage_preflight:
|
|
print("gitea_transition=incident-salvage-clean-state")
|
|
print(f"gitea_root={GITEA_ROOT}:required-absent")
|
|
print(f"gitea_image={GITEA_SALVAGE_IMAGE}")
|
|
print(f"gitea_image_id={GITEA_SALVAGE_IMAGE_ID}")
|
|
print(f"gitea_repo_digest={GITEA_SALVAGE_REPO_DIGEST}")
|
|
print("gitea_database=new-sqlite-no-legacy-import")
|
|
print("gitea_users=2-active-8-locked-new-credentials")
|
|
print("gitea_repositories=45-exact-object-and-ref-material-only")
|
|
print("gitea_repository_visibility=dctouch-private-32,silver-public-13")
|
|
print("gitea_two_factor_authentication=unchanged-not-configured")
|
|
print("gitea_candidate_publication=socket-parent-0700")
|
|
print("gitea_candidate_restart=no")
|
|
legacy = gitea_salvage_preflight["legacy"]
|
|
legacy_mount = legacy["mounts"][0]
|
|
print(f"gitea_legacy_container_name={legacy['name']}")
|
|
print(f"gitea_legacy_container_state={legacy['state']}")
|
|
print(f"gitea_legacy_image_ref={legacy['image']}")
|
|
print(f"gitea_legacy_image_id={legacy['image_id']}")
|
|
print(
|
|
"gitea_legacy_mount="
|
|
f"{legacy_mount[0]}:{legacy_mount[1]}:{legacy_mount[2]}:"
|
|
f"{'rw' if legacy_mount[3] else 'ro'}"
|
|
)
|
|
print(
|
|
"gitea_snapshot_uuid="
|
|
f"{gitea_salvage_preflight['snapshot']['uuid']}"
|
|
)
|
|
print(
|
|
"gitea_snapshot_database_sha256="
|
|
f"{gitea_salvage_preflight['snapshot']['database_sha256']}"
|
|
)
|
|
print(
|
|
"gitea_reference_manifest_sha256="
|
|
f"{gitea_salvage_preflight['refs']['sha256']}"
|
|
)
|
|
print(
|
|
"gitea_reference_manifest_bytes="
|
|
f"{gitea_salvage_preflight['refs']['bytes']}"
|
|
)
|
|
print(
|
|
"gitea_reference_manifest_json="
|
|
f"{gitea_salvage_preflight['refs']['json']}"
|
|
)
|
|
print(
|
|
"gitea_reference_inventory="
|
|
f"stores:{gitea_salvage_preflight['refs']['repository_stores']},"
|
|
f"refs:{gitea_salvage_preflight['refs']['refs']},"
|
|
f"object_files:{gitea_salvage_preflight['refs']['object_files']},"
|
|
f"object_bytes:{gitea_salvage_preflight['refs']['object_bytes']},"
|
|
"excluded_derived_files:"
|
|
f"{gitea_salvage_preflight['refs']['excluded_derived_files']},"
|
|
"excluded_derived_bytes:"
|
|
f"{gitea_salvage_preflight['refs']['excluded_derived_bytes']},"
|
|
"excluded_quarantine_directories:"
|
|
f"{gitea_salvage_preflight['refs']['excluded_quarantine_directories']}"
|
|
)
|
|
disposition = gitea_salvage_preflight["disposition"]
|
|
print(f"gitea_incident_disposition_sha256={disposition['sha256']}")
|
|
print(
|
|
"gitea_incident_disposition_refs="
|
|
f"forensic:{disposition['forensic_refs']},"
|
|
f"live:{disposition['live_refs']},"
|
|
f"archive-only:{disposition['archive_only_refs']}"
|
|
)
|
|
print(
|
|
"gitea_incident_disposition_policy="
|
|
"heads-tags-and-wiki-heads-live;pull-remote-refs-archive-only;"
|
|
"legacy-metadata-sanitized-archive-only;no-legacy-row-import"
|
|
)
|
|
topics = disposition["topics"]
|
|
print(f"gitea_semantic_topics_sha256={topics['sha256']}")
|
|
print(f"gitea_semantic_topics_bytes={topics['bytes']}")
|
|
print(f"gitea_semantic_topics_json={topics['json']}")
|
|
print(
|
|
"gitea_incident_disposition_remaining_blockers="
|
|
+ ",".join(disposition["blockers"])
|
|
)
|
|
unsupported = gitea_salvage_preflight["database"]["unsupported"]
|
|
print(
|
|
"gitea_unsupported_repository_report_sha256="
|
|
f"{unsupported['sha256']}"
|
|
)
|
|
print(
|
|
"gitea_unsupported_repository_report_bytes="
|
|
f"{unsupported['bytes']}"
|
|
)
|
|
print(
|
|
"gitea_unsupported_schema_catalog_sha256="
|
|
f"{unsupported['report']['schema_catalog_sha256']}"
|
|
)
|
|
print(
|
|
"gitea_unsupported_repository_report_json="
|
|
f"{unsupported['json']}"
|
|
)
|
|
closure = gitea_salvage_preflight["closure_disposition"]
|
|
print(
|
|
"gitea_incident_closure_disposition_sha256="
|
|
f"{GITEA_SALVAGE_CLOSURE_DISPOSITION_SHA256}"
|
|
)
|
|
print(f"gitea_incident_closure_report_sha256={closure['sha256']}")
|
|
print(f"gitea_incident_closure_report_bytes={closure['bytes']}")
|
|
print(f"gitea_incident_closure_report_json={closure['json']}")
|
|
print(
|
|
"gitea_apply_blockers="
|
|
+ (",".join(gitea_salvage_preflight["blockers"]) or "none")
|
|
)
|
|
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:
|
|
descriptor = node_intelligence_preflight["descriptor"]
|
|
print(f"node_intelligence_transition={descriptor['action']}")
|
|
print(f"node_intelligence_release={descriptor['releaseId']}")
|
|
print(f"node_intelligence_current_state={node_intelligence_preflight['current_state']}")
|
|
print(f"node_intelligence_upstream_commit={descriptor['upstream']['commit']}")
|
|
print(f"node_intelligence_image={descriptor['image']['tag']}")
|
|
print(f"node_intelligence_image_config_sha256={descriptor['image']['configSha256']}")
|
|
if descriptor["action"] == "activate":
|
|
print(f"node_intelligence_image_archive_sha256={descriptor['image']['archiveSha256']}")
|
|
print("node_intelligence_build=offline-image-load")
|
|
print("node_intelligence_pull=never")
|
|
print(f"node_intelligence_secret=runner-managed:{ENGINE_NODE_INTELLIGENCE_SECRET_FILE}")
|
|
print("node_intelligence_host_ports=none")
|
|
print(f"predecessor_gateway_sha256={node_intelligence_preflight['gateway_sha256']}")
|
|
print(f"backend_current_barrier={node_intelligence_preflight['backend_mode']}")
|
|
if mcp_control_plane_preflight:
|
|
print("engine_mcp_transition=managed-reader-grant+validator-reconciliation")
|
|
print("engine_mcp_version=0.5.0")
|
|
print("engine_mcp_installer=0.1.4")
|
|
print("engine_mcp_reader_grant=plan+apply+accept+rollback")
|
|
print(
|
|
"predecessor_gateway_sha256="
|
|
f"{mcp_control_plane_preflight['predecessor_gateway_sha256']}"
|
|
)
|
|
print(
|
|
"target_gateway_sha256="
|
|
f"{mcp_control_plane_preflight['target_gateway_sha256']}"
|
|
)
|
|
print(f"backend_current_barrier={mcp_control_plane_preflight['backend_mode']}")
|
|
print("node_intelligence_image=preserved")
|
|
print("n8n_l1=untouched")
|
|
if mcp_ontology_sdk_preflight:
|
|
print("engine_mcp_transition=separate-ontology+gelios-sdk-query-auth")
|
|
print("engine_mcp_version=0.6.0")
|
|
print("engine_mcp_installer=0.1.5")
|
|
print("engine_mcp_ontology=separate-read-only-proxy")
|
|
print("engine_provider_package=gelios.provider.v2")
|
|
print("engine_provider_credential=httpQueryAuth")
|
|
print(
|
|
"predecessor_gateway_sha256="
|
|
f"{mcp_ontology_sdk_preflight['predecessor_gateway_sha256']}"
|
|
)
|
|
print(
|
|
"target_gateway_sha256="
|
|
f"{mcp_ontology_sdk_preflight['target_gateway_sha256']}"
|
|
)
|
|
print(f"backend_current_barrier={mcp_ontology_sdk_preflight['backend_mode']}")
|
|
print("node_intelligence_image=preserved")
|
|
print("n8n_l1=untouched")
|
|
if mcp_autonomy_provider_v5_preflight:
|
|
print("engine_mcp_transition=capability-scoped-autonomy+provider-v5")
|
|
print("engine_mcp_version=0.6.0")
|
|
print("engine_mcp_installer=0.1.6")
|
|
print("engine_mcp_authority=mcp-capability-intersect-user-objective")
|
|
print("engine_mcp_plan=machine-safety-barrier")
|
|
print("engine_mcp_retry_stop=three-identical-failures-without-new-evidence")
|
|
print("engine_provider_packages=gelios.provider.v4,gelios.provider.v5")
|
|
print("engine_data_product=fleet.positions.current.v4")
|
|
print(
|
|
"predecessor_gateway_sha256="
|
|
f"{mcp_autonomy_provider_v5_preflight['predecessor_gateway_sha256']}"
|
|
)
|
|
print(
|
|
"target_gateway_sha256="
|
|
f"{mcp_autonomy_provider_v5_preflight['target_gateway_sha256']}"
|
|
)
|
|
print(f"backend_current_barrier={mcp_autonomy_provider_v5_preflight['backend_mode']}")
|
|
print("node_intelligence_image=preserved")
|
|
print("n8n_l1=untouched")
|
|
if provider_catalog_preflight:
|
|
print("engine_provider_catalog_transition=gelios-unit-profile-authority-v8")
|
|
print("engine_provider_package=gelios.provider.v8")
|
|
print("engine_provider_credential=ndcProviderRotatingAccessApi")
|
|
print(
|
|
"engine_provider_endpoint=https://api.geliospro.com/api/v1/units?"
|
|
"incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true"
|
|
)
|
|
print("engine_data_product=fleet.units.profile.current.v1")
|
|
print(
|
|
"predecessor_catalog_sha256="
|
|
f"{provider_catalog_preflight['catalog_sha256']}"
|
|
)
|
|
print(f"target_catalog_sha256={ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256}")
|
|
print(f"backend_current_barrier={provider_catalog_preflight['backend_mode']}")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("mcp_nginx=untouched")
|
|
if composite_provider_v4_preflight:
|
|
print("engine_composite_provider_transition=exact-v3-to-v4")
|
|
print("engine_provider_package=gelios.provider.v4")
|
|
print(
|
|
"engine_provider_capabilities="
|
|
"gelios.monitoring_config.current.read,gelios.units.current.read"
|
|
)
|
|
print("engine_provider_requests=monitoring-config,units?incltrip=true")
|
|
print("engine_data_product=fleet.positions.current.v3")
|
|
for changed_path in ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES:
|
|
print(f"engine_composite_provider_changed_path={changed_path}")
|
|
print(
|
|
f"engine_composite_provider_predecessor_sha256[{changed_path}]="
|
|
f"{composite_provider_v4_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
f"engine_composite_provider_target_sha256[{changed_path}]="
|
|
f"{composite_provider_v4_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(f"backend_current_barrier={composite_provider_v4_preflight['backend_mode']}")
|
|
print("provider_credential_values=preserved")
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
if provider_rotating_slot_preflight:
|
|
print("engine_provider_credential_transition=exact-active-slot-alignment")
|
|
print("engine_provider_package=gelios.provider.v4")
|
|
print("engine_provider_auth_mode=gelios.rest-rotating-bearer.v3")
|
|
print("engine_provider_credential_slot=ndcProviderRotatingAccessApi")
|
|
print("engine_data_product=fleet.positions.current.v3")
|
|
for changed_path in ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES:
|
|
print(f"engine_provider_rotating_slot_changed_path={changed_path}")
|
|
print(
|
|
f"engine_provider_rotating_slot_predecessor_sha256[{changed_path}]="
|
|
f"{provider_rotating_slot_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
f"engine_provider_rotating_slot_target_sha256[{changed_path}]="
|
|
f"{provider_rotating_slot_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(f"backend_current_barrier={provider_rotating_slot_preflight['backend_mode']}")
|
|
print("provider_credential_values=preserved")
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
if provider_authority_diagnostics_preflight:
|
|
print("engine_validation_transition=private-node-name-reconciliation")
|
|
print("engine_validation_boundary=engine-pinned-private-node-catalog")
|
|
print("private_node_identity=node-id-or-exact-display-name")
|
|
print("provider_credential_values=preserved")
|
|
for changed_path in ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES:
|
|
print(f"engine_provider_authority_changed_path={changed_path}")
|
|
print(
|
|
f"engine_provider_authority_predecessor_sha256[{changed_path}]="
|
|
f"{provider_authority_diagnostics_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
f"engine_provider_authority_target_sha256[{changed_path}]="
|
|
f"{provider_authority_diagnostics_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(f"backend_current_barrier={provider_authority_diagnostics_preflight['backend_mode']}")
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
if depttrans_zone_authority_v1_preflight:
|
|
print_engine_depttrans_zone_authority_v1_plan(
|
|
depttrans_zone_authority_v1_preflight
|
|
)
|
|
if provider_target_host_policy_preflight:
|
|
print("engine_provider_transport_transition=exact-literal-target-host")
|
|
print("engine_provider_package=gelios.provider.v4")
|
|
print("engine_data_product=fleet.positions.current.v3")
|
|
print("provider_credential_values=preserved")
|
|
for changed_path in ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES:
|
|
print(f"engine_provider_target_host_policy_changed_path={changed_path}")
|
|
print(
|
|
f"engine_provider_target_host_policy_predecessor_sha256[{changed_path}]="
|
|
f"{provider_target_host_policy_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
f"engine_provider_target_host_policy_target_sha256[{changed_path}]="
|
|
f"{provider_target_host_policy_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(f"backend_current_barrier={provider_target_host_policy_preflight['backend_mode']}")
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
if mcp_execution_profile_decoder_preflight:
|
|
print(
|
|
"engine_mcp_observability_transition="
|
|
"exact-flatted-numeric-string-preservation"
|
|
)
|
|
print("engine_mcp_surface=external-codex")
|
|
print("engine_mcp_tool=engine_get_node_output_profile")
|
|
print("engine_mcp_profile_values_included=no")
|
|
print("engine_mcp_raw_execution_data_included=no")
|
|
for changed_path in ENGINE_MCP_EXECUTION_PROFILE_DECODER_ARTIFACT_ENTRIES:
|
|
print(f"engine_mcp_profile_decoder_changed_path={changed_path}")
|
|
if changed_path in mcp_execution_profile_decoder_preflight[
|
|
"predecessor_sha256"
|
|
]:
|
|
print(
|
|
f"engine_mcp_profile_decoder_predecessor_sha256[{changed_path}]="
|
|
f"{mcp_execution_profile_decoder_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_execution_profile_decoder_preflight["new_paths"]:
|
|
print(
|
|
f"engine_mcp_profile_decoder_predecessor_state[{changed_path}]="
|
|
"absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP execution profile decoder plan has no predecessor "
|
|
f"state: {changed_path}"
|
|
)
|
|
print(
|
|
f"engine_mcp_profile_decoder_target_sha256[{changed_path}]="
|
|
f"{mcp_execution_profile_decoder_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_execution_profile_decoder_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_telemetry_catalog_preflight:
|
|
print(
|
|
"engine_mcp_observability_transition="
|
|
f"{mcp_telemetry_catalog_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.8.0")
|
|
print("engine_mcp_surface=external-codex")
|
|
print("engine_mcp_tool=engine_get_telemetry_reading_catalog")
|
|
print("engine_mcp_reading_values_included=no")
|
|
print("engine_mcp_raw_execution_data_included=no")
|
|
print("node_intelligence_descriptor=attested-gateway-successor")
|
|
print("node_intelligence_release=2.33.2-974a9fb3492f")
|
|
print("node_intelligence_image=preserved")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_telemetry_catalog_preflight["foundation_sha256"].items()
|
|
):
|
|
print(
|
|
f"engine_mcp_telemetry_catalog_foundation_sha256[{foundation_path}]="
|
|
f"{foundation_sha256}"
|
|
)
|
|
for changed_path in ENGINE_MCP_TELEMETRY_CATALOG_ARTIFACT_ENTRIES:
|
|
print(f"engine_mcp_telemetry_catalog_changed_path={changed_path}")
|
|
if changed_path in mcp_telemetry_catalog_preflight[
|
|
"predecessor_sha256"
|
|
]:
|
|
print(
|
|
f"engine_mcp_telemetry_catalog_predecessor_sha256[{changed_path}]="
|
|
f"{mcp_telemetry_catalog_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_telemetry_catalog_preflight["new_paths"]:
|
|
print(
|
|
f"engine_mcp_telemetry_catalog_predecessor_state[{changed_path}]="
|
|
"absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP telemetry catalog plan has no predecessor state: "
|
|
f"{changed_path}"
|
|
)
|
|
print(
|
|
f"engine_mcp_telemetry_catalog_target_sha256[{changed_path}]="
|
|
f"{mcp_telemetry_catalog_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_telemetry_catalog_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_execution_plan_materialization_preflight:
|
|
print(
|
|
"engine_mcp_authoring_transition="
|
|
f"{mcp_execution_plan_materialization_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.9.0")
|
|
print("engine_mcp_surface=external-codex")
|
|
print(
|
|
"engine_mcp_tool="
|
|
"engine_plan_l2_execution_plan_materialization"
|
|
)
|
|
print(
|
|
"engine_mcp_tool="
|
|
"engine_apply_l2_execution_plan_materialization"
|
|
)
|
|
print("engine_mcp_provider_logic_authority=trusted-provider-package")
|
|
print("engine_mcp_unmanaged_graph_policy=explicit-adoption-required")
|
|
print("engine_mcp_plan_phase=read-only")
|
|
print("engine_mcp_apply_phase=opaque-plan-ref-only")
|
|
print("node_intelligence_descriptor=attested-gateway-successor")
|
|
print("node_intelligence_release=2.33.2-974a9fb3492f")
|
|
print("node_intelligence_image=preserved")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_execution_plan_materialization_preflight[
|
|
"foundation_sha256"
|
|
].items()
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_materialization_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_ARTIFACT_ENTRIES
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_materialization_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in mcp_execution_plan_materialization_preflight[
|
|
"predecessor_sha256"
|
|
]:
|
|
print(
|
|
"engine_mcp_execution_plan_materialization_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_execution_plan_materialization_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_execution_plan_materialization_preflight[
|
|
"new_paths"
|
|
]:
|
|
print(
|
|
"engine_mcp_execution_plan_materialization_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP execution plan materialization plan has no "
|
|
f"predecessor state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_execution_plan_materialization_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_execution_plan_materialization_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_execution_plan_materialization_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_execution_plan_telemetry_runtime_preflight:
|
|
print(
|
|
"engine_mcp_authoring_runtime_transition="
|
|
f"{mcp_execution_plan_telemetry_runtime_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.9.0")
|
|
print("engine_mcp_surface=external-codex")
|
|
print("engine_mcp_compiler_predecessor=1.1.0")
|
|
print("engine_mcp_compiler_supported=1.1.0,1.2.0")
|
|
print("engine_mcp_legacy_runtime_preserved=yes")
|
|
print("engine_mcp_provider_logic_authority=trusted-provider-package")
|
|
print(
|
|
"engine_mcp_telemetry_authority="
|
|
"trusted-projection+visible-declared-sensor"
|
|
)
|
|
print("engine_mcp_unprojected_parameters=discarded")
|
|
print("engine_mcp_raw_provider_payload_at_publish=forbidden")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_execution_plan_telemetry_runtime_preflight[
|
|
"foundation_sha256"
|
|
].items()
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_telemetry_runtime_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_ARTIFACT_ENTRIES
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_telemetry_runtime_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in mcp_execution_plan_telemetry_runtime_preflight[
|
|
"predecessor_sha256"
|
|
]:
|
|
print(
|
|
"engine_mcp_execution_plan_telemetry_runtime_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_execution_plan_telemetry_runtime_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_execution_plan_telemetry_runtime_preflight[
|
|
"new_paths"
|
|
]:
|
|
print(
|
|
"engine_mcp_execution_plan_telemetry_runtime_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime plan has no "
|
|
f"predecessor state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_execution_plan_telemetry_runtime_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_execution_plan_telemetry_runtime_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_execution_plan_telemetry_runtime_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("node_intelligence_image=preserved")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_execution_plan_module_ownership_preflight:
|
|
print(
|
|
"engine_mcp_module_ownership_transition="
|
|
f"{mcp_execution_plan_module_ownership_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.10.0")
|
|
print("engine_mcp_surface=external-codex")
|
|
print(
|
|
"engine_mcp_materialization_strategies="
|
|
"create_or_reconcile_owned,adopt_existing,adopt_existing_module"
|
|
)
|
|
print("engine_mcp_module_node_bindings=exact-one-to-one")
|
|
print("engine_mcp_module_retirement_boundary=closed")
|
|
print("engine_mcp_shared_manual_webhook=compatible-config-preserved")
|
|
print("engine_mcp_provider_credential_nodes=engine-managed")
|
|
print("engine_mcp_publisher_nodes=engine-managed")
|
|
print("engine_provider_package_added=gelios.provider.v9")
|
|
print("engine_provider_package_legacy=gelios.provider.v8:preserved")
|
|
print("node_intelligence_descriptor=attested-gateway-successor")
|
|
print("node_intelligence_release=2.33.2-974a9fb3492f")
|
|
print("node_intelligence_image=preserved")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_execution_plan_module_ownership_preflight[
|
|
"foundation_sha256"
|
|
].items()
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_module_ownership_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_ARTIFACT_ENTRIES
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_module_ownership_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in mcp_execution_plan_module_ownership_preflight[
|
|
"predecessor_sha256"
|
|
]:
|
|
print(
|
|
"engine_mcp_execution_plan_module_ownership_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_execution_plan_module_ownership_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_execution_plan_module_ownership_preflight[
|
|
"new_paths"
|
|
]:
|
|
print(
|
|
"engine_mcp_execution_plan_module_ownership_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP execution plan module ownership plan has no "
|
|
f"predecessor state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_execution_plan_module_ownership_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_execution_plan_module_ownership_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_execution_plan_module_ownership_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_normalized_identity_search_preflight:
|
|
print(
|
|
"engine_mcp_identity_transition="
|
|
f"{mcp_normalized_identity_search_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.11.0")
|
|
print("engine_mcp_surface=external-codex")
|
|
print("engine_mcp_tool=engine_find_normalized_subjects")
|
|
print("engine_mcp_identity_values=full-authorized-admin")
|
|
print("engine_mcp_search_authority=canonical-normalized-facts")
|
|
print("engine_mcp_raw_execution_data_included=no")
|
|
print("engine_mcp_raw_provider_payload_included=no")
|
|
print("engine_mcp_command_surface_included=no")
|
|
print("engine_mcp_compiler_added=1.4.0")
|
|
print("engine_mcp_legacy_compilers=1.1.0,1.2.0,1.3.0:preserved")
|
|
print("engine_provider_package_added=gelios.provider.v11")
|
|
print("engine_data_product_added=fleet.units.identity.current.v1")
|
|
print("node_intelligence_descriptor=attested-gateway-successor")
|
|
print("node_intelligence_release=2.33.2-974a9fb3492f")
|
|
print("node_intelligence_image=preserved")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_normalized_identity_search_preflight[
|
|
"foundation_sha256"
|
|
].items()
|
|
):
|
|
print(
|
|
"engine_mcp_normalized_identity_search_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_ARTIFACT_ENTRIES
|
|
):
|
|
print(
|
|
"engine_mcp_normalized_identity_search_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in mcp_normalized_identity_search_preflight[
|
|
"predecessor_sha256"
|
|
]:
|
|
print(
|
|
"engine_mcp_normalized_identity_search_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_normalized_identity_search_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_normalized_identity_search_preflight[
|
|
"new_paths"
|
|
]:
|
|
print(
|
|
"engine_mcp_normalized_identity_search_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP normalized identity search plan has no "
|
|
f"predecessor state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_normalized_identity_search_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_normalized_identity_search_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_normalized_identity_search_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_l1_credential_reuse_preflight:
|
|
print(
|
|
"engine_mcp_l1_credential_reuse_transition="
|
|
f"{mcp_l1_credential_reuse_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.11.0")
|
|
print("engine_mcp_surface=external-codex")
|
|
print("engine_mcp_tool=engine_list_l2_credential_refs")
|
|
print("engine_mcp_apply_operation=assignCredentialRef")
|
|
print("engine_mcp_credential_scope=same-l1-workflow")
|
|
print("engine_mcp_visibility_proof=local-registry-provenance")
|
|
print("engine_mcp_cross_l1_sharing=no")
|
|
print("engine_mcp_managed_grants=target-local")
|
|
print("engine_mcp_native_credential_ids_included=no")
|
|
print("engine_mcp_logical_credential_ids_included=no")
|
|
print("engine_mcp_credential_values_included=no")
|
|
print("engine_mcp_transport_https_required=yes")
|
|
print("engine_mcp_transport_redirects_disabled=yes")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_l1_credential_reuse_preflight["foundation_sha256"].items()
|
|
):
|
|
print(
|
|
"engine_mcp_l1_credential_reuse_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in ENGINE_MCP_L1_CREDENTIAL_REUSE_ARTIFACT_ENTRIES:
|
|
print(
|
|
"engine_mcp_l1_credential_reuse_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in mcp_l1_credential_reuse_preflight[
|
|
"predecessor_sha256"
|
|
]:
|
|
print(
|
|
"engine_mcp_l1_credential_reuse_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_l1_credential_reuse_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_l1_credential_reuse_preflight["new_paths"]:
|
|
print(
|
|
"engine_mcp_l1_credential_reuse_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP L1 credential reuse plan has no predecessor "
|
|
f"state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_l1_credential_reuse_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_l1_credential_reuse_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_l1_credential_reuse_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_l1_credential_provenance_preflight:
|
|
print(
|
|
"engine_mcp_l1_credential_provenance_transition="
|
|
f"{mcp_l1_credential_provenance_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.11.0")
|
|
print("engine_mcp_surface=external-codex")
|
|
print("engine_mcp_tool=engine_list_l2_credential_refs")
|
|
print("engine_mcp_apply_operation=assignCredentialRef")
|
|
print("engine_mcp_credential_scope=same-l1-workflow")
|
|
print(
|
|
"engine_mcp_local_provenance_sources="
|
|
"manual,workflow-ref,credentials-file"
|
|
)
|
|
print("engine_mcp_referenced_source_requires_sync_payload=yes")
|
|
print("engine_mcp_logical_key_equality_required=yes")
|
|
print("engine_mcp_cross_l1_sharing=no")
|
|
print("engine_mcp_managed_grants=target-local")
|
|
print("engine_mcp_credential_ids_included=no")
|
|
print("engine_mcp_credential_values_included=no")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_l1_credential_provenance_preflight[
|
|
"foundation_sha256"
|
|
].items()
|
|
):
|
|
print(
|
|
"engine_mcp_l1_credential_provenance_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_ARTIFACT_ENTRIES
|
|
):
|
|
print(
|
|
"engine_mcp_l1_credential_provenance_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in mcp_l1_credential_provenance_preflight[
|
|
"predecessor_sha256"
|
|
]:
|
|
print(
|
|
"engine_mcp_l1_credential_provenance_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_l1_credential_provenance_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_l1_credential_provenance_preflight[
|
|
"new_paths"
|
|
]:
|
|
print(
|
|
"engine_mcp_l1_credential_provenance_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP L1 credential provenance plan has no "
|
|
f"predecessor state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_l1_credential_provenance_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_l1_credential_provenance_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_l1_credential_provenance_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_execution_plan_sandbox_runtime_preflight:
|
|
print(
|
|
"engine_mcp_execution_plan_sandbox_runtime_transition="
|
|
f"{mcp_execution_plan_sandbox_runtime_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.11.0")
|
|
print("engine_mcp_execution_plan_compiler_version=1.4.0")
|
|
print("engine_mcp_execution_runtime=n8n-2.3.2-code")
|
|
print("engine_mcp_execution_sandbox_global_dependencies=none")
|
|
print("engine_mcp_execution_byte_budget=pure-js-utf8")
|
|
print("engine_mcp_provider_logic=trusted-package")
|
|
print("engine_mcp_provider_hardcode=no")
|
|
print("engine_mcp_raw_provider_publish=forbidden")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_execution_plan_sandbox_runtime_preflight[
|
|
"foundation_sha256"
|
|
].items()
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_sandbox_runtime_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_ARTIFACT_ENTRIES
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_sandbox_runtime_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in (
|
|
mcp_execution_plan_sandbox_runtime_preflight[
|
|
"predecessor_sha256"
|
|
]
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_sandbox_runtime_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_execution_plan_sandbox_runtime_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in (
|
|
mcp_execution_plan_sandbox_runtime_preflight["new_paths"]
|
|
):
|
|
print(
|
|
"engine_mcp_execution_plan_sandbox_runtime_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime plan has no "
|
|
f"predecessor state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_execution_plan_sandbox_runtime_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_execution_plan_sandbox_runtime_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_execution_plan_sandbox_runtime_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("n8n_core=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_gelios_items_envelope_preflight:
|
|
print(
|
|
"engine_mcp_gelios_items_envelope_transition="
|
|
f"{mcp_gelios_items_envelope_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.11.0")
|
|
print("engine_mcp_provider_package=gelios.provider.v12")
|
|
print("engine_mcp_response_collection_path=items")
|
|
print("engine_mcp_execution_history_v11=preserved")
|
|
print("engine_mcp_security_authority_v12=exclusive")
|
|
print("engine_mcp_provider_logic=trusted-package")
|
|
print("engine_mcp_compiler_changed=no")
|
|
print("engine_mcp_raw_provider_values_included=no")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_gelios_items_envelope_preflight[
|
|
"foundation_sha256"
|
|
].items()
|
|
):
|
|
print(
|
|
"engine_mcp_gelios_items_envelope_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_ARTIFACT_ENTRIES
|
|
):
|
|
print(
|
|
"engine_mcp_gelios_items_envelope_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in (
|
|
mcp_gelios_items_envelope_preflight["predecessor_sha256"]
|
|
):
|
|
print(
|
|
"engine_mcp_gelios_items_envelope_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_gelios_items_envelope_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in (
|
|
mcp_gelios_items_envelope_preflight["new_paths"]
|
|
):
|
|
print(
|
|
"engine_mcp_gelios_items_envelope_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP Gelios items envelope plan has no predecessor "
|
|
f"state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_gelios_items_envelope_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_gelios_items_envelope_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_gelios_items_envelope_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("n8n_core=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_registered_execution_profiles_preflight:
|
|
print(
|
|
"engine_mcp_registered_execution_profiles_transition="
|
|
f"{mcp_registered_execution_profiles_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.12.0")
|
|
print(
|
|
"engine_mcp_tool=engine_list_l2_execution_profiles,"
|
|
"engine_plan_registered_l2_execution"
|
|
)
|
|
print("engine_mcp_registered_profiles=6")
|
|
print("engine_mcp_profile_refs=actor-and-target-scoped-opaque-ttl")
|
|
print("engine_mcp_target_scope=server-derived")
|
|
print("engine_mcp_execution_plan_digests=server-derived")
|
|
print("engine_mcp_existing_materializer=reused")
|
|
print("engine_mcp_provider_hardcode=no")
|
|
print("engine_mcp_internal_descriptors_included=no")
|
|
print("engine_mcp_provider_endpoints_included=no")
|
|
print("engine_mcp_customer_scope_included=no")
|
|
print("engine_mcp_credential_ids_included=no")
|
|
print("engine_mcp_credential_values_included=no")
|
|
print("engine_node_intelligence_attestation=updated")
|
|
print("engine_node_intelligence_image=untouched")
|
|
print("engine_node_intelligence_source=untouched")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_registered_execution_profiles_preflight[
|
|
"foundation_sha256"
|
|
].items()
|
|
):
|
|
print(
|
|
"engine_mcp_registered_execution_profiles_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in (
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES
|
|
):
|
|
print(
|
|
"engine_mcp_registered_execution_profiles_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in (
|
|
mcp_registered_execution_profiles_preflight[
|
|
"predecessor_sha256"
|
|
]
|
|
):
|
|
print(
|
|
"engine_mcp_registered_execution_profiles_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_registered_execution_profiles_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in (
|
|
mcp_registered_execution_profiles_preflight["new_paths"]
|
|
):
|
|
print(
|
|
"engine_mcp_registered_execution_profiles_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP registered execution profiles plan has no "
|
|
f"predecessor state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_registered_execution_profiles_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_registered_execution_profiles_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_registered_execution_profiles_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("n8n_core=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("foundry=untouched")
|
|
print("ontology=untouched")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if mcp_gelios_units_items_preflight:
|
|
print(
|
|
"engine_mcp_gelios_units_items_transition="
|
|
f"{mcp_gelios_units_items_preflight['mode']}"
|
|
)
|
|
print("engine_mcp_version=0.12.0")
|
|
print("engine_mcp_provider_package=gelios.provider.v12@12.0.1")
|
|
print("engine_mcp_registered_profile=gelios.units.profile.cold.v1")
|
|
print("engine_mcp_data_product=fleet.units.profile.current.v1")
|
|
print("engine_mcp_response_collection_path=items")
|
|
print("engine_mcp_execution_plan_compiler_version=1.4.0")
|
|
print("engine_mcp_existing_materializer=reused")
|
|
print("engine_mcp_runtime_code_changed=no")
|
|
print("engine_mcp_provider_endpoints_changed=no")
|
|
print("engine_mcp_raw_provider_values_included=no")
|
|
for foundation_path, foundation_sha256 in (
|
|
mcp_gelios_units_items_preflight["foundation_sha256"].items()
|
|
):
|
|
print(
|
|
"engine_mcp_gelios_units_items_foundation_sha256"
|
|
f"[{foundation_path}]={foundation_sha256}"
|
|
)
|
|
for changed_path in ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES:
|
|
print(
|
|
"engine_mcp_gelios_units_items_changed_path="
|
|
f"{changed_path}"
|
|
)
|
|
if changed_path in (
|
|
mcp_gelios_units_items_preflight["predecessor_sha256"]
|
|
):
|
|
print(
|
|
"engine_mcp_gelios_units_items_predecessor_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_gelios_units_items_preflight['predecessor_sha256'][changed_path]}"
|
|
)
|
|
elif changed_path in mcp_gelios_units_items_preflight["new_paths"]:
|
|
print(
|
|
"engine_mcp_gelios_units_items_predecessor_state"
|
|
f"[{changed_path}]=absent"
|
|
)
|
|
else:
|
|
die(
|
|
"Engine MCP Gelios units items plan has no predecessor "
|
|
f"state: {changed_path}"
|
|
)
|
|
print(
|
|
"engine_mcp_gelios_units_items_target_sha256"
|
|
f"[{changed_path}]="
|
|
f"{mcp_gelios_units_items_preflight['target_sha256'][changed_path]}"
|
|
)
|
|
print(
|
|
"backend_current_barrier="
|
|
f"{mcp_gelios_units_items_preflight['backend_mode']}"
|
|
)
|
|
print("backend_force_recreate=yes")
|
|
print("backend_pull=never")
|
|
print("l2_graph=untouched")
|
|
print("n8n_l1=untouched")
|
|
print("n8n_core=untouched")
|
|
print("engine_ui=untouched")
|
|
print("engine_databases=untouched")
|
|
print("credentials=preserved")
|
|
print("foundry=untouched")
|
|
print("ontology=untouched")
|
|
print("mcp_nginx=untouched")
|
|
print("embedded_ai_workspace=untouched")
|
|
if transition_descriptor:
|
|
print(f"n8n_transition={transition_descriptor['action']}")
|
|
print(f"n8n_version={transition_descriptor['n8nVersion']}")
|
|
print(f"n8n_release={transition_descriptor['releaseId']}")
|
|
print(f"n8n_package_sha256={transition_descriptor['packageSha256']}")
|
|
print(f"n8n_base_image={transition_descriptor['baseImage']}")
|
|
print(f"n8n_base_image_id={transition_preflight['base_image_id']}")
|
|
print(f"n8n_base_image_architecture={transition_preflight['base_image_architecture']}")
|
|
for repo_digest in transition_preflight["base_image_repo_digests"]:
|
|
print(f"n8n_base_image_repo_digest={repo_digest}")
|
|
print(f"n8n_current_state={transition_preflight['current_state']}")
|
|
print(f"n8n_expected_current={transition_descriptor['expectedCurrent']}")
|
|
print(f"n8n_state_matches={'yes' if transition_preflight['state_matches'] else 'not-yet'}")
|
|
print(f"n8n_sealed_package=/volume2/nodedc-demo/{transition_descriptor['sealedReleaseRelativePath']}")
|
|
print("n8n_build=none")
|
|
print("n8n_pull=never")
|
|
print("n8n_force_recreate=yes")
|
|
print("n8n_persistent_data=preserved:/volume2/nodedc-demo/n8n-data")
|
|
print("n8n_database=preserved:n8n-postgres")
|
|
print(f"n8n_expected_node_types={','.join(transition_descriptor['expectedNodeTypes'])}")
|
|
print(f"n8n_expected_credential_types={','.join(transition_descriptor['expectedCredentialTypes'])}")
|
|
print(f"n8n_rollback_baseline={transition_descriptor['rollbackBaseline']}")
|
|
touches_map_gateway = component == "platform" and any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries)
|
|
touches_external_data_plane = component == "platform" and touches_external_data_plane_files(entries)
|
|
if component == "module-foundry" or touches_map_gateway:
|
|
print(f"runtime_secret=runner-managed:{MAP_GATEWAY_SECRET_FILE}")
|
|
if component == "module-foundry":
|
|
print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}")
|
|
print(f"runtime_grants=runner-managed:{FOUNDRY_BINDING_GRANTS_DIR}")
|
|
print(f"runtime_private_key=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}")
|
|
print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}")
|
|
if component == "device-plane":
|
|
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_POSTGRES_PASSWORD_FILE}")
|
|
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE}")
|
|
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_IDENTIFIER_PEPPER_FILE}")
|
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
|
print(
|
|
"runtime_secret=runner-managed:"
|
|
f"{DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE}"
|
|
)
|
|
print(
|
|
"runtime_secret=runner-managed:"
|
|
f"{PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE}"
|
|
)
|
|
if (
|
|
is_device_plane_control_core_release_slice(component, entries)
|
|
or is_device_plane_edge_core_channel_bootstrap_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
):
|
|
print(
|
|
"runtime_secret=runner-managed:"
|
|
f"{DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE}"
|
|
)
|
|
print(
|
|
"runtime_private_key=runner-managed-host-local:"
|
|
f"{DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE}"
|
|
)
|
|
print(
|
|
"runtime_public_certificate_export=runner-managed:"
|
|
f"{DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE}"
|
|
)
|
|
print(
|
|
"device_postgres="
|
|
f"{device_plane_postgres_plan_selection(device_plane_postgres_preflight)}"
|
|
)
|
|
print("device_postgres_volume=preserved:nodedc-device-plane-postgres-data")
|
|
if device_plane_b2_ingress_preflight is not None:
|
|
print("device_gateway_public_ingress=disabled:loopback-test:tcp:9921")
|
|
print("device_control_core_discovery_ingest=enabled:authenticated")
|
|
else:
|
|
print("device_gateway_public_ingress=disabled")
|
|
if (
|
|
device_plane_manager_activation_preflight is not None
|
|
and device_plane_manager_activation_preflight["descriptor"].get(
|
|
"commandTransport"
|
|
) == "typed-service-ping-v1"
|
|
):
|
|
print("device_gateway_command_transport=typed-service-ping-v1")
|
|
else:
|
|
print("device_gateway_command_transport=disabled")
|
|
print("gelios=untouched")
|
|
if device_plane_foundation_recovery_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_foundation_recovery_preflight['mode']}"
|
|
)
|
|
print(
|
|
"failed_patch="
|
|
f"{DEVICE_PLANE_FOUNDATION_FAILED_PATCH_ID}"
|
|
)
|
|
print(
|
|
"failed_artifact_sha256="
|
|
f"{DEVICE_PLANE_FOUNDATION_FAILED_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"recovery_backup="
|
|
f"{device_plane_foundation_recovery_preflight['backup'].name}"
|
|
)
|
|
print("device_plane_build=none")
|
|
print("device_plane_runtime_mutation=none")
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-control-core,device-gateway,device-postgres"
|
|
)
|
|
print("device_plane_source_action=publish-exact-failed-source")
|
|
print("device_plane_rollback=source-only-runtime-unchanged")
|
|
if device_plane_network_publication_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_network_publication_preflight['mode']}"
|
|
)
|
|
print(
|
|
"failed_recovery_patch="
|
|
f"{DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_PATCH_ID}"
|
|
)
|
|
print(
|
|
"failed_recovery_artifact_sha256="
|
|
f"{DEVICE_PLANE_FOUNDATION_RECOVERY_FAILED_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"failed_recovery_backup="
|
|
f"{device_plane_network_publication_preflight['backup'].name}"
|
|
)
|
|
print("device_plane_build=none")
|
|
print(
|
|
"device_plane_runtime_mutation="
|
|
"recreate:device-control-core,device-gateway"
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-postgres"
|
|
)
|
|
print(
|
|
"device_plane_private_network="
|
|
"preserved:internal:true"
|
|
)
|
|
print(
|
|
"device_plane_control_network="
|
|
"create:internal:false:masquerade:false"
|
|
)
|
|
print(
|
|
"device_plane_actual_ports="
|
|
"required:127.0.0.1:18120,127.0.0.1:18121"
|
|
)
|
|
print("device_gateway_tcp_9921=disabled:unpublished")
|
|
print(
|
|
"device_plane_source_action="
|
|
"publish-network-corrected-foundation-source"
|
|
)
|
|
print(
|
|
"device_plane_rollback="
|
|
"partial-source+internal-only-stateless-runtime"
|
|
)
|
|
if device_plane_b2_ingress_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_b2_ingress_preflight['mode']}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_patch="
|
|
f"{DEVICE_PLANE_B2_DISCOVERY_INGRESS_PREDECESSOR_PATCH_ID}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_artifact_sha256="
|
|
f"{DEVICE_PLANE_B2_DISCOVERY_INGRESS_PREDECESSOR_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"device_plane_runtime_mutation="
|
|
"build+recreate:device-control-core,device-gateway"
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-postgres"
|
|
)
|
|
print(
|
|
"device_plane_actual_ports="
|
|
"required:127.0.0.1:18120,127.0.0.1:18121,"
|
|
"127.0.0.1:9921/tcp"
|
|
)
|
|
print(
|
|
"device_gateway_framing="
|
|
"verified-read-only:"
|
|
"arusnavi.internal.protocol-sheet.gid-12.v1"
|
|
)
|
|
print(
|
|
"device_gateway_identity="
|
|
"header2-imei:claimed-not-ownership-proof"
|
|
)
|
|
print("device_gateway_discovery_lifecycle=quarantine")
|
|
print(
|
|
"device_plane_rollback="
|
|
"source+predecessor-stateless-runtime"
|
|
)
|
|
if device_plane_b2_recovery_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_b2_recovery_preflight['mode']}"
|
|
)
|
|
print(
|
|
"failed_patch="
|
|
f"{DEVICE_PLANE_B2_DISCOVERY_FAILED_PATCH_ID}"
|
|
)
|
|
print(
|
|
"failed_artifact_sha256="
|
|
f"{DEVICE_PLANE_B2_DISCOVERY_FAILED_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"recovery_backup="
|
|
f"{device_plane_b2_recovery_preflight['backup'].name}"
|
|
)
|
|
print("device_plane_build=none")
|
|
print("device_plane_runtime_mutation=none")
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-control-core,device-gateway,device-postgres"
|
|
)
|
|
print(
|
|
"device_plane_source_action="
|
|
"publish-reconciliation-marker-only"
|
|
)
|
|
print("device_gateway_tcp_9921=disabled:unpublished")
|
|
print("device_plane_rollback=marker-only-runtime-unchanged")
|
|
if device_plane_manager_activation_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_manager_activation_preflight['mode']}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_patch="
|
|
f"{device_plane_manager_activation_preflight['descriptor']['predecessor']['patchId']}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_artifact_sha256="
|
|
f"{device_plane_manager_activation_preflight['descriptor']['predecessor']['artifactSha256']}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_kind="
|
|
f"{device_plane_manager_activation_preflight['descriptor']['predecessor']['kind']}"
|
|
)
|
|
print(
|
|
"device_plane_runtime_mutation="
|
|
+ (
|
|
"build+recreate:device-manager"
|
|
if device_plane_manager_activation_preflight["descriptor"].get(
|
|
"schemaVersion"
|
|
) in (
|
|
"nodedc.device-plane.device-manager-release.v3",
|
|
"nodedc.device-plane.device-manager-release.v4",
|
|
"nodedc.device-plane.device-manager-release.v5",
|
|
"nodedc.device-plane.device-manager-release.v6",
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
)
|
|
else "build+recreate:device-control-core,device-manager"
|
|
)
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
+ (
|
|
"preserved:device-control-core,device-gateway,"
|
|
"device-postgres,device-backhaul-target"
|
|
if device_plane_manager_activation_preflight["descriptor"].get(
|
|
"schemaVersion"
|
|
) in (
|
|
"nodedc.device-plane.device-manager-release.v3",
|
|
"nodedc.device-plane.device-manager-release.v4",
|
|
"nodedc.device-plane.device-manager-release.v5",
|
|
"nodedc.device-plane.device-manager-release.v6",
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
)
|
|
else "preserved:device-gateway,device-postgres,"
|
|
"device-backhaul-target"
|
|
)
|
|
)
|
|
manager_schema = device_plane_manager_activation_preflight[
|
|
"descriptor"
|
|
].get("schemaVersion")
|
|
manager_persistent = manager_schema in (
|
|
"nodedc.device-plane.device-manager-release.v4",
|
|
"nodedc.device-plane.device-manager-release.v5",
|
|
"nodedc.device-plane.device-manager-release.v6",
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
)
|
|
print(
|
|
"device_manager_health_gate="
|
|
+ (
|
|
"bounded-grace+contract+persistent-data"
|
|
if manager_persistent
|
|
else "bounded-grace+contract"
|
|
)
|
|
)
|
|
if manager_persistent:
|
|
print(
|
|
"device_manager_persistent_data=runner-managed-preserved:"
|
|
f"{DEVICE_PLANE_MANAGER_DATA_DIR}"
|
|
)
|
|
print(
|
|
"device_manager_persistent_mount=read-write:"
|
|
f"{DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}"
|
|
)
|
|
print("device_manager_default_accent=#f5f5f5")
|
|
if manager_schema in (
|
|
"nodedc.device-plane.device-manager-release.v5",
|
|
"nodedc.device-plane.device-manager-release.v6",
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
):
|
|
print(
|
|
"device_manager_overview_layout="
|
|
"mission-core-landing-stage-v1"
|
|
)
|
|
if manager_schema in (
|
|
"nodedc.device-plane.device-manager-release.v6",
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
):
|
|
print("device_manager_favicon_set=nodedc-adaptive-v1")
|
|
if manager_schema in (
|
|
"nodedc.device-plane.device-manager-release.v7",
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
):
|
|
print("device_manager_command_form=aligned-control-row-v1")
|
|
print("device_manager_secondary_empty_text=help-text-sm-v1")
|
|
if manager_schema == (
|
|
"nodedc.device-plane.device-manager-release.v7"
|
|
):
|
|
print(
|
|
"device_manager_infrastructure_hosts="
|
|
"edge-registration-live-channel-v1"
|
|
)
|
|
print(
|
|
"device_manager_host_ontology="
|
|
"candidate-not-canonical"
|
|
)
|
|
if manager_schema in (
|
|
"nodedc.device-plane.device-manager-release.v8",
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
):
|
|
print(
|
|
"device_manager_infrastructure_hosts="
|
|
"ontology-backed-host-runtime-v1"
|
|
)
|
|
print(
|
|
"device_manager_ontology_foundation="
|
|
"ontology-core-device-foundation-20260822-001"
|
|
)
|
|
print(
|
|
"device_manager_ontology_catalog_hash="
|
|
"229c61c02a790906"
|
|
)
|
|
print(
|
|
"device_manager_asset_binding="
|
|
"temporal-device-asset-binding-v1"
|
|
)
|
|
print(
|
|
"device_manager_infrastructure_runtime="
|
|
"host-endpoint-deployment-service-instance-v1"
|
|
)
|
|
print(
|
|
"device_manager_health_evidence="
|
|
"ttl-observation-missing-not-unhealthy-v1"
|
|
)
|
|
print(
|
|
"device_manager_interactive_shell="
|
|
"disabled-pending-managed-session-boundary"
|
|
)
|
|
if manager_schema in (
|
|
"nodedc.device-plane.device-manager-release.v9",
|
|
"nodedc.device-plane.device-manager-release.v10",
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
):
|
|
print(
|
|
"device_manager_host_telemetry_workspace="
|
|
+ device_plane_manager_activation_preflight[
|
|
"descriptor"
|
|
]["telemetryWorkspace"]
|
|
)
|
|
print(
|
|
"device_manager_host_telemetry_navigation="
|
|
"full-workspace-back-navigation-v1"
|
|
)
|
|
print("device_manager_host_telemetry_poll=three-seconds")
|
|
print(
|
|
"device_manager_host_telemetry_freshness="
|
|
"fifteen-seconds-missing-stale-not-unhealthy"
|
|
)
|
|
if manager_schema in (
|
|
"nodedc.device-plane.device-manager-release.v11",
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
):
|
|
manager_descriptor = (
|
|
device_plane_manager_activation_preflight["descriptor"]
|
|
)
|
|
print(
|
|
"device_manager_design_system="
|
|
+ manager_descriptor["designSystem"]
|
|
)
|
|
print(
|
|
"device_manager_mission_core_reference="
|
|
+ manager_descriptor["missionCoreReference"]
|
|
)
|
|
print(
|
|
"device_manager_infrastructure_workspace="
|
|
+ manager_descriptor["infrastructureWorkspaceLayout"]
|
|
)
|
|
print(
|
|
"device_manager_host_inventory="
|
|
+ manager_descriptor["hostInventoryComposition"]
|
|
)
|
|
print(
|
|
"device_manager_host_telemetry_surface="
|
|
+ manager_descriptor["telemetrySurface"]
|
|
)
|
|
print(
|
|
"device_manager_host_telemetry_status="
|
|
+ manager_descriptor["telemetryStatus"]
|
|
)
|
|
print(
|
|
"device_manager_host_telemetry_scroll="
|
|
+ manager_descriptor["telemetryScroll"]
|
|
)
|
|
if manager_schema in (
|
|
"nodedc.device-plane.device-manager-release.v12",
|
|
"nodedc.device-plane.device-manager-release.v13",
|
|
):
|
|
manager_descriptor = (
|
|
device_plane_manager_activation_preflight["descriptor"]
|
|
)
|
|
print(
|
|
"device_manager_host_telemetry_graph_scale="
|
|
+ manager_descriptor["telemetryGraphScale"]
|
|
)
|
|
print(
|
|
"device_manager_host_telemetry_cpu_minimum_span="
|
|
+ manager_descriptor["telemetryCpuMinimumSpan"]
|
|
)
|
|
print(
|
|
"device_manager_host_telemetry_memory_minimum_span="
|
|
+ manager_descriptor["telemetryMemoryMinimumSpan"]
|
|
)
|
|
print(
|
|
"device_manager_host_telemetry_network_missing="
|
|
+ manager_descriptor[
|
|
"telemetryNetworkMissingSemantics"
|
|
]
|
|
)
|
|
if manager_schema == (
|
|
"nodedc.device-plane.device-manager-release.v13"
|
|
):
|
|
manager_descriptor = (
|
|
device_plane_manager_activation_preflight["descriptor"]
|
|
)
|
|
print(
|
|
"device_manager_host_inventory_overview_surface="
|
|
+ manager_descriptor["hostInventoryOverviewSurface"]
|
|
)
|
|
print(
|
|
"device_manager_host_inventory_collection_surface="
|
|
+ manager_descriptor["hostInventoryCollectionSurface"]
|
|
)
|
|
print(
|
|
"device_manager_host_inventory_row="
|
|
+ manager_descriptor["hostInventoryRow"]
|
|
)
|
|
print(
|
|
"device_manager_host_inventory_freshness="
|
|
+ manager_descriptor["hostInventoryFreshness"]
|
|
)
|
|
print(
|
|
"device_manager_host_inventory_relations="
|
|
+ manager_descriptor["hostInventoryRelations"]
|
|
)
|
|
print(
|
|
"device_manager_host_inventory_default_expansion="
|
|
+ manager_descriptor["hostInventoryDefaultExpansion"]
|
|
)
|
|
print(
|
|
"device_manager_host_inventory_scale_target="
|
|
+ manager_descriptor["hostInventoryScaleTarget"]
|
|
)
|
|
if device_plane_manager_activation_preflight["descriptor"].get(
|
|
"commandTransport"
|
|
) == "typed-service-ping-v1":
|
|
print("device_manager_public_route=unchanged:active")
|
|
print("device_edge_channel_commands=typed-service-ping-v1")
|
|
print("device_command_credential=transient-core-memory-only")
|
|
print("gelios=untouched:legacy-only")
|
|
else:
|
|
print("device_manager_public_route=unchanged:absent")
|
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
|
print(
|
|
"device_plane_rollback="
|
|
+ (
|
|
"source+reconciled-baseline-runtime+"
|
|
"persistent-manager-data-preserved"
|
|
if manager_persistent
|
|
else "source+reconciled-baseline-runtime"
|
|
)
|
|
)
|
|
if device_plane_control_core_release_preflight is not None:
|
|
core_release = device_plane_control_core_release_preflight
|
|
descriptor = core_release["descriptor"]
|
|
predecessor = descriptor["predecessor"]
|
|
print(
|
|
"device_plane_transition="
|
|
f"{core_release['mode']}"
|
|
)
|
|
print(f"device_control_core_release={descriptor['releaseId']}")
|
|
print(
|
|
"device_plane_predecessor_kind="
|
|
f"{predecessor['kind']}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_patch="
|
|
f"{predecessor['patchId']}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_artifact_sha256="
|
|
f"{predecessor['artifactSha256']}"
|
|
)
|
|
print(
|
|
"device_edge_channel_identity_preflight="
|
|
f"{core_release['identityState']}"
|
|
)
|
|
print("device_plane_runtime_mutation=build+recreate:device-control-core")
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-manager,device-gateway,device-postgres,"
|
|
"device-backhaul-target"
|
|
)
|
|
print("device_edge_channel=preserved:core-initiated:pinned-mtls:registered-edges-only")
|
|
print("device_edge_channel_networks=preserved:device-plane-private,device-plane-egress")
|
|
print("device_edge_channel_registrations=preserved")
|
|
print(
|
|
"device_edge_channel_commands="
|
|
f"{descriptor['commandTransport']}"
|
|
)
|
|
if descriptor.get("schemaVersion") in (
|
|
"nodedc.device-plane.device-control-core-release.v3",
|
|
"nodedc.device-plane.device-control-core-release.v4",
|
|
):
|
|
print(
|
|
"device_host_telemetry_transport="
|
|
"edge-channel-host-telemetry-observed-v1"
|
|
)
|
|
print(
|
|
"device_host_telemetry_storage="
|
|
"device-control-core-postgres-seven-day-retention"
|
|
)
|
|
print(
|
|
"device_host_telemetry_ontology="
|
|
"observation-observed-property-provenance-freshness-v1"
|
|
)
|
|
print(
|
|
"device_host_telemetry_freshness="
|
|
"fifteen-seconds-missing-stale-not-unhealthy"
|
|
)
|
|
if descriptor.get("schemaVersion") == (
|
|
"nodedc.device-plane.device-control-core-release.v4"
|
|
):
|
|
recovery_database = core_release["recoveryDatabase"]
|
|
recovery_runtime = core_release["recoveryRuntime"]
|
|
print(
|
|
"device_control_core_recovery_backup="
|
|
f"{core_release['recoveryBackup'].name}"
|
|
)
|
|
print(
|
|
"device_control_core_current_runtime="
|
|
f"{recovery_runtime['core']['containerId']}:"
|
|
f"{recovery_runtime['core']['imageId']}:"
|
|
f"{recovery_runtime['core']['status']}:"
|
|
f"{recovery_runtime['core']['health']}:"
|
|
f"restarts={recovery_runtime['core']['restartCount']}"
|
|
)
|
|
emit_device_plane_control_core_migration_replay_database_evidence(
|
|
recovery_database
|
|
)
|
|
print("device_postgres_row_mutation=none")
|
|
print("device_postgres_telemetry_table=absent")
|
|
print(
|
|
"device_control_core_target_telemetry_table="
|
|
"migration-017:present"
|
|
)
|
|
print("device_manager=preserved:active")
|
|
print("device_manager_public_route=unchanged:active")
|
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
|
print(f"gelios={descriptor['gelios']}")
|
|
print("device_plane_rollback=source+preapply-core-runtime")
|
|
if device_plane_edge_core_channel_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_edge_core_channel_preflight['mode']}"
|
|
)
|
|
edge_descriptor = (
|
|
device_plane_edge_core_channel_preflight["descriptor"]
|
|
)
|
|
upgrade = edge_descriptor["action"] == "upgrade"
|
|
if upgrade:
|
|
predecessor = edge_descriptor.get(
|
|
"upgradePredecessor",
|
|
edge_descriptor.get("bootstrapPredecessor"),
|
|
)
|
|
print(
|
|
"device_plane_predecessor_patch="
|
|
f"{predecessor['patchId']}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_artifact_sha256="
|
|
f"{predecessor['artifactSha256']}"
|
|
)
|
|
print("device_edge_channel_endpoint_policy=public-ipv4-standard-https-tcp-443-only")
|
|
if "coreNetworks" in edge_descriptor:
|
|
print(
|
|
"device_edge_channel_core_networks="
|
|
f"{','.join(edge_descriptor['coreNetworks'])}"
|
|
)
|
|
print(
|
|
"device_edge_channel_removed_core_network="
|
|
f"{edge_descriptor['removedCoreNetwork']}"
|
|
)
|
|
print(
|
|
"device_edge_channel_compose_compatibility="
|
|
f"{edge_descriptor['composeCompatibility']}"
|
|
)
|
|
else:
|
|
print(
|
|
"device_plane_predecessor_patch="
|
|
f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_PATCH_ID}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_artifact_sha256="
|
|
f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"failed_patch="
|
|
f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_PATCH_ID}"
|
|
)
|
|
print(
|
|
"failed_artifact_sha256="
|
|
f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"recovery_backup="
|
|
f"{DEVICE_PLANE_EDGE_CORE_CHANNEL_FAILED_BACKUP_ID}"
|
|
)
|
|
print(
|
|
"device_edge_channel_identity_preflight="
|
|
f"{device_plane_edge_core_channel_preflight['identityState']}"
|
|
)
|
|
print("device_plane_runtime_mutation=build+recreate:device-control-core")
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-manager,device-gateway,device-postgres,"
|
|
"device-backhaul-target"
|
|
)
|
|
print("device_edge_channel=enabled:core-initiated:pinned-mtls:registered-edges-only")
|
|
print("device_edge_channel_egress=dedicated-core-only-bridge:no-host-ingress")
|
|
print("device_edge_channel_identity=host-local-private-key:public-certificate-export-only")
|
|
if upgrade:
|
|
print("device_edge_channel_identity_recovery=forbidden:reuse-valid-only")
|
|
if edge_descriptor["edgeRegistrations"] == "preserved":
|
|
print("device_edge_channel_registrations=preserved")
|
|
else:
|
|
print("device_edge_channel_registrations=preserved:explicit-443-reconciliation-required")
|
|
else:
|
|
print("device_edge_channel_invalid_failed_016=recover-exact-unexported-only")
|
|
print("device_edge_channel_registrations=preserved")
|
|
print("device_edge_channel_commands=disabled")
|
|
print("device_manager=preserved:active")
|
|
print("device_manager_public_route=unchanged:active")
|
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
|
print("gelios=untouched")
|
|
print("device_plane_rollback=source+predecessor-core-runtime")
|
|
if device_plane_manager_reconciliation_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_manager_reconciliation_preflight['mode']}"
|
|
)
|
|
print(
|
|
"failed_patch="
|
|
f"{DEVICE_PLANE_MANAGER_FAILED_PATCH_ID}"
|
|
)
|
|
print(
|
|
"failed_artifact_sha256="
|
|
f"{DEVICE_PLANE_MANAGER_FAILED_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"recovery_backup="
|
|
f"{device_plane_manager_reconciliation_preflight['backup'].name}"
|
|
)
|
|
print("device_plane_build=none")
|
|
print("device_plane_runtime_mutation=none")
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-control-core,device-gateway,"
|
|
"device-postgres,device-backhaul-target"
|
|
)
|
|
print("device_manager=absent")
|
|
print(
|
|
"device_plane_source_action="
|
|
"publish-reconciliation-marker-only"
|
|
)
|
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
|
print("device_manager_public_route=unchanged:absent")
|
|
print("device_plane_rollback=marker-only-runtime-unchanged")
|
|
if device_plane_manager_v2_reconciliation_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_manager_v2_reconciliation_preflight['mode']}"
|
|
)
|
|
print(
|
|
"failed_patch="
|
|
f"{DEVICE_PLANE_MANAGER_V2_FAILED_PATCH_ID}"
|
|
)
|
|
print(
|
|
"failed_artifact_sha256="
|
|
f"{DEVICE_PLANE_MANAGER_V2_FAILED_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"recovery_backup="
|
|
f"{device_plane_manager_v2_reconciliation_preflight['backup'].name}"
|
|
)
|
|
print(
|
|
"device_plane_failure_class="
|
|
"deterministic-runtime-module-resolution"
|
|
)
|
|
print("device_plane_build=none")
|
|
print("device_plane_runtime_mutation=none")
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-control-core,device-gateway,"
|
|
"device-postgres,device-backhaul-target"
|
|
)
|
|
print("device_manager=absent")
|
|
print(
|
|
"device_plane_source_action="
|
|
"publish-v2-reconciliation-marker-only"
|
|
)
|
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
|
print("device_manager_public_route=unchanged:absent")
|
|
print("device_plane_rollback=marker-only-runtime-unchanged")
|
|
if device_plane_control_core_v3_reconciliation_preflight is not None:
|
|
recovery = device_plane_control_core_v3_reconciliation_preflight
|
|
current_core = recovery["runtime"]["core"]
|
|
print(f"device_plane_transition={recovery['mode']}")
|
|
print(
|
|
"failed_patch="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_V3_FAILED_PATCH_ID}"
|
|
)
|
|
print(
|
|
"failed_artifact_sha256="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_V3_FAILED_ARTIFACT_SHA256}"
|
|
)
|
|
print(f"recovery_backup={recovery['backup'].name}")
|
|
print(
|
|
"device_control_core_current_runtime="
|
|
f"{current_core['status']}:{current_core['health']}"
|
|
)
|
|
print(
|
|
"device_control_core_recovery_image="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID}"
|
|
)
|
|
print("device_plane_build=none")
|
|
print(
|
|
"device_plane_runtime_mutation="
|
|
"retag-exact-preapply-image+recreate:device-control-core"
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-manager,device-gateway,device-postgres,"
|
|
"device-backhaul-target"
|
|
)
|
|
print("device_postgres=preserved:not-selected")
|
|
print(
|
|
"device_postgres_volume="
|
|
"preserved:nodedc-device-plane-postgres-data"
|
|
)
|
|
print("device_gateway_public_ingress=disabled")
|
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
|
print("gelios=untouched-legacy-only")
|
|
print("device_plane_rollback=marker+exact-preapply-image-runtime")
|
|
if device_plane_control_core_incident_audit_preflight is not None:
|
|
audit = device_plane_control_core_incident_audit_preflight
|
|
diagnostics = audit["diagnostics"]
|
|
runtime_by_service = {
|
|
item["service"]: item for item in audit["runtime"]["services"]
|
|
}
|
|
core = runtime_by_service["device-control-core"]
|
|
print(f"device_plane_transition={audit['mode']}")
|
|
print("allowed_operation=canonical-plan-only")
|
|
print("apply=forbidden")
|
|
print("device_plane_build=none")
|
|
print("device_plane_runtime_mutation=none")
|
|
print("device_plane_source_mutation=none")
|
|
print("device_postgres_mutation=none")
|
|
print("device_plane_network_mutation=none")
|
|
print("device_plane_secret_read=none")
|
|
print(f"first_failed_backup={audit['firstBackup'].name}")
|
|
print(f"second_failed_backup={audit['secondBackup'].name}")
|
|
print(
|
|
"device_control_core_runtime="
|
|
f"{core['containerId']}:{core['imageId']}:"
|
|
f"{core['status']}:{core['health']}:restarts={core['restartCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_log_sha256="
|
|
f"{diagnostics['logSha256']}"
|
|
)
|
|
print(
|
|
"device_control_core_log_error_count="
|
|
f"{len(diagnostics['logErrors'])}"
|
|
)
|
|
for index, line in enumerate(diagnostics["logErrors"], start=1):
|
|
print(f"device_control_core_log_error_{index:02d}={line}")
|
|
for index, row in enumerate(diagnostics["database"], start=1):
|
|
print(
|
|
f"device_postgres_audit_{index:02d}="
|
|
f"{row.replace(chr(9), ':')}"
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"read-only:device-control-core,device-manager,device-gateway,"
|
|
"device-postgres,device-backhaul-target"
|
|
)
|
|
if (
|
|
device_plane_control_core_migration_replay_audit_preflight
|
|
is not None
|
|
):
|
|
audit = device_plane_control_core_migration_replay_audit_preflight
|
|
database = audit["database"]
|
|
core = audit["core"]
|
|
print(f"device_plane_transition={audit['mode']}")
|
|
print("allowed_operation=canonical-plan-only")
|
|
print("apply=forbidden")
|
|
print("device_plane_build=none")
|
|
print("device_plane_runtime_mutation=none")
|
|
print("device_plane_source_mutation=none")
|
|
print("device_postgres_mutation=none")
|
|
print("device_plane_network_mutation=none")
|
|
print("device_plane_secret_read=none")
|
|
print(
|
|
"device_control_core_current_runtime="
|
|
f"{core['containerId']}:{core['imageId']}:"
|
|
f"{core['status']}:{core['health']}:"
|
|
f"restarts={core['restartCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_rejected_recovery_artifact_sha256="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256}"
|
|
)
|
|
emit_device_plane_control_core_migration_replay_database_evidence(
|
|
database
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"read-only:device-control-core,device-manager,device-gateway,"
|
|
"device-postgres,device-backhaul-target"
|
|
)
|
|
if (
|
|
device_plane_control_core_migration_replay_recovery_preflight
|
|
is not None
|
|
):
|
|
recovery = (
|
|
device_plane_control_core_migration_replay_recovery_preflight
|
|
)
|
|
database = recovery["database"]
|
|
core = recovery["core"]
|
|
print(f"device_plane_transition={recovery['mode']}")
|
|
print(
|
|
"device_control_core_root_cause="
|
|
"migration-014-intermediate-constraint-revalidated-"
|
|
"newer-valid-receipts"
|
|
)
|
|
print(
|
|
"device_control_core_migration_predecessor_sha256="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256}"
|
|
)
|
|
print(
|
|
"device_control_core_migration_target_sha256="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256}"
|
|
)
|
|
print(
|
|
"device_control_core_current_runtime="
|
|
f"{core['containerId']}:{core['imageId']}:"
|
|
f"{core['status']}:{core['health']}:"
|
|
f"restarts={core['restartCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_live_command_kind_incompatible_count="
|
|
f"{database['invalidCommandKindCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_triggering_newer_receipt_count="
|
|
f"{database['triggeringReceiptCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_current_constraint="
|
|
"not-validated:exact-migration-011-command-kinds"
|
|
)
|
|
print(
|
|
"device_control_core_target_constraint="
|
|
"validated:covers-migration-016-command-kinds"
|
|
)
|
|
emit_device_plane_control_core_migration_replay_database_evidence(
|
|
database
|
|
)
|
|
print("device_postgres_row_mutation=none")
|
|
print("device_postgres_telemetry_table=absent")
|
|
print(
|
|
"device_plane_runtime_mutation="
|
|
"build+recreate:device-control-core"
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-manager,device-gateway,device-postgres,"
|
|
"device-backhaul-target"
|
|
)
|
|
print("device_gateway_public_ingress=disabled")
|
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
|
print("gelios=untouched-legacy-only")
|
|
print(
|
|
"device_plane_rollback="
|
|
"source+exact-degraded-predecessor-image-runtime"
|
|
)
|
|
if (
|
|
device_plane_control_core_migration_replay_checkpoint_recovery_preflight
|
|
is not None
|
|
):
|
|
recovery = (
|
|
device_plane_control_core_migration_replay_checkpoint_recovery_preflight
|
|
)
|
|
database = recovery["database"]
|
|
core = recovery["core"]
|
|
print(f"device_plane_transition={recovery['mode']}")
|
|
print(
|
|
"device_control_core_root_cause="
|
|
"restart-replay-cycles-exact-committed-migration-checkpoints"
|
|
)
|
|
print(
|
|
"device_control_core_failed_recovery_044_artifact_sha256="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"device_control_core_failed_recovery_044_started_apply=false"
|
|
)
|
|
print(
|
|
"device_control_core_migration_predecessor_sha256="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256}"
|
|
)
|
|
print(
|
|
"device_control_core_migration_target_sha256="
|
|
f"{DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256}"
|
|
)
|
|
print(
|
|
"device_control_core_current_runtime="
|
|
f"{core['containerId']}:{core['imageId']}:"
|
|
f"{core['status']}:{core['health']}:"
|
|
f"restarts={core['restartCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_live_command_kind_incompatible_count="
|
|
f"{database['invalidCommandKindCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_triggering_newer_receipt_count="
|
|
f"{database['triggeringReceiptCount']}"
|
|
)
|
|
print(
|
|
"device_control_core_current_constraint="
|
|
f"not-validated:exact-{database['constraintPhase']}-command-kinds"
|
|
)
|
|
print(
|
|
"device_control_core_allowed_predecessor_constraints="
|
|
"exact-replay-005|replay-007|replay-009|replay-011"
|
|
)
|
|
print(
|
|
"device_control_core_target_constraint="
|
|
"validated:exact-migration-016-command-kinds"
|
|
)
|
|
emit_device_plane_control_core_migration_replay_database_evidence(
|
|
database
|
|
)
|
|
print("device_postgres_row_mutation=none")
|
|
print("device_postgres_telemetry_table=absent")
|
|
print(
|
|
"device_plane_runtime_mutation="
|
|
"build+recreate:device-control-core"
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-manager,device-gateway,device-postgres,"
|
|
"device-backhaul-target"
|
|
)
|
|
print("device_gateway_public_ingress=disabled")
|
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
|
print("gelios=untouched-legacy-only")
|
|
print(
|
|
"device_plane_rollback="
|
|
"source+exact-degraded-predecessor-image-runtime"
|
|
)
|
|
if device_plane_backhaul_vps_enrollment_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_backhaul_vps_enrollment_preflight['mode']}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_patch="
|
|
f"{DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_PATCH_ID}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_artifact_sha256="
|
|
f"{DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_ARTIFACT_SHA256}"
|
|
)
|
|
print("device_plane_build=none")
|
|
print(
|
|
"device_plane_runtime_mutation="
|
|
"rotate-authorized-key+recreate:device-backhaul-target"
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-control-core,device-gateway,device-postgres"
|
|
)
|
|
print(
|
|
"device_backhaul_previous_enrollment_public_key_sha256="
|
|
f"{device_plane_backhaul_vps_enrollment_preflight['previousEnrollmentPublicKeySha256']}"
|
|
)
|
|
print(
|
|
"device_backhaul_next_enrollment_public_key_sha256="
|
|
f"{device_plane_backhaul_vps_enrollment_preflight['nextEnrollmentPublicKeySha256']}"
|
|
)
|
|
print(
|
|
"device_backhaul_next_key_fingerprint="
|
|
f"{device_plane_backhaul_vps_enrollment_preflight['nextKeyFingerprint']}"
|
|
)
|
|
print(
|
|
"device_backhaul_permitopen="
|
|
f"{DEVICE_PLANE_BACKHAUL_PERMITTED_TARGET}"
|
|
)
|
|
print("device_backhaul_docker_port_publication=disabled")
|
|
print("device_backhaul_tailscale_serve=unchanged")
|
|
print("device_backhaul_tailscale_funnel=disabled")
|
|
print("device_backhaul_router_nat_firewall=unchanged")
|
|
print("device_edge_public_ingress=disabled")
|
|
print("device_command_transport=disabled")
|
|
print("gelios=untouched")
|
|
print(
|
|
"device_plane_rollback="
|
|
"restore-previous-authorized-key+recreate-target"
|
|
)
|
|
if device_plane_backhaul_preflight is not None:
|
|
print(
|
|
"device_plane_transition="
|
|
f"{device_plane_backhaul_preflight['mode']}"
|
|
)
|
|
print(
|
|
"failed_patch="
|
|
f"{DEVICE_PLANE_BACKHAUL_FAILED_PATCH_ID}"
|
|
)
|
|
print(
|
|
"failed_artifact_sha256="
|
|
f"{DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"failed_backup="
|
|
f"{DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_ID}"
|
|
)
|
|
print(
|
|
"failed_rollback="
|
|
"ok:source-restored-target-removed-preserved-runtime-unchanged"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_patch="
|
|
f"{DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_PATCH_ID}"
|
|
)
|
|
print(
|
|
"device_plane_predecessor_artifact_sha256="
|
|
f"{DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_ARTIFACT_SHA256}"
|
|
)
|
|
print(
|
|
"device_plane_runtime_mutation="
|
|
"build+create:device-backhaul-target+tailscale-serve"
|
|
)
|
|
print(
|
|
"device_plane_runtime_services="
|
|
"preserved:device-control-core,device-gateway,device-postgres"
|
|
)
|
|
print(
|
|
"device_backhaul_loopback_listen="
|
|
f"{DEVICE_PLANE_BACKHAUL_LOOPBACK_ADDRESS}:"
|
|
f"{DEVICE_PLANE_BACKHAUL_LISTEN_PORT}/tcp"
|
|
)
|
|
print(
|
|
"device_backhaul_tailnet_listen="
|
|
f"tailscale-serve:{DEVICE_PLANE_BACKHAUL_TAILNET_ADDRESS}:"
|
|
f"{DEVICE_PLANE_BACKHAUL_LISTEN_PORT}/tcp=>"
|
|
f"{DEVICE_PLANE_BACKHAUL_TAILSCALE_SERVE_TARGET}"
|
|
)
|
|
print(
|
|
"device_backhaul_permitopen="
|
|
f"{DEVICE_PLANE_BACKHAUL_PERMITTED_TARGET}"
|
|
)
|
|
print(
|
|
"device_backhaul_enrollment_public_key_sha256="
|
|
f"{device_plane_backhaul_preflight['enrollmentPublicKeySha256']}"
|
|
)
|
|
print(
|
|
"device_backhaul_runtime_trust="
|
|
"runner-managed:host-key,authorized-keys,public-trust"
|
|
)
|
|
print(
|
|
"device_backhaul_tailscale_cli="
|
|
"official-package-account:"
|
|
f"{DEVICE_PLANE_TAILSCALE_USER}:sha256:"
|
|
f"{device_plane_backhaul_preflight['tailscaleCli']['binarySha256']}"
|
|
)
|
|
print("device_backhaul_docker_port_publication=disabled")
|
|
print("device_backhaul_tailscale_funnel=disabled")
|
|
print("device_backhaul_router_nat_firewall=unchanged")
|
|
print("device_edge_public_ingress=disabled")
|
|
print(
|
|
"device_plane_rollback="
|
|
"remove-tailnet-serve-target-and-restore-source"
|
|
)
|
|
if device_plane_postgres_preflight is not None:
|
|
print(
|
|
"device_postgres_bootstrap="
|
|
f"{device_plane_postgres_preflight}"
|
|
)
|
|
print("device_postgres_bootstrap_mode=create-if-absent")
|
|
print("device_postgres_rollback_volume=preserve")
|
|
if is_platform_device_core_hub_trust_slice(component, entries):
|
|
print(
|
|
"runtime_secret=runner-managed:"
|
|
f"{PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE}"
|
|
)
|
|
print("device_core_hub_credential_scope=handoff+session-only")
|
|
print("device_manager_public_route=unchanged")
|
|
if is_platform_device_manager_public_route_slice(component, entries):
|
|
print("device_manager_public_route=https-via-reverse-proxy")
|
|
print("device_manager_raw_tcp_ingress=forbidden")
|
|
if is_launcher_device_core_session_slice(component, entries):
|
|
print("device_core_owner_scopes=hub-signed-runtime-claims")
|
|
print("device_core_company_scope=active-admin-grant-only")
|
|
if touches_external_data_plane:
|
|
print(f"runtime_secret=runner-managed:{EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
|
|
print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}")
|
|
print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}")
|
|
if touches_engine_credential_sink(component, entries):
|
|
print(f"runtime_identity_key_id={ENGINE_CREDENTIAL_PROVISIONER_KEY_ID}")
|
|
print(f"runtime_private_key=runner-managed:{ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE}")
|
|
print(f"runtime_public_key=runner-managed:{ENGINE_CREDENTIAL_SINK_PUBLIC_KEY_FILE}")
|
|
if touches_engine_credential_sink(component, entries):
|
|
print(f"backend_source_container_id={credential_backend_preflight['container_id']}")
|
|
print(f"backend_source_image_id={credential_backend_preflight['source_image_id']}")
|
|
print(f"backend_current_image_id={credential_backend_preflight['current_image_id']}")
|
|
print(f"backend_current_barrier={credential_backend_preflight['mode']}")
|
|
print(f"backend_derived_image={ENGINE_CREDENTIAL_BACKEND_IMAGE}")
|
|
print(f"backend_package_lock_sha256={ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256}")
|
|
print(f"backend_node_modules_tree_sha256={credential_backend_preflight['node_modules_tree_sha256']}")
|
|
print("backend_rootfs=safe-roots:/bin,/sbin,/lib,/usr,/etc,/var/lib/apk")
|
|
print("backend_build_network=none")
|
|
print("backend_pull=never")
|
|
print("backend_force_recreate=yes")
|
|
if touches_publish_grant or touches_external_data_plane:
|
|
print(f"runtime_private_key=runner-managed:{ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}")
|
|
print(f"runtime_public_trust=runner-managed:{ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}")
|
|
if touches_publish_grant:
|
|
print(f"publish_grant_transition={publish_grant_preflight['mode']}")
|
|
for changed_path in publish_grant_preflight["changed_paths"]:
|
|
print(f"publish_grant_changed_path={changed_path}")
|
|
print(f"predecessor_compose_sha256={publish_grant_predecessor_sha256}")
|
|
print(f"backend_current_barrier={credential_backend_preflight['mode']}")
|
|
print(f"runtime_private_state=runner-managed:{component_root('engine') / ENGINE_PUBLISH_GRANT_STATE_REL}")
|
|
if publish_grant_preflight["mode"] == "installed-composite-provider-update":
|
|
print("engine_provider_package=gelios.provider.v4")
|
|
print("engine_provider_requests=monitoring-config,units?incltrip=true")
|
|
print("engine_data_product=fleet.positions.current.v3")
|
|
print(
|
|
"predecessor_catalog_sha256="
|
|
f"{ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_PREDECESSOR_SHA256}"
|
|
)
|
|
print(
|
|
"target_catalog_sha256="
|
|
f"{ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256}"
|
|
)
|
|
print("provider_credential_values=preserved")
|
|
if touches_agent_grant_migration:
|
|
print(f"predecessor_store_sha256={agent_grant_migration_predecessor_sha256}")
|
|
print(f"target_store_sha256={ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256}")
|
|
print(f"backend_current_barrier={credential_backend_preflight['mode']}")
|
|
print("agent_grant_migration=v1-full-bundle-to-v2-named-full-developer-profile")
|
|
print("backend_pull=never")
|
|
if component in ("proxy-contur", "dc-amd-proxy") or touches_map_gateway:
|
|
print(f"runtime_secret=runner-synced:{MAP_EGRESS_PROXY_SECRET_FILE}")
|
|
if component == "dc-amd-proxy":
|
|
print(f"runtime_state=runner-prepared:{DC_AMD_PROXY_RUNTIME_DIR}")
|
|
if state_has_sha(sha):
|
|
print("state=sha-already-applied")
|
|
elif state_has_patch_id(manifest["id"]):
|
|
print("state=patch-id-already-applied")
|
|
else:
|
|
print("state=new")
|
|
print("== files ==")
|
|
for rel in entries:
|
|
print(f" {rel}")
|
|
|
|
|
|
def add_backup_path(tar, root, rel):
|
|
src = root / rel
|
|
if src.exists():
|
|
tar.add(src, arcname=rel, recursive=True)
|
|
return True
|
|
return False
|
|
|
|
|
|
def create_backup(root, backup_dir, entries, include_nginx_html):
|
|
existing = []
|
|
missing = []
|
|
|
|
with tarfile.open(backup_dir / "source-before.tgz", "w:gz") as tar:
|
|
for rel in entries:
|
|
if add_backup_path(tar, root, rel):
|
|
existing.append(rel)
|
|
else:
|
|
missing.append(rel)
|
|
|
|
if include_nginx_html and (root / "nginx-html").exists():
|
|
tar.add(root / "nginx-html", arcname="nginx-html", recursive=True)
|
|
existing.append("nginx-html")
|
|
|
|
(backup_dir / "existing-files.txt").write_text("\n".join(existing) + ("\n" if existing else ""), encoding="utf-8")
|
|
(backup_dir / "missing-files.txt").write_text("\n".join(missing) + ("\n" if missing else ""), encoding="utf-8")
|
|
|
|
|
|
def backup_device_plane_backhaul_authorized_keys(backup_dir):
|
|
source_stat = DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE.lstat()
|
|
if (
|
|
stat.S_ISLNK(source_stat.st_mode)
|
|
or not stat.S_ISREG(source_stat.st_mode)
|
|
or source_stat.st_uid != 0
|
|
or stat.S_IMODE(source_stat.st_mode) != 0o444
|
|
or source_stat.st_size > 2048
|
|
):
|
|
die("Device Plane backhaul authorized_keys backup source is unsafe")
|
|
destination = backup_dir / DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_BACKUP
|
|
if destination.exists() or destination.is_symlink():
|
|
die("Device Plane backhaul authorized_keys backup collision")
|
|
shutil.copy2(DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE, destination)
|
|
os.chown(destination, 0, 0)
|
|
destination.chmod(0o600)
|
|
return sha256_file(destination)
|
|
|
|
|
|
def read_backup_path_list(path):
|
|
if not path.is_file():
|
|
die(f"deploy backup path list missing: {path}")
|
|
return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
|
|
|
|
def validate_backup_partition(entries, existing, missing, label):
|
|
entry_set = set(entries)
|
|
existing_set = set(existing)
|
|
missing_set = set(missing)
|
|
if len(entry_set) != len(entries):
|
|
die(f"{label} entries contain duplicates")
|
|
if len(existing_set) != len(existing) or len(missing_set) != len(missing):
|
|
die(f"{label} backup path lists contain duplicates")
|
|
if existing_set & missing_set or existing_set | missing_set != entry_set:
|
|
die(f"{label} backup entry set mismatch")
|
|
for rel in entries:
|
|
if any(rel != other and rel.startswith(other.rstrip("/") + "/") for other in entries):
|
|
die(f"{label} entries overlap")
|
|
return existing_set, missing_set
|
|
|
|
|
|
def materialize_backup_tree(backup_archive, restore_root, existing):
|
|
seen_names = set()
|
|
seen_roots = set()
|
|
directory_modes = []
|
|
member_count = 0
|
|
payload_bytes = 0
|
|
with tarfile.open(backup_archive, "r:gz") as archive:
|
|
for member in archive:
|
|
member_count += 1
|
|
if member_count > MAX_MEMBER_COUNT:
|
|
die("Platform rollback backup has too many members")
|
|
validate_posix_path(member.name)
|
|
if member.name in seen_names:
|
|
die("Platform rollback backup contains duplicate members")
|
|
seen_names.add(member.name)
|
|
owners = [
|
|
rel
|
|
for rel in existing
|
|
if member.name == rel or member.name.startswith(rel.rstrip("/") + "/")
|
|
]
|
|
if len(owners) != 1:
|
|
die("Platform rollback backup contains an unexpected member")
|
|
if member.name == owners[0]:
|
|
seen_roots.add(owners[0])
|
|
if not (member.isfile() or member.isdir()):
|
|
die("Platform rollback backup contains an unsupported member")
|
|
if member.mode & (stat.S_ISUID | stat.S_ISGID):
|
|
die("Platform rollback backup contains a privileged member")
|
|
|
|
target = tar_name_to_path(restore_root, member.name)
|
|
if member.isdir():
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
target.chmod(0o755)
|
|
directory_modes.append((target, stat.S_IMODE(member.mode) & 0o777))
|
|
continue
|
|
|
|
if member.size > MAX_FILE_BYTES:
|
|
die("Platform rollback backup member is too large")
|
|
payload_bytes += member.size
|
|
if payload_bytes > MAX_PAYLOAD_BYTES:
|
|
die("Platform rollback backup is too large")
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
if target.exists():
|
|
die("Platform rollback backup member collision")
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die("Platform rollback backup member is unreadable")
|
|
with source, target.open("xb") as output:
|
|
shutil.copyfileobj(source, output, 1024 * 1024)
|
|
target.chmod(stat.S_IMODE(member.mode) & 0o777)
|
|
|
|
if seen_roots != set(existing):
|
|
die("Platform rollback backup root set mismatch")
|
|
for target, mode in sorted(directory_modes, key=lambda item: len(item[0].parts), reverse=True):
|
|
target.chmod(mode)
|
|
|
|
|
|
def restore_platform_overlay(root, backup_dir, entries, current_stamp):
|
|
existing = read_backup_path_list(backup_dir / "existing-files.txt")
|
|
missing = read_backup_path_list(backup_dir / "missing-files.txt")
|
|
existing_set, missing_set = validate_backup_partition(
|
|
entries,
|
|
existing,
|
|
missing,
|
|
"Platform rollback",
|
|
)
|
|
backup_archive = backup_dir / "source-before.tgz"
|
|
try:
|
|
backup_stat = backup_archive.lstat()
|
|
except FileNotFoundError:
|
|
die("Platform rollback backup archive is missing")
|
|
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISREG(backup_stat.st_mode):
|
|
die("Platform rollback backup archive is unsafe")
|
|
|
|
with tempfile.TemporaryDirectory(prefix="platform-rollback-", dir=TMP_DIR) as tmp:
|
|
restore_root = Path(tmp)
|
|
materialize_backup_tree(backup_archive, restore_root, existing_set)
|
|
for rel in entries:
|
|
destination = root / rel
|
|
destination_resolved = destination.resolve(strict=False)
|
|
if not is_relative_to(destination_resolved, root.resolve()):
|
|
die("Platform rollback target escaped component root")
|
|
if destination.is_symlink():
|
|
die("Platform rollback target is a symlink")
|
|
|
|
if rel in existing_set:
|
|
staged = restore_root / rel
|
|
rollback_stamp = f"{current_stamp}-platform-rollback"
|
|
if staged.is_dir():
|
|
replace_directory(staged, destination, rollback_stamp)
|
|
elif staged.is_file():
|
|
replace_file(staged, destination, rollback_stamp)
|
|
else:
|
|
die("Platform rollback staged path is missing")
|
|
continue
|
|
|
|
if rel not in missing_set or not destination.exists():
|
|
continue
|
|
if destination.is_dir():
|
|
shutil.rmtree(destination)
|
|
elif destination.is_file():
|
|
destination.unlink()
|
|
else:
|
|
die("Platform rollback target has unsupported type")
|
|
return len(entries)
|
|
|
|
|
|
def rollback_platform_apply(root, backup_dir, entries, current_stamp, runtime_started, applied_services):
|
|
existing = read_backup_path_list(backup_dir / "existing-files.txt")
|
|
missing = read_backup_path_list(backup_dir / "missing-files.txt")
|
|
existing_set, missing_set = validate_backup_partition(
|
|
entries,
|
|
existing,
|
|
missing,
|
|
"Platform runtime rollback",
|
|
)
|
|
|
|
edp_was_new = PLATFORM_EXTERNAL_DATA_PLANE_COMPOSE_REL in missing_set
|
|
candidate_cleanup_failed = False
|
|
if runtime_started and edp_was_new:
|
|
# Remove only candidate-only containers while its Compose overlay is
|
|
# still installed. Deliberately omit --volumes: rollback never deletes
|
|
# newly written data, even when the first EDP activation is rejected.
|
|
try:
|
|
candidate_services = tuple(
|
|
service
|
|
for service in applied_services
|
|
if service == EXTERNAL_DATA_PLANE_SERVICE
|
|
)
|
|
if not candidate_services:
|
|
die("Platform candidate-only EDP runtime selection is invalid")
|
|
stop_and_remove_compose_services(
|
|
"platform",
|
|
candidate_services,
|
|
)
|
|
except Exception:
|
|
candidate_cleanup_failed = True
|
|
|
|
restored_count = restore_platform_overlay(root, backup_dir, entries, current_stamp)
|
|
if candidate_cleanup_failed:
|
|
die("Platform candidate-only runtime cleanup failed after source restore")
|
|
if not runtime_started:
|
|
return f"source-restored-runtime-unchanged:{restored_count}"
|
|
|
|
baseline_entries = [rel for rel in entries if rel in existing_set]
|
|
if edp_was_new:
|
|
baseline_entries = [
|
|
rel
|
|
for rel in baseline_entries
|
|
if not touches_external_data_plane_files((rel,))
|
|
]
|
|
baseline_services = tuple(
|
|
service
|
|
for service in applied_services
|
|
if not edp_was_new
|
|
or service not in (EXTERNAL_DATA_PLANE_SERVICE, EXTERNAL_DATA_PLANE_DATABASE_SERVICE)
|
|
)
|
|
if baseline_services:
|
|
run_component_runtime("platform", baseline_entries, baseline_services)
|
|
if baseline_services == (EXTERNAL_DATA_PLANE_SERVICE,):
|
|
# The restored EDP may predate managed provisioning. Prove the
|
|
# stable baseline contract without requiring a candidate-only field.
|
|
healthcheck_url(external_data_plane_healthcheck(require_managed=False))
|
|
else:
|
|
run_healthchecks("platform", baseline_entries, baseline_services)
|
|
return f"source+runtime-restored:{restored_count}"
|
|
|
|
|
|
def remove_device_plane_control_network_if_unused():
|
|
network = inspect_device_plane_network_optional(
|
|
DEVICE_PLANE_CONTROL_NETWORK
|
|
)
|
|
if network is None:
|
|
return "absent"
|
|
if (
|
|
network.get("Name") != DEVICE_PLANE_CONTROL_NETWORK
|
|
or network.get("Driver") != "bridge"
|
|
or network.get("Internal") is not False
|
|
or (network.get("Containers") or {})
|
|
or (network.get("Options") or {}).get(
|
|
"com.docker.network.bridge.enable_ip_masquerade"
|
|
)
|
|
!= "false"
|
|
):
|
|
die("Device Plane control network is unsafe to remove")
|
|
subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"network",
|
|
"rm",
|
|
DEVICE_PLANE_CONTROL_NETWORK,
|
|
],
|
|
check=True,
|
|
)
|
|
if inspect_device_plane_network_optional(DEVICE_PLANE_CONTROL_NETWORK):
|
|
die("Device Plane control network removal failed")
|
|
return "removed"
|
|
|
|
|
|
def rollback_device_plane_network_publication_apply(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started,
|
|
applied_services,
|
|
):
|
|
if tuple(applied_services) != (
|
|
"device-control-core",
|
|
"device-gateway",
|
|
):
|
|
die("Device Plane network-publication rollback service mismatch")
|
|
if runtime_started:
|
|
stop_and_remove_compose_services(
|
|
"device-plane",
|
|
applied_services,
|
|
)
|
|
restored_count = restore_platform_overlay(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
)
|
|
remove_device_plane_control_network_if_unused()
|
|
run_compose(
|
|
"device-plane",
|
|
applied_services,
|
|
("docker-compose.device-plane.yml",),
|
|
)
|
|
for service in DEVICE_PLANE_RUNTIME_SERVICES:
|
|
healthcheck_compose_service("device-plane", service)
|
|
runtime = validate_device_plane_foundation_runtime()
|
|
if (
|
|
runtime["device-postgres"]["containerId"]
|
|
!= DEVICE_PLANE_FOUNDATION_PREDECESSOR_CONTAINER_IDS[
|
|
"device-postgres"
|
|
]
|
|
):
|
|
die("Device Plane rollback changed PostgreSQL generation")
|
|
assert_loopback_tcp_port_closed(18120)
|
|
assert_loopback_tcp_port_closed(18121)
|
|
assert_loopback_tcp_port_closed(9921)
|
|
return f"source+internal-runtime-restored:{restored_count}"
|
|
|
|
|
|
def rollback_device_plane_apply(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started,
|
|
applied_services,
|
|
):
|
|
existing = read_backup_path_list(backup_dir / "existing-files.txt")
|
|
missing = read_backup_path_list(backup_dir / "missing-files.txt")
|
|
existing_set, _missing_set = validate_backup_partition(
|
|
entries,
|
|
existing,
|
|
missing,
|
|
"Device Plane runtime rollback",
|
|
)
|
|
baseline_entries = [rel for rel in entries if rel in existing_set]
|
|
runtime_inventory = read_strict_json(
|
|
backup_dir / "runtime-before.json",
|
|
"Device Plane pre-apply runtime inventory",
|
|
max_bytes=64 * 1024,
|
|
)
|
|
preapply_services = set(
|
|
device_plane_inventory_service_names(runtime_inventory)
|
|
)
|
|
baseline_services = tuple(
|
|
service
|
|
for service in applied_services
|
|
if service in preapply_services
|
|
)
|
|
candidate_only_services = tuple(
|
|
service
|
|
for service in applied_services
|
|
if service not in baseline_services
|
|
)
|
|
tailscale_serve_before = None
|
|
if is_device_plane_backhaul_target_slice("device-plane", entries):
|
|
tailscale_serve_before = read_strict_json(
|
|
backup_dir / "tailscale-serve-before.json",
|
|
"Device Plane pre-apply Tailscale Serve state",
|
|
max_bytes=4 * 1024 * 1024,
|
|
)
|
|
disable_device_plane_tailscale_serve(tailscale_serve_before)
|
|
candidate_cleanup_failed = False
|
|
if runtime_started and candidate_only_services:
|
|
# Remove only candidate services while the candidate Compose file is
|
|
# still present. PostgreSQL can appear only in the exact one-time
|
|
# bootstrap slice; volume flags are deliberately never used.
|
|
try:
|
|
stop_and_remove_compose_services(
|
|
"device-plane",
|
|
candidate_only_services,
|
|
)
|
|
except Exception:
|
|
candidate_cleanup_failed = True
|
|
|
|
restored_count = restore_platform_overlay(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
)
|
|
if candidate_cleanup_failed:
|
|
die("Device Plane candidate-only runtime cleanup failed after source restore")
|
|
if is_device_plane_backhaul_target_slice("device-plane", entries):
|
|
if device_plane_service_container_ids(
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE
|
|
):
|
|
die("Device Plane backhaul rollback retained target runtime")
|
|
validate_device_plane_preserved_runtime_unchanged(
|
|
runtime_inventory,
|
|
"Device Plane backhaul rollback",
|
|
)
|
|
tailscale = validate_device_plane_tailscale_runtime(
|
|
require_target=False
|
|
)
|
|
if tailscale["serve"] != tailscale_serve_before:
|
|
die("Device Plane backhaul rollback did not restore Tailscale Serve")
|
|
assert_loopback_tcp_port_open(9921)
|
|
return (
|
|
"tailscale-serve-restored-source-restored-target-removed-"
|
|
"preserved-runtime-unchanged:"
|
|
f"{restored_count}"
|
|
)
|
|
if not runtime_started or not baseline_services:
|
|
return f"source-restored-runtime-unchanged:{restored_count}"
|
|
|
|
control_core_release_rollback = (
|
|
is_device_plane_control_core_release_slice(
|
|
"device-plane",
|
|
entries,
|
|
)
|
|
or is_device_plane_control_core_migration_replay_recovery_slice(
|
|
"device-plane",
|
|
entries,
|
|
)
|
|
or is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
"device-plane",
|
|
entries,
|
|
)
|
|
)
|
|
if control_core_release_rollback:
|
|
before = {
|
|
item["service"]: item
|
|
for item in runtime_inventory["services"]
|
|
}
|
|
selected_before = before.get("device-control-core") or {}
|
|
image_id = selected_before.get("imageId")
|
|
if not isinstance(image_id, str):
|
|
die("Device Control Core rollback image evidence is missing")
|
|
retag_device_plane_control_core_image(
|
|
image_id,
|
|
"Device Control Core exact pre-apply rollback image",
|
|
)
|
|
prepare_component_runtime("device-plane", baseline_entries)
|
|
run_compose("device-plane", baseline_services, baseline_entries)
|
|
else:
|
|
run_component_runtime(
|
|
"device-plane",
|
|
baseline_entries,
|
|
baseline_services,
|
|
)
|
|
if control_core_release_rollback:
|
|
accept_device_plane_control_core_rollback_runtime(
|
|
runtime_inventory
|
|
)
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
elif is_device_plane_edge_core_channel_bootstrap_slice(
|
|
"device-plane",
|
|
entries,
|
|
):
|
|
for service in (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=is_device_plane_edge_core_channel_upgrade_slice(
|
|
"device-plane",
|
|
entries,
|
|
)
|
|
)
|
|
elif is_device_plane_manager_control_plane_slice("device-plane", entries):
|
|
for service in baseline_services:
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(
|
|
"device-plane",
|
|
baseline_entries,
|
|
baseline_services,
|
|
):
|
|
healthcheck_url(check)
|
|
else:
|
|
run_healthchecks(
|
|
"device-plane",
|
|
baseline_entries,
|
|
baseline_services,
|
|
)
|
|
return f"source+runtime-restored:{restored_count}"
|
|
|
|
|
|
def rollback_device_plane_backhaul_vps_enrollment(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_before,
|
|
):
|
|
restored_count = restore_platform_overlay(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
)
|
|
backup = backup_dir / DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_BACKUP
|
|
backup_stat = backup.lstat()
|
|
previous = read_device_plane_backhaul_enrollment_public_key()
|
|
expected = (
|
|
'restrict,port-forwarding,permitopen="127.0.0.1:9921" '
|
|
f"{previous['line']}\n"
|
|
)
|
|
if (
|
|
stat.S_ISLNK(backup_stat.st_mode)
|
|
or not stat.S_ISREG(backup_stat.st_mode)
|
|
or backup_stat.st_uid != 0
|
|
or stat.S_IMODE(backup_stat.st_mode) != 0o600
|
|
or backup_stat.st_size > 2048
|
|
or backup.read_text(encoding="ascii") != expected
|
|
):
|
|
die("Device Plane VPS enrollment rollback backup mismatch")
|
|
install_device_plane_backhaul_authorized_key(previous)
|
|
run_compose(
|
|
"device-plane",
|
|
(DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
|
|
entries,
|
|
)
|
|
run_healthchecks(
|
|
"device-plane",
|
|
entries,
|
|
(DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
|
|
)
|
|
validate_device_plane_backhaul_target_runtime(
|
|
runtime_before,
|
|
expected_enrollment=previous,
|
|
)
|
|
return f"previous-key+target+source-restored:{restored_count}"
|
|
|
|
|
|
def rollback_engine_apply(root, backup_dir, entries, current_stamp, runtime_started, applied_services):
|
|
existing = read_backup_path_list(backup_dir / "existing-files.txt")
|
|
missing = read_backup_path_list(backup_dir / "missing-files.txt")
|
|
restore_entries = list(entries)
|
|
if "nginx-html" in existing:
|
|
restore_entries.append("nginx-html")
|
|
validate_backup_partition(
|
|
restore_entries,
|
|
existing,
|
|
missing,
|
|
"Engine runtime rollback",
|
|
)
|
|
restored_count = restore_platform_overlay(
|
|
root,
|
|
backup_dir,
|
|
restore_entries,
|
|
current_stamp,
|
|
)
|
|
if not runtime_started:
|
|
return f"source-restored-runtime-unchanged:{restored_count}"
|
|
run_component_runtime("engine", entries, applied_services)
|
|
run_healthchecks("engine", entries, applied_services)
|
|
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):
|
|
inactive = dict(descriptor)
|
|
inactive["action"] = "rollback-inactive"
|
|
inactive["expectedCurrent"] = descriptor["releaseId"]
|
|
inactive["expectedNodeTypes"] = []
|
|
inactive["expectedCredentialTypes"] = []
|
|
return inactive
|
|
|
|
|
|
def restore_engine_n8n_transition(root, backup_dir, entries, descriptor, current_stamp):
|
|
existing = set(read_backup_path_list(backup_dir / "existing-files.txt"))
|
|
missing = set(read_backup_path_list(backup_dir / "missing-files.txt"))
|
|
if existing | missing != set(entries):
|
|
die("Engine n8n rollback backup entry set mismatch")
|
|
required_baseline = {
|
|
ENGINE_N8N_NODES_CATALOG_REL,
|
|
ENGINE_N8N_CREDENTIALS_CATALOG_REL,
|
|
ENGINE_N8N_SCHEMA_META_REL,
|
|
}
|
|
if not required_baseline.issubset(existing):
|
|
die("Engine n8n rollback baseline catalogs were not present before apply")
|
|
|
|
with tempfile.TemporaryDirectory(prefix="engine-n8n-rollback-", dir=TMP_DIR) as tmp:
|
|
restore_root = Path(tmp)
|
|
with tarfile.open(backup_dir / "source-before.tgz", "r:gz") as archive:
|
|
for rel in entries:
|
|
if rel not in existing:
|
|
continue
|
|
try:
|
|
member = archive.getmember(rel)
|
|
except KeyError:
|
|
die(f"Engine n8n rollback backup member missing: {rel}")
|
|
if not member.isfile():
|
|
die(f"Engine n8n rollback backup member is not a file: {rel}")
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die(f"Engine n8n rollback backup member unreadable: {rel}")
|
|
staged = restore_root / rel
|
|
staged.parent.mkdir(parents=True, exist_ok=True)
|
|
with staged.open("xb") as output:
|
|
shutil.copyfileobj(source, output)
|
|
replace_file(staged, root / rel, f"{current_stamp}-rollback")
|
|
|
|
if ENGINE_N8N_TRANSITION_DESCRIPTOR_REL not in existing:
|
|
inactive = inactive_engine_n8n_descriptor(descriptor)
|
|
descriptor_path = root / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL
|
|
descriptor_path.parent.mkdir(parents=True, exist_ok=True)
|
|
next_path = descriptor_path.with_name(f"{descriptor_path.name}.next-{current_stamp}-rollback")
|
|
if next_path.exists() or next_path.is_symlink():
|
|
die(f"Engine n8n rollback descriptor staging path exists: {next_path}")
|
|
next_path.write_text(json.dumps(inactive, indent=2) + "\n", encoding="utf-8")
|
|
os.replace(next_path, descriptor_path)
|
|
|
|
restored_descriptor = current_engine_n8n_transition_descriptor()
|
|
if restored_descriptor is None:
|
|
die("Engine n8n rollback did not restore an explicit baseline descriptor")
|
|
run_compose("engine", ("n8n",), entries)
|
|
accept_engine_n8n_runtime(restored_descriptor)
|
|
return restored_descriptor["action"]
|
|
|
|
|
|
def replace_file(src, dst, current_stamp):
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = dst.with_name(f"{dst.name}.next-{current_stamp}")
|
|
if tmp.exists():
|
|
die(f"staging path already exists: {tmp}")
|
|
shutil.copy2(src, tmp)
|
|
os.replace(tmp, dst)
|
|
|
|
|
|
def replace_directory(src, dst, current_stamp):
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
next_path = dst.with_name(f"{dst.name}.next-{current_stamp}")
|
|
prev_path = dst.with_name(f"{dst.name}.prev-{current_stamp}")
|
|
if next_path.exists() or prev_path.exists():
|
|
die(f"staging path already exists near: {dst}")
|
|
shutil.copytree(src, next_path)
|
|
if dst.exists():
|
|
dst.rename(prev_path)
|
|
next_path.rename(dst)
|
|
|
|
|
|
def copy_payload_path(payload_dir, root, rel, current_stamp):
|
|
src = payload_dir / rel
|
|
dst = root / rel
|
|
root_real = root.resolve()
|
|
dst_real = dst.resolve(strict=False)
|
|
if not is_relative_to(dst_real, root_real):
|
|
die(f"target escaped component root: {rel}")
|
|
|
|
if src.is_dir():
|
|
replace_directory(src, dst, current_stamp)
|
|
elif src.is_file():
|
|
replace_file(src, dst, current_stamp)
|
|
else:
|
|
die(f"payload path is neither file nor directory: {rel}")
|
|
|
|
|
|
def restore_overlay_source(root, backup_dir, entries, current_stamp):
|
|
existing = set(read_backup_path_list(backup_dir / "existing-files.txt"))
|
|
missing = set(read_backup_path_list(backup_dir / "missing-files.txt"))
|
|
if existing | missing != set(entries) or existing & missing:
|
|
die("credential bridge rollback backup entry set mismatch")
|
|
|
|
with tempfile.TemporaryDirectory(prefix="credential-bridge-rollback-", dir=TMP_DIR) as tmp:
|
|
restore_root = Path(tmp)
|
|
names = set()
|
|
with tarfile.open(backup_dir / "source-before.tgz", "r:gz") as archive:
|
|
for member in archive:
|
|
validate_posix_path(member.name)
|
|
if member.name in names:
|
|
die(f"credential bridge rollback duplicate backup member: {member.name}")
|
|
names.add(member.name)
|
|
if not path_is_covered_by_files_list(member.name, existing):
|
|
die(f"credential bridge rollback unexpected backup member: {member.name}")
|
|
if not (member.isfile() or member.isdir()):
|
|
die(f"credential bridge rollback special backup member: {member.name}")
|
|
target = tar_name_to_path(restore_root, member.name)
|
|
if member.isdir():
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
target.chmod(0o755)
|
|
continue
|
|
if member.size > MAX_FILE_BYTES:
|
|
die(f"credential bridge rollback backup member too large: {member.name}")
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
source = archive.extractfile(member)
|
|
if source is None:
|
|
die(f"credential bridge rollback unreadable backup member: {member.name}")
|
|
with source, target.open("xb") as output:
|
|
shutil.copyfileobj(source, output, 1024 * 1024)
|
|
target.chmod(0o644)
|
|
|
|
for rel in entries:
|
|
destination = root / rel
|
|
if rel in existing:
|
|
source = restore_root / rel
|
|
if not source.exists() or source.is_symlink():
|
|
die(f"credential bridge rollback source missing or unsafe: {rel}")
|
|
copy_payload_path(restore_root, root, rel, f"{current_stamp}-rollback")
|
|
continue
|
|
if not destination.exists() and not destination.is_symlink():
|
|
continue
|
|
if destination.is_symlink():
|
|
die(f"credential bridge rollback installed symlink rejected: {rel}")
|
|
retained = destination.with_name(f"{destination.name}.failed-{current_stamp}")
|
|
if retained.exists() or retained.is_symlink():
|
|
die(f"credential bridge rollback retained path already exists: {retained}")
|
|
destination.rename(retained)
|
|
|
|
|
|
def rollback_engine_credential_bridge(
|
|
component,
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
engine_backend_recreated=False,
|
|
engine_backend_initial_mode=None,
|
|
):
|
|
if not touches_engine_credential_sink(component, entries):
|
|
die("credential bridge rollback called for unrelated artifact")
|
|
restore_overlay_source(root, backup_dir, entries, current_stamp)
|
|
if not engine_backend_recreated:
|
|
if engine_backend_initial_mode == "verified-derived-retry":
|
|
validate_engine_backend_activation_marker()
|
|
if not engine_backend_immutable_runtime_is_current():
|
|
raise ReconciliationRequired("engine_backend_prior_derived_runtime_unproven")
|
|
return "engine-sink-source-restored-active-runtime-preserved"
|
|
if engine_backend_initial_mode == "verified-prepared-not-active":
|
|
validate_engine_backend_activation_marker()
|
|
return "engine-sink-source-restored-prepared-runtime-preserved"
|
|
quarantine = quarantine_engine_backend_partial_runtime()
|
|
return f"engine-sink-source-restored-before-backend-recreate-{quarantine}"
|
|
rollback_entries = ("nodedc-source/server/index.js",)
|
|
# The first Compose call may have failed before replacing the base
|
|
# container. Original sink entries explicitly authorize the already proven
|
|
# prepared override for this rollback recreate.
|
|
run_compose("engine", ("nodedc-backend",), entries)
|
|
healthcheck_compose_service("engine", "nodedc-backend")
|
|
run_healthchecks("engine", rollback_entries)
|
|
runtime = preflight_engine_credential_backend_runtime()
|
|
if runtime["mode"] != "verified-derived-retry":
|
|
die("Engine backend immutable rollback runtime acceptance failed")
|
|
return "engine-sink-source-restored"
|
|
|
|
|
|
def rollback_gitea_fresh_install(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started=False,
|
|
):
|
|
if root != GITEA_ROOT or not is_gitea_fresh_install_slice("gitea", entries):
|
|
die("Gitea fresh-install rollback called for unrelated artifact")
|
|
existing = read_backup_path_list(backup_dir / "existing-files.txt")
|
|
missing = read_backup_path_list(backup_dir / "missing-files.txt")
|
|
existing_set, missing_set = validate_backup_partition(
|
|
entries,
|
|
existing,
|
|
missing,
|
|
"Gitea fresh-install rollback",
|
|
)
|
|
if existing_set or missing_set != set(GITEA_FRESH_INSTALL_ENTRIES):
|
|
die("Gitea fresh-install rollback predecessor must be absent")
|
|
|
|
if runtime_started:
|
|
try:
|
|
stop_and_remove_compose_services("gitea", (GITEA_SERVICE,))
|
|
except Exception:
|
|
# Never restore an absent source root while a bind-mounted
|
|
# candidate may still be live. Preserve the complete root in
|
|
# place and require explicit reconciliation.
|
|
raise ReconciliationRequired(
|
|
"gitea_candidate_stop_failed_root_preserved"
|
|
)
|
|
|
|
try:
|
|
candidate_ids = gitea_compose_project_container_ids()
|
|
except Exception:
|
|
raise ReconciliationRequired(
|
|
"gitea_candidate_absence_unproven_root_preserved"
|
|
)
|
|
if candidate_ids:
|
|
raise ReconciliationRequired(
|
|
"gitea_candidate_still_present_root_preserved"
|
|
)
|
|
|
|
quarantined_root = None
|
|
if root.exists() or root.is_symlink():
|
|
root_stat = root.lstat()
|
|
if stat.S_ISLNK(root_stat.st_mode) or not stat.S_ISDIR(root_stat.st_mode):
|
|
die("Gitea fresh-install rollback root is unsafe")
|
|
quarantined_root = root.with_name(
|
|
f"{root.name}.failed-{current_stamp}"
|
|
)
|
|
if quarantined_root.exists() or quarantined_root.is_symlink():
|
|
die("Gitea fresh-install rollback quarantine collision")
|
|
root.rename(quarantined_root)
|
|
|
|
restore_overlay_source(root, backup_dir, entries, current_stamp)
|
|
if root.exists() or root.is_symlink():
|
|
die("Gitea fresh-install rollback did not restore absent root")
|
|
assert_loopback_tcp_port_closed(GITEA_DISABLED_SSH_HOST_PORT)
|
|
validate_legacy_gitea_container_isolation()
|
|
if gitea_compose_project_container_ids():
|
|
raise ReconciliationRequired(
|
|
"gitea_candidate_reappeared_after_quarantine"
|
|
)
|
|
validate_gitea_legacy_candidate_network_absent()
|
|
validate_gitea_no_docker_port_publications()
|
|
# Data/config/secrets are intentionally retained in the isolated new root
|
|
# as incident evidence. No legacy path or legacy container is referenced.
|
|
return (
|
|
"candidate-stopped-root-quarantined-runtime-state-preserved:"
|
|
+ (quarantined_root.name if quarantined_root else "none-created")
|
|
)
|
|
|
|
|
|
def rollback_engine_node_intelligence(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started=False,
|
|
node_intelligence_service_stopped=False,
|
|
):
|
|
candidate_descriptor = current_engine_node_intelligence_descriptor()
|
|
candidate_active = (
|
|
candidate_descriptor is not None
|
|
and candidate_descriptor["action"] == "activate"
|
|
)
|
|
cleanup_failed = False
|
|
if runtime_started and candidate_active:
|
|
try:
|
|
stop_and_remove_compose_services(
|
|
"engine",
|
|
(ENGINE_NODE_INTELLIGENCE_SERVICE,),
|
|
)
|
|
except Exception:
|
|
cleanup_failed = True
|
|
|
|
restore_overlay_source(root, backup_dir, entries, current_stamp)
|
|
restored = current_engine_node_intelligence_descriptor()
|
|
restored_action = restored["action"] if restored is not None else "inactive"
|
|
if cleanup_failed:
|
|
die("Engine node-intelligence candidate runtime cleanup failed after source restore")
|
|
|
|
reconcile_runtime = runtime_started or node_intelligence_service_stopped
|
|
if not reconcile_runtime:
|
|
return f"source-restored-runtime-unchanged:{restored_action}"
|
|
|
|
if restored is not None and restored["action"] == "activate":
|
|
validate_installed_engine_node_intelligence_source(restored)
|
|
ensure_engine_node_intelligence_secret()
|
|
install_engine_node_intelligence_image(restored)
|
|
run_engine_node_intelligence_compose(
|
|
(ENGINE_NODE_INTELLIGENCE_SERVICE, "nodedc-backend"),
|
|
ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
|
|
)
|
|
accept_engine_node_intelligence_runtime(restored)
|
|
return "active"
|
|
|
|
run_engine_node_intelligence_compose(
|
|
("nodedc-backend",),
|
|
ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES,
|
|
)
|
|
healthcheck_compose_service("engine", "nodedc-backend")
|
|
healthcheck_url("http://127.0.0.1:3001/health")
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine node-intelligence rollback backend barrier failed")
|
|
assert_engine_node_intelligence_container_absent()
|
|
return restored_action
|
|
|
|
|
|
def seal_n8n_private_extension_release(root, rel):
|
|
release_dir = root / rel
|
|
if not release_dir.is_dir() or release_dir.is_symlink():
|
|
die("private extension release was not installed as a directory")
|
|
for path in [release_dir, *release_dir.rglob("*")]:
|
|
path_stat = path.lstat()
|
|
if stat.S_ISLNK(path_stat.st_mode):
|
|
die(f"private extension installed symlink rejected: {path}")
|
|
os.chown(path, 0, 0)
|
|
if stat.S_ISDIR(path_stat.st_mode):
|
|
path.chmod(0o555)
|
|
elif stat.S_ISREG(path_stat.st_mode):
|
|
path.chmod(0o444)
|
|
else:
|
|
die(f"private extension installed special file rejected: {path}")
|
|
|
|
|
|
def publish_engine_dist(root, current_stamp):
|
|
dist = root / "nodedc-source" / "dist"
|
|
if not dist.is_dir():
|
|
return
|
|
|
|
dst = root / "nginx-html"
|
|
next_path = root / f"nginx-html.next-{current_stamp}"
|
|
prev_path = root / f"nginx-html.prev-{current_stamp}"
|
|
if next_path.exists() or prev_path.exists():
|
|
die("nginx-html staging path already exists")
|
|
|
|
shutil.copytree(dist, next_path)
|
|
if dst.exists():
|
|
dst.rename(prev_path)
|
|
next_path.rename(dst)
|
|
|
|
|
|
def component_publish_dist(component, entries=None):
|
|
if not COMPONENTS[component].get("publish_dist"):
|
|
return False
|
|
if component == "engine":
|
|
return entries is not None and any(
|
|
rel == "nodedc-source/dist" or rel.startswith("nodedc-source/dist/")
|
|
for rel in entries
|
|
)
|
|
return True
|
|
|
|
|
|
def run_build(component, entries=None):
|
|
for build_root, build_args in component_builds(component, entries):
|
|
dockerfile = "Dockerfile"
|
|
for idx, arg in enumerate(build_args):
|
|
if arg == "-f" and idx + 1 < len(build_args):
|
|
dockerfile = build_args[idx + 1]
|
|
break
|
|
|
|
if not (build_root / dockerfile).is_file():
|
|
die(f"Dockerfile not found in build root: {build_root / dockerfile}")
|
|
|
|
cmd = [str(DOCKER), *build_args]
|
|
env = os.environ.copy()
|
|
if component == "tasker":
|
|
env["DOCKER_BUILDKIT"] = "0"
|
|
subprocess.run(cmd, cwd=str(build_root), env=env, check=True)
|
|
|
|
|
|
def compose_base_cmd(
|
|
component,
|
|
allow_prepared_engine_backend=False,
|
|
expected_node_intelligence_gateway_sha256=None,
|
|
):
|
|
cmd = [str(DOCKER), "compose"]
|
|
compose_project = component_compose_project(component)
|
|
if compose_project:
|
|
cmd.extend(["-p", compose_project])
|
|
env_file = component_compose_env_file(component)
|
|
if env_file:
|
|
cmd.extend(["--env-file", str(env_file)])
|
|
for compose_file in component_compose_files(
|
|
component,
|
|
allow_prepared_engine_backend=allow_prepared_engine_backend,
|
|
expected_node_intelligence_gateway_sha256=
|
|
expected_node_intelligence_gateway_sha256,
|
|
):
|
|
cmd.extend(["-f", str(compose_file)])
|
|
return cmd
|
|
|
|
|
|
def run_compose(component, services, entries=None):
|
|
compose_root = component_compose_root(component)
|
|
allow_prepared_engine_backend = touches_engine_credential_sink(component, entries)
|
|
cmd = [
|
|
*compose_base_cmd(
|
|
component,
|
|
allow_prepared_engine_backend=allow_prepared_engine_backend,
|
|
),
|
|
"up", "-d", "--force-recreate",
|
|
]
|
|
if (is_engine_n8n_transition(component, entries)
|
|
or touches_engine_credential_sink(component, entries)
|
|
or touches_engine_credential_provisioner(component, entries)
|
|
or (component == "engine" and ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.exists())):
|
|
cmd.extend(["--pull", "never"])
|
|
if COMPONENTS[component].get("compose_build"):
|
|
cmd.append("--build")
|
|
if component_compose_no_deps(component, entries):
|
|
cmd.append("--no-deps")
|
|
cmd.extend(services)
|
|
try:
|
|
subprocess.run(cmd, cwd=str(compose_root), check=True)
|
|
except subprocess.CalledProcessError:
|
|
subprocess.run(
|
|
[
|
|
*compose_base_cmd(
|
|
component,
|
|
allow_prepared_engine_backend=allow_prepared_engine_backend,
|
|
),
|
|
"logs", "--no-color", "--tail=180", *services,
|
|
],
|
|
cwd=str(compose_root),
|
|
check=False,
|
|
)
|
|
raise
|
|
subprocess.run(
|
|
[
|
|
*compose_base_cmd(
|
|
component,
|
|
allow_prepared_engine_backend=allow_prepared_engine_backend,
|
|
),
|
|
"ps",
|
|
],
|
|
cwd=str(compose_root),
|
|
check=True,
|
|
)
|
|
|
|
|
|
def run_gitea_fresh_install_compose(services, entries):
|
|
if (
|
|
tuple(services) != (GITEA_SERVICE,)
|
|
or not is_gitea_fresh_install_slice("gitea", entries)
|
|
):
|
|
die("Gitea fresh-install Compose service set mismatch")
|
|
cmd = [
|
|
*compose_base_cmd("gitea"),
|
|
"up",
|
|
"-d",
|
|
"--force-recreate",
|
|
"--pull",
|
|
"never",
|
|
"--no-deps",
|
|
GITEA_SERVICE,
|
|
]
|
|
try:
|
|
subprocess.run(cmd, cwd=str(GITEA_ROOT), check=True)
|
|
except subprocess.CalledProcessError:
|
|
subprocess.run(
|
|
[
|
|
*compose_base_cmd("gitea"),
|
|
"logs",
|
|
"--no-color",
|
|
"--tail=180",
|
|
GITEA_SERVICE,
|
|
],
|
|
cwd=str(GITEA_ROOT),
|
|
check=False,
|
|
)
|
|
raise
|
|
subprocess.run(
|
|
[*compose_base_cmd("gitea"), "ps"],
|
|
cwd=str(GITEA_ROOT),
|
|
check=True,
|
|
)
|
|
|
|
|
|
def run_engine_node_intelligence_compose(services, entries):
|
|
if not is_engine_node_intelligence_transition("engine", entries):
|
|
die("Engine node-intelligence Compose called for unrelated artifact")
|
|
compose_root = component_compose_root("engine")
|
|
cmd = [
|
|
*compose_base_cmd("engine"),
|
|
"up",
|
|
"-d",
|
|
"--force-recreate",
|
|
"--pull",
|
|
"never",
|
|
"--no-deps",
|
|
*services,
|
|
]
|
|
try:
|
|
subprocess.run(cmd, cwd=str(compose_root), check=True)
|
|
except subprocess.CalledProcessError:
|
|
subprocess.run(
|
|
[
|
|
*compose_base_cmd("engine"),
|
|
"logs",
|
|
"--no-color",
|
|
"--tail=180",
|
|
*services,
|
|
],
|
|
cwd=str(compose_root),
|
|
check=False,
|
|
)
|
|
raise
|
|
subprocess.run(
|
|
[*compose_base_cmd("engine"), "ps"],
|
|
cwd=str(compose_root),
|
|
check=True,
|
|
)
|
|
|
|
|
|
def install_device_plane_backhaul_authorized_key(enrollment):
|
|
DEVICE_PLANE_BACKHAUL_SECRET_DIR.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
os.chown(DEVICE_PLANE_BACKHAUL_SECRET_DIR, 0, 0)
|
|
DEVICE_PLANE_BACKHAUL_SECRET_DIR.chmod(0o700)
|
|
authorized = (
|
|
'restrict,port-forwarding,permitopen="127.0.0.1:9921" '
|
|
f"{enrollment['line']}\n"
|
|
)
|
|
temporary = DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE.with_suffix(
|
|
".installing"
|
|
)
|
|
if temporary.exists() or temporary.is_symlink():
|
|
die("Device Plane backhaul authorized_keys staging path exists")
|
|
temporary.write_text(authorized, encoding="ascii")
|
|
os.chown(temporary, 0, 0)
|
|
temporary.chmod(0o444)
|
|
os.replace(temporary, DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE)
|
|
expected_sha256 = hashlib.sha256(authorized.encode("ascii")).hexdigest()
|
|
if sha256_file(DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE) != expected_sha256:
|
|
die("Device Plane backhaul authorized key verification failed")
|
|
return expected_sha256
|
|
|
|
|
|
def ensure_device_plane_backhaul_target_state():
|
|
enrollment = read_device_plane_backhaul_enrollment_public_key()
|
|
DEVICE_PLANE_BACKHAUL_SECRET_DIR.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
os.chown(DEVICE_PLANE_BACKHAUL_SECRET_DIR, 0, 0)
|
|
DEVICE_PLANE_BACKHAUL_SECRET_DIR.chmod(0o700)
|
|
|
|
if not DEVICE_PLANE_BACKHAUL_HOST_KEY_FILE.exists():
|
|
subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"run",
|
|
"--rm",
|
|
"--entrypoint",
|
|
"/usr/bin/ssh-keygen",
|
|
"-v",
|
|
f"{DEVICE_PLANE_BACKHAUL_SECRET_DIR}:/keys",
|
|
DEVICE_PLANE_BACKHAUL_TARGET_IMAGE,
|
|
"-q",
|
|
"-t",
|
|
"ed25519",
|
|
"-N",
|
|
"",
|
|
"-C",
|
|
"nodedc-device-plane-backhaul-target",
|
|
"-f",
|
|
"/keys/ssh_host_ed25519_key",
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
host_public_source = Path(
|
|
f"{DEVICE_PLANE_BACKHAUL_HOST_KEY_FILE}.pub"
|
|
)
|
|
for path, expected_mode, max_size in (
|
|
(DEVICE_PLANE_BACKHAUL_HOST_KEY_FILE, 0o400, 2048),
|
|
(host_public_source, 0o444, 1024),
|
|
):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Plane backhaul host key generation failed")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_uid != 0
|
|
or path_stat.st_size > max_size
|
|
):
|
|
die("Device Plane backhaul host key boundary mismatch")
|
|
os.chown(path, 0, 0)
|
|
path.chmod(expected_mode)
|
|
|
|
expected_authorized_sha256 = install_device_plane_backhaul_authorized_key(
|
|
enrollment
|
|
)
|
|
|
|
DEVICE_PLANE_BACKHAUL_TRUST_DIR.mkdir(parents=True, exist_ok=True)
|
|
os.chown(DEVICE_PLANE_BACKHAUL_TRUST_DIR, 0, 0)
|
|
DEVICE_PLANE_BACKHAUL_TRUST_DIR.chmod(0o755)
|
|
public_value = host_public_source.read_text(encoding="ascii")
|
|
public_temporary = DEVICE_PLANE_BACKHAUL_HOST_PUBLIC_KEY_FILE.with_suffix(
|
|
".installing"
|
|
)
|
|
if public_temporary.exists() or public_temporary.is_symlink():
|
|
die("Device Plane backhaul public trust staging path exists")
|
|
public_temporary.write_text(public_value, encoding="ascii")
|
|
os.chown(public_temporary, 0, 0)
|
|
public_temporary.chmod(0o444)
|
|
os.replace(public_temporary, DEVICE_PLANE_BACKHAUL_HOST_PUBLIC_KEY_FILE)
|
|
|
|
if (
|
|
sha256_file(DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE)
|
|
!= expected_authorized_sha256
|
|
or sha256_file(DEVICE_PLANE_BACKHAUL_HOST_PUBLIC_KEY_FILE)
|
|
!= sha256_file(host_public_source)
|
|
):
|
|
die("Device Plane backhaul runtime trust verification failed")
|
|
|
|
|
|
def validate_device_plane_manager_persistent_data_metadata():
|
|
try:
|
|
data_stat = DEVICE_PLANE_MANAGER_DATA_DIR.lstat()
|
|
except FileNotFoundError:
|
|
die("Device Manager persistent data directory is missing")
|
|
if (
|
|
stat.S_ISLNK(data_stat.st_mode)
|
|
or not stat.S_ISDIR(data_stat.st_mode)
|
|
or data_stat.st_uid != 1000
|
|
or data_stat.st_gid != 1000
|
|
or stat.S_IMODE(data_stat.st_mode) != 0o750
|
|
):
|
|
die("Device Manager persistent data directory boundary mismatch")
|
|
return "uid-1000-gid-1000-mode-0750"
|
|
|
|
|
|
def ensure_device_plane_manager_persistent_data():
|
|
data_parent = DEVICE_PLANE_MANAGER_DATA_DIR.parent
|
|
parent_created = False
|
|
try:
|
|
parent_stat = data_parent.lstat()
|
|
except FileNotFoundError:
|
|
data_parent.mkdir(mode=0o755)
|
|
parent_created = True
|
|
parent_stat = data_parent.lstat()
|
|
if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(
|
|
parent_stat.st_mode
|
|
):
|
|
die("Device Manager persistent data parent is unsafe")
|
|
if parent_created:
|
|
os.chown(data_parent, 0, 0)
|
|
data_parent.chmod(0o755)
|
|
DEVICE_PLANE_MANAGER_DATA_DIR.mkdir(exist_ok=True)
|
|
data_stat = DEVICE_PLANE_MANAGER_DATA_DIR.lstat()
|
|
if stat.S_ISLNK(data_stat.st_mode) or not stat.S_ISDIR(data_stat.st_mode):
|
|
die("Device Manager persistent data path is unsafe")
|
|
os.chown(DEVICE_PLANE_MANAGER_DATA_DIR, 1000, 1000)
|
|
DEVICE_PLANE_MANAGER_DATA_DIR.chmod(0o750)
|
|
return validate_device_plane_manager_persistent_data_metadata()
|
|
|
|
|
|
def stop_and_remove_compose_services(component, services):
|
|
if not services:
|
|
return
|
|
compose_root = component_compose_root(component)
|
|
subprocess.run(
|
|
[*compose_base_cmd(component), "rm", "--stop", "--force", *services],
|
|
cwd=str(compose_root),
|
|
check=True,
|
|
)
|
|
|
|
|
|
def create_gitea_runtime_secret(path, label):
|
|
if path.exists() or path.is_symlink():
|
|
die(f"Gitea {label} secret already exists during fresh install")
|
|
value = secrets.token_urlsafe(64)
|
|
if not GITEA_SECRET_RE.fullmatch(value):
|
|
die(f"Gitea {label} secret generation failed")
|
|
temporary = path.with_name(
|
|
f".{path.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
)
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(
|
|
str(temporary),
|
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
0o400,
|
|
)
|
|
os.write(descriptor, f"{value}\n".encode("ascii"))
|
|
os.fsync(descriptor)
|
|
os.fchown(descriptor, GITEA_RUNTIME_UID, GITEA_RUNTIME_GID)
|
|
os.fchmod(descriptor, 0o400)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
os.replace(temporary, path)
|
|
fsync_directory(path.parent)
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
|
|
|
|
def prepare_gitea_fresh_runtime(entries):
|
|
if not is_gitea_fresh_install_slice("gitea", entries):
|
|
die("Gitea runtime preparation called for unrelated artifact")
|
|
compose = GITEA_ROOT / GITEA_COMPOSE_REL
|
|
descriptor_path = GITEA_ROOT / GITEA_FRESH_INSTALL_DESCRIPTOR_REL
|
|
if (
|
|
compose.is_symlink()
|
|
or not compose.is_file()
|
|
or sha256_file(compose) != GITEA_COMPOSE_SHA256
|
|
):
|
|
die("installed Gitea fresh-install Compose source mismatch")
|
|
descriptor = read_strict_json(
|
|
descriptor_path,
|
|
"installed Gitea fresh-install descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if descriptor != expected_gitea_fresh_install_descriptor():
|
|
die("installed Gitea fresh-install descriptor mismatch")
|
|
|
|
root_stat = GITEA_ROOT.lstat()
|
|
if (
|
|
stat.S_ISLNK(root_stat.st_mode)
|
|
or not stat.S_ISDIR(root_stat.st_mode)
|
|
or root_stat.st_uid != 0
|
|
):
|
|
die("Gitea fresh-install root is unsafe")
|
|
os.chown(GITEA_ROOT, 0, 0)
|
|
GITEA_ROOT.chmod(0o755)
|
|
for directory, uid, gid, mode, label in (
|
|
(
|
|
GITEA_DATA_DIR,
|
|
GITEA_RUNTIME_UID,
|
|
GITEA_RUNTIME_GID,
|
|
0o750,
|
|
"data",
|
|
),
|
|
(
|
|
GITEA_CONFIG_DIR,
|
|
GITEA_RUNTIME_UID,
|
|
GITEA_RUNTIME_GID,
|
|
0o750,
|
|
"config",
|
|
),
|
|
(
|
|
GITEA_SOCKET_DIR,
|
|
GITEA_RUNTIME_UID,
|
|
GITEA_NGINX_GID,
|
|
0o750,
|
|
"socket",
|
|
),
|
|
(GITEA_SECRET_DIR, 0, GITEA_RUNTIME_GID, 0o710, "secret"),
|
|
):
|
|
if directory.exists() or directory.is_symlink():
|
|
die(f"Gitea fresh-install {label} directory already exists")
|
|
directory.mkdir(parents=False, exist_ok=False)
|
|
os.chown(directory, uid, gid)
|
|
directory.chmod(mode)
|
|
create_gitea_runtime_secret(GITEA_SECRET_KEY_FILE, "global")
|
|
create_gitea_runtime_secret(GITEA_INTERNAL_TOKEN_FILE, "internal token")
|
|
|
|
|
|
def prepare_component_runtime(component, entries=None):
|
|
if is_gitea_fresh_install_slice(component, entries):
|
|
prepare_gitea_fresh_runtime(entries)
|
|
return
|
|
if is_gitea_incident_salvage_slice(component, entries):
|
|
die(
|
|
"Gitea incident-salvage activation is frozen before candidate "
|
|
"root creation until every disposition verifier is implemented"
|
|
)
|
|
|
|
if is_engine_data_product_publish_grant_slice(component, entries):
|
|
ensure_engine_edp_managed_provisioner_keypair()
|
|
ensure_engine_publish_grant_private_state()
|
|
|
|
if is_engine_mcp_control_plane_slice(component, entries):
|
|
ensure_engine_edp_managed_provisioner_keypair()
|
|
ensure_engine_data_product_grant_private_state(include_reader=True)
|
|
|
|
if is_engine_n8n_transition(component, entries):
|
|
descriptor = current_engine_n8n_transition_descriptor()
|
|
if descriptor is None:
|
|
die("installed Engine n8n transition descriptor is missing")
|
|
if descriptor["action"] == "activate":
|
|
ensure_engine_n8n_sealed_release(descriptor, stamp())
|
|
return
|
|
|
|
if is_engine_node_intelligence_transition(component, entries):
|
|
ensure_engine_node_intelligence_secret()
|
|
descriptor = current_engine_node_intelligence_descriptor()
|
|
if descriptor is None:
|
|
die("installed Engine node-intelligence descriptor is missing")
|
|
if descriptor["action"] == "activate":
|
|
install_engine_node_intelligence_image(descriptor)
|
|
return
|
|
|
|
if touches_engine_credential_sink(component, entries):
|
|
ensure_engine_credential_issuer_keypair()
|
|
prepare_engine_credential_backend_runtime()
|
|
return
|
|
|
|
if component == "module-foundry":
|
|
ensure_map_gateway_admin_secret()
|
|
ensure_foundry_edp_managed_provisioner_keypair()
|
|
ensure_root_owned_grant_directory(
|
|
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
|
|
"external data plane reader grants",
|
|
)
|
|
ensure_root_owned_grant_directory(
|
|
FOUNDRY_BINDING_GRANTS_DIR,
|
|
"foundry binding grants",
|
|
)
|
|
return
|
|
|
|
if component == "device-plane":
|
|
ensure_platform_runtime_secret(
|
|
DEVICE_PLANE_POSTGRES_PASSWORD_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"device plane PostgreSQL",
|
|
)
|
|
ensure_platform_runtime_secret(
|
|
DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"device plane Gateway to Core",
|
|
)
|
|
ensure_platform_runtime_secret(
|
|
DEVICE_PLANE_IDENTIFIER_PEPPER_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"device plane identifier pepper",
|
|
)
|
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
|
ensure_platform_runtime_secret(
|
|
DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"device plane management to Core",
|
|
)
|
|
ensure_platform_runtime_secret(
|
|
PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"Device Core Hub handoff",
|
|
)
|
|
if is_device_plane_manager_persistent_release_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
ensure_device_plane_manager_persistent_data()
|
|
if (
|
|
is_device_plane_control_core_release_slice(component, entries)
|
|
or is_device_plane_edge_core_channel_bootstrap_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
):
|
|
ensure_platform_runtime_secret(
|
|
DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"device plane management to Core",
|
|
)
|
|
ensure_device_edge_channel_core_identity(
|
|
allow_invalid_unexported_recovery=(
|
|
not is_device_plane_control_core_release_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
and not is_device_plane_edge_core_channel_upgrade_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
)
|
|
if is_device_plane_backhaul_vps_enrollment_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
install_device_plane_backhaul_authorized_key(
|
|
read_device_plane_backhaul_vps_enrollment_public_key()
|
|
)
|
|
return
|
|
if is_device_plane_backhaul_target_slice(component, entries):
|
|
ensure_device_plane_backhaul_target_state()
|
|
return
|
|
|
|
if component == "proxy-contur":
|
|
sync_map_egress_proxy_secret()
|
|
return
|
|
|
|
if component == "dc-amd-proxy":
|
|
# The connector access value arrives only through the one-time pairing
|
|
# endpoint. The artifact must never carry it, and the service user is
|
|
# the only non-root identity that can write this directory.
|
|
sync_map_egress_proxy_secret()
|
|
try:
|
|
runtime_stat = DC_AMD_PROXY_RUNTIME_DIR.lstat()
|
|
except FileNotFoundError:
|
|
DC_AMD_PROXY_RUNTIME_DIR.mkdir(parents=True, exist_ok=False)
|
|
runtime_stat = DC_AMD_PROXY_RUNTIME_DIR.lstat()
|
|
if stat.S_ISLNK(runtime_stat.st_mode) or not stat.S_ISDIR(runtime_stat.st_mode):
|
|
die(f"dc-amd-proxy runtime directory is unsafe: {DC_AMD_PROXY_RUNTIME_DIR}")
|
|
os.chown(DC_AMD_PROXY_RUNTIME_DIR, MAP_GATEWAY_RUNTIME_GID, MAP_GATEWAY_RUNTIME_GID)
|
|
DC_AMD_PROXY_RUNTIME_DIR.chmod(0o700)
|
|
|
|
connector_state = DC_AMD_PROXY_RUNTIME_DIR / "connector-access"
|
|
if connector_state.exists() or connector_state.is_symlink():
|
|
connector_stat = connector_state.lstat()
|
|
if (
|
|
stat.S_ISLNK(connector_stat.st_mode)
|
|
or not stat.S_ISREG(connector_stat.st_mode)
|
|
or connector_stat.st_uid != MAP_GATEWAY_RUNTIME_GID
|
|
or connector_stat.st_mode & (stat.S_IRWXG | stat.S_IRWXO)
|
|
or connector_stat.st_size > 512
|
|
):
|
|
die("dc-amd-proxy connector pair state is unsafe")
|
|
return
|
|
|
|
if component == "platform" and entries is not None:
|
|
if is_platform_device_core_hub_trust_slice(component, entries):
|
|
ensure_platform_runtime_secret(
|
|
PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE,
|
|
MAP_GATEWAY_SECRET_RE,
|
|
"Device Core Hub handoff",
|
|
)
|
|
touches_map_gateway = any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries)
|
|
touches_external_data_plane = touches_external_data_plane_files(entries)
|
|
if touches_map_gateway:
|
|
# These NAS directories are the only canonical persistent stores
|
|
# for all Map Page instances. Creation lives in the root-owned
|
|
# runner, never in a user-supplied artifact or a Compose side effect.
|
|
cache_root = Path("/volume1/docker/nodedc-platform/map-gateway")
|
|
for cache_dir in (cache_root / "live-tile-cache", cache_root / "offline-snapshot"):
|
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
os.chown(cache_dir, 1000, 1000)
|
|
os.chmod(cache_dir, 0o750)
|
|
ensure_map_gateway_admin_secret()
|
|
sync_map_egress_proxy_secret()
|
|
if touches_external_data_plane:
|
|
ensure_external_data_plane_provisioner_secret()
|
|
ensure_engine_edp_managed_provisioner_keypair()
|
|
ensure_foundry_edp_managed_provisioner_keypair()
|
|
ensure_root_owned_grant_directory(
|
|
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
|
|
"external data plane reader grants",
|
|
)
|
|
return
|
|
|
|
if component in ("dc-cms", "dc-cms-site-nodedc"):
|
|
(Path("/volume1/docker/dc-cms/sites") / "nodedc").mkdir(parents=True, exist_ok=True)
|
|
return
|
|
|
|
if component != "bim-viewer":
|
|
return
|
|
|
|
data_root = component_root(component) / "server" / "data"
|
|
for rel in ("uploads", "projects", "shares", "models", "comments"):
|
|
(data_root / rel).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def run_component_runtime(component, entries, services):
|
|
if component == "platform" and is_platform_provider_catalog_only(entries):
|
|
return
|
|
if component_artifact_only(component):
|
|
return
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return
|
|
if is_device_plane_manager_reconciliation_slice(component, entries):
|
|
return
|
|
if is_device_plane_manager_v2_reconciliation_slice(component, entries):
|
|
return
|
|
if is_device_plane_foundation_recovery_slice(component, entries):
|
|
return
|
|
if is_device_plane_foundation_network_publication_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
prepare_component_runtime(component, entries)
|
|
run_compose(component, services, entries)
|
|
return
|
|
run_build(component, entries)
|
|
prepare_component_runtime(component, entries)
|
|
if is_gitea_fresh_install_slice(component, entries):
|
|
run_gitea_fresh_install_compose(services, entries)
|
|
elif is_gitea_incident_salvage_slice(component, entries):
|
|
die("Gitea incident-salvage runtime is not activation-ready")
|
|
elif is_engine_node_intelligence_transition(component, entries):
|
|
run_engine_node_intelligence_compose(services, entries)
|
|
else:
|
|
run_compose(component, services, entries)
|
|
|
|
|
|
def retag_device_plane_control_core_image(image_id, label):
|
|
image_id = inspect_optional_local_image(
|
|
image_id,
|
|
label,
|
|
)
|
|
if image_id is None:
|
|
die(f"{label} is unavailable")
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"image",
|
|
"tag",
|
|
image_id,
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
die(f"{label} retag failed")
|
|
tagged = inspect_optional_local_image(
|
|
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
|
f"{label} recovered tag",
|
|
)
|
|
if tagged != image_id:
|
|
die(f"{label} recovered tag mismatch")
|
|
|
|
|
|
def restore_device_plane_control_core_v3_preapply_image():
|
|
retag_device_plane_control_core_image(
|
|
DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID,
|
|
"Device Control Core v3 exact pre-apply image",
|
|
)
|
|
|
|
|
|
def run_device_plane_runtime_for_apply(
|
|
entries,
|
|
services,
|
|
mark_runtime_started,
|
|
backhaul_serve_before=None,
|
|
):
|
|
if is_device_plane_control_core_v3_reconciliation_slice(
|
|
"device-plane",
|
|
entries,
|
|
):
|
|
if tuple(services or ()) != ("device-control-core",):
|
|
die("Device Control Core v3 reconciliation service set mismatch")
|
|
prepare_component_runtime("device-plane", entries)
|
|
mark_runtime_started()
|
|
restore_device_plane_control_core_v3_preapply_image()
|
|
run_compose("device-plane", services, entries)
|
|
return
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
"device-plane",
|
|
entries,
|
|
) or is_device_plane_foundation_recovery_slice(
|
|
"device-plane", entries
|
|
) or is_device_plane_manager_reconciliation_slice(
|
|
"device-plane", entries
|
|
) or is_device_plane_manager_v2_reconciliation_slice(
|
|
"device-plane", entries
|
|
):
|
|
return
|
|
# Build and runtime preparation are pre-runtime phases. A failure here
|
|
# must restore source only; it must never trigger a predecessor rebuild.
|
|
run_build("device-plane", entries)
|
|
prepare_component_runtime("device-plane", entries)
|
|
if services:
|
|
mark_runtime_started()
|
|
run_compose("device-plane", services, entries)
|
|
if is_device_plane_backhaul_target_slice("device-plane", entries):
|
|
if backhaul_serve_before is None:
|
|
die("Device Plane backhaul Tailscale preflight state is missing")
|
|
# Expose nothing to the tailnet until the loopback-only SSH target is
|
|
# healthy. Tailscale Serve is the only ingress mutation and remains
|
|
# private to the tailnet; Docker publishes no host port.
|
|
healthcheck_compose_service(
|
|
"device-plane",
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,
|
|
)
|
|
enable_device_plane_tailscale_serve(backhaul_serve_before)
|
|
|
|
|
|
def healthcheck_url(check):
|
|
headers = {}
|
|
expected_json = {}
|
|
if isinstance(check, dict):
|
|
url = check.get("url")
|
|
headers = check.get("headers") or {}
|
|
expected_json = check.get("expected_json") or {}
|
|
else:
|
|
url = check
|
|
|
|
last_error = None
|
|
for _attempt in range(1, 61):
|
|
try:
|
|
request = urllib.request.Request(url, headers=headers)
|
|
with NO_REDIRECT_OPENER.open(request, timeout=10) as response:
|
|
if 200 <= response.status < 400:
|
|
if expected_json:
|
|
raw = response.read(64 * 1024 + 1)
|
|
if len(raw) > 64 * 1024:
|
|
last_error = "health response too large"
|
|
continue
|
|
try:
|
|
payload = json.loads(raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
last_error = "health response is not json"
|
|
continue
|
|
if not isinstance(payload, dict) or any(
|
|
payload.get(key) != expected
|
|
for key, expected in expected_json.items()
|
|
):
|
|
last_error = "health response contract mismatch"
|
|
continue
|
|
return
|
|
last_error = f"HTTP {response.status}"
|
|
except urllib.error.HTTPError as exc:
|
|
if 200 <= exc.code < 400 and not expected_json:
|
|
return
|
|
last_error = f"HTTP {exc.code}"
|
|
except Exception as exc:
|
|
last_error = str(exc)
|
|
time.sleep(5)
|
|
die(f"healthcheck failed for {url}: {last_error}")
|
|
|
|
|
|
def assert_loopback_tcp_port_closed(port):
|
|
try:
|
|
connection = socket.create_connection(("127.0.0.1", port), timeout=3)
|
|
except OSError:
|
|
return
|
|
connection.close()
|
|
die(f"unexpected loopback TCP listener is open: {port}")
|
|
|
|
|
|
def assert_loopback_tcp_port_open(port):
|
|
try:
|
|
connection = socket.create_connection(("127.0.0.1", port), timeout=3)
|
|
except OSError as exc:
|
|
die(f"expected loopback TCP listener is closed: {port}: {exc}")
|
|
connection.close()
|
|
|
|
|
|
def healthcheck_gitea_json():
|
|
url = "http://127.0.0.1:3000/api/healthz"
|
|
last_error = None
|
|
for attempt in range(1, 61):
|
|
try:
|
|
request = urllib.request.Request(
|
|
url,
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
with NO_REDIRECT_OPENER.open(request, timeout=10) as response:
|
|
raw = response.read(64 * 1024 + 1)
|
|
if not (200 <= response.status < 300):
|
|
last_error = f"HTTP {response.status}"
|
|
elif len(raw) > 64 * 1024:
|
|
last_error = "health response too large"
|
|
else:
|
|
try:
|
|
payload = json.loads(raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
last_error = "health response is not json"
|
|
else:
|
|
database = (
|
|
payload.get("checks", {}).get("database:ping")
|
|
if isinstance(payload, dict)
|
|
else None
|
|
)
|
|
if (
|
|
isinstance(payload, dict)
|
|
and payload.get("status") == "pass"
|
|
and isinstance(database, list)
|
|
and len(database) == 1
|
|
and isinstance(database[0], dict)
|
|
and database[0].get("status") == "pass"
|
|
):
|
|
return payload
|
|
last_error = "health response contract mismatch"
|
|
except urllib.error.HTTPError as exc:
|
|
last_error = f"HTTP {exc.code}"
|
|
except Exception as exc:
|
|
last_error = str(exc)
|
|
if attempt < 60:
|
|
time.sleep(5)
|
|
die(f"Gitea healthcheck failed for {url}: {last_error}")
|
|
|
|
|
|
def validate_gitea_socket_boundary():
|
|
try:
|
|
parent_stat = GITEA_SOCKET_DIR.lstat()
|
|
socket_stat = GITEA_SOCKET_FILE.lstat()
|
|
except FileNotFoundError:
|
|
die("Gitea Unix socket boundary is missing")
|
|
if (
|
|
GITEA_SOCKET_FILE.parent != GITEA_SOCKET_DIR
|
|
or stat.S_ISLNK(parent_stat.st_mode)
|
|
or not stat.S_ISDIR(parent_stat.st_mode)
|
|
or parent_stat.st_uid != GITEA_RUNTIME_UID
|
|
or parent_stat.st_gid != GITEA_NGINX_GID
|
|
or stat.S_IMODE(parent_stat.st_mode) != 0o750
|
|
or not stat.S_ISSOCK(socket_stat.st_mode)
|
|
or socket_stat.st_uid != GITEA_RUNTIME_UID
|
|
or socket_stat.st_gid != GITEA_RUNTIME_GID
|
|
or stat.S_IMODE(socket_stat.st_mode) != 0o666
|
|
):
|
|
die("Gitea Unix socket identity/metadata mismatch")
|
|
return str(GITEA_SOCKET_FILE)
|
|
|
|
|
|
def healthcheck_gitea_uds_json():
|
|
last_error = None
|
|
for attempt in range(1, 61):
|
|
connection = None
|
|
response = None
|
|
try:
|
|
connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
connection.settimeout(10)
|
|
connection.connect(str(GITEA_SOCKET_FILE))
|
|
connection.sendall(
|
|
b"GET /api/healthz HTTP/1.1\r\n"
|
|
b"Host: git.dcserve.ru\r\n"
|
|
b"Accept: application/json\r\n"
|
|
b"Connection: close\r\n\r\n"
|
|
)
|
|
response = http.client.HTTPResponse(connection)
|
|
response.begin()
|
|
raw = response.read(64 * 1024 + 1)
|
|
if not (200 <= response.status < 300):
|
|
last_error = f"HTTP {response.status}"
|
|
elif len(raw) > 64 * 1024:
|
|
last_error = "health response too large"
|
|
else:
|
|
try:
|
|
payload = json.loads(raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
last_error = "health response is not json"
|
|
else:
|
|
database = (
|
|
payload.get("checks", {}).get("database:ping")
|
|
if isinstance(payload, dict)
|
|
else None
|
|
)
|
|
if (
|
|
isinstance(payload, dict)
|
|
and payload.get("status") == "pass"
|
|
and isinstance(database, list)
|
|
and len(database) == 1
|
|
and isinstance(database[0], dict)
|
|
and database[0].get("status") == "pass"
|
|
):
|
|
return payload
|
|
last_error = "health response contract mismatch"
|
|
except (OSError, http.client.HTTPException) as exc:
|
|
last_error = str(exc)
|
|
finally:
|
|
if response is not None:
|
|
response.close()
|
|
if connection is not None:
|
|
connection.close()
|
|
if attempt < 60:
|
|
time.sleep(5)
|
|
die(f"Gitea UDS healthcheck failed: {last_error}")
|
|
|
|
|
|
def external_data_plane_healthcheck(require_managed=True):
|
|
expected_json = {
|
|
"ok": True,
|
|
"service": "nodedc-external-data-plane",
|
|
"database": "ready",
|
|
}
|
|
if require_managed:
|
|
expected_json["managedWriterBindingProvisioning"] = "enabled"
|
|
server_source = (
|
|
component_root("platform")
|
|
/ "platform/services/external-data-plane/src/server.mjs"
|
|
)
|
|
try:
|
|
server_text = server_source.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
server_text = ""
|
|
if 'managedReaderBindingProvisioning: config.managedProvisionerApiEnabled' in server_text:
|
|
expected_json["managedReaderBindingProvisioning"] = "enabled"
|
|
if 'managedWriterBindingLifetime: config.managedProvisionerApiEnabled ? "explicit-revoke" : "disabled"' in server_text:
|
|
expected_json["managedWriterBindingLifetime"] = "explicit-revoke"
|
|
if 'managedReaderBindingLifetime: config.managedProvisionerApiEnabled ? "explicit-revoke" : "disabled"' in server_text:
|
|
expected_json["managedReaderBindingLifetime"] = "explicit-revoke"
|
|
if 'foundryReaderBindingProvisioning: config.foundryProvisionerApiEnabled ? "digest+server-resolved-source" : "disabled"' in server_text:
|
|
expected_json["foundryReaderBindingProvisioning"] = "digest+server-resolved-source"
|
|
expected_json["foundryReaderBindingLifetime"] = "explicit-revoke"
|
|
return {
|
|
"url": "http://127.0.0.1:18106/healthz",
|
|
"expected_json": expected_json,
|
|
}
|
|
|
|
|
|
def module_foundry_healthcheck():
|
|
expected_json = {
|
|
"status": "ok",
|
|
"service": "nodedc-module-foundry",
|
|
}
|
|
server_source = component_root("module-foundry") / "server/catalog-server.mjs"
|
|
try:
|
|
server_text = server_source.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
server_text = ""
|
|
if "dataProductConsumerProvisioner" in server_text:
|
|
expected_json["dataProductConsumerProvisioner"] = {
|
|
"configured": True,
|
|
"auth": "dedicated-ed25519-service-identity",
|
|
"sourceScope": "external-data-plane-resolved",
|
|
}
|
|
return {
|
|
"url": "http://172.22.0.222:9920/healthz",
|
|
"expected_json": expected_json,
|
|
}
|
|
|
|
|
|
def component_healthchecks(component, entries=None, services=None):
|
|
if is_platform_device_core_hub_trust_slice(component, entries):
|
|
return ({
|
|
"url": "http://127.0.0.1:18080/healthz",
|
|
"headers": {"Host": "hub.nodedc.ru"},
|
|
},)
|
|
if is_platform_device_manager_public_route_slice(component, entries):
|
|
return ({
|
|
"url": "http://127.0.0.1:18080/healthz",
|
|
"headers": {"Host": "device.nodedc.ru"},
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-manager",
|
|
"authRequired": True,
|
|
"deviceCoreConfigured": True,
|
|
},
|
|
},)
|
|
if is_launcher_device_core_session_slice(component, entries):
|
|
return ({
|
|
"url": "http://127.0.0.1:18080/healthz",
|
|
"headers": {"Host": "hub.nodedc.ru"},
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-launcher-bff",
|
|
"deviceCoreInternalAccessConfigured": True,
|
|
},
|
|
},)
|
|
if is_device_plane_control_core_migration_replay_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ({
|
|
"url": "http://127.0.0.1:18120/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-control-core",
|
|
"database": "ready",
|
|
"discoveryIngest": "enabled",
|
|
"managementApi": "enabled",
|
|
"commandTransport": "typed-service-ping-v1",
|
|
},
|
|
},)
|
|
if is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return ({
|
|
"url": "http://127.0.0.1:18120/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-control-core",
|
|
"database": "ready",
|
|
"discoveryIngest": "enabled",
|
|
"managementApi": "enabled",
|
|
"commandTransport": "typed-service-ping-v1",
|
|
},
|
|
},)
|
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
|
command_transport = (
|
|
"typed-service-ping-v1"
|
|
if is_device_plane_manager_only_release_slice(component, entries)
|
|
else "disabled"
|
|
)
|
|
checks = ({
|
|
"url": "http://127.0.0.1:18120/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-control-core",
|
|
"database": "ready",
|
|
"discoveryIngest": "enabled",
|
|
"managementApi": "enabled",
|
|
"commandTransport": command_transport,
|
|
},
|
|
},)
|
|
if is_device_plane_manager_only_release_slice(component, entries):
|
|
checks += ({
|
|
"url": "http://127.0.0.1:18080/healthz",
|
|
"headers": {"Host": "device.nodedc.ru"},
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-manager",
|
|
"authRequired": True,
|
|
"deviceCoreConfigured": True,
|
|
},
|
|
},)
|
|
return checks
|
|
if (
|
|
is_device_plane_control_core_release_slice(component, entries)
|
|
or is_device_plane_edge_core_channel_bootstrap_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
):
|
|
return ({
|
|
"url": "http://127.0.0.1:18120/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-control-core",
|
|
"database": "ready",
|
|
"discoveryIngest": "enabled",
|
|
"managementApi": "enabled",
|
|
"commandTransport": (
|
|
"typed-service-ping-v1"
|
|
if (
|
|
is_device_plane_control_core_release_v2_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_device_plane_control_core_release_v3_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_device_plane_control_core_release_v4_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
else "disabled"
|
|
),
|
|
},
|
|
},)
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
return (
|
|
{
|
|
"url": "http://127.0.0.1:18120/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-control-core",
|
|
"database": "ready",
|
|
"discoveryIngest": "disabled",
|
|
"commandTransport": "disabled",
|
|
},
|
|
},
|
|
{
|
|
"url": "http://127.0.0.1:18121/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-gateway",
|
|
"tcpListener": "disabled",
|
|
"publicIngress": "disabled",
|
|
"commandTransport": "disabled",
|
|
},
|
|
},
|
|
)
|
|
if is_device_plane_postgres_bootstrap_slice(component, entries):
|
|
return ()
|
|
if is_device_plane_foundation_recovery_slice(component, entries):
|
|
return (
|
|
{
|
|
"url": "http://127.0.0.1:18120/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-control-core",
|
|
"database": "ready",
|
|
"discoveryIngest": "disabled",
|
|
"commandTransport": "disabled",
|
|
},
|
|
},
|
|
{
|
|
"url": "http://127.0.0.1:18121/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-gateway",
|
|
"tcpListener": "disabled",
|
|
"publicIngress": "disabled",
|
|
"commandTransport": "disabled",
|
|
},
|
|
},
|
|
)
|
|
if is_engine_n8n_transition(component, entries):
|
|
# The transition does not restart the Engine UI/backend generation.
|
|
# Its own acceptance below verifies n8n readiness, image, mount, logs,
|
|
# live node export and the pinned Engine MCP catalogs.
|
|
return ()
|
|
if is_engine_node_intelligence_transition(component, entries):
|
|
# The exact sidecar/backend topology and live MCP capability calls are
|
|
# accepted separately below. Keep the generic HTTP barrier scoped to
|
|
# the backend generation recreated by this transition.
|
|
return ("http://127.0.0.1:3001/health",)
|
|
if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
|
|
return ()
|
|
if component == "module-foundry":
|
|
return (module_foundry_healthcheck(),)
|
|
if component == "device-plane":
|
|
if is_device_plane_b2_discovery_ingress_slice(component, entries):
|
|
return (
|
|
{
|
|
"url": "http://127.0.0.1:18120/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-control-core",
|
|
"database": "ready",
|
|
"discoveryIngest": "enabled",
|
|
"commandTransport": "disabled",
|
|
},
|
|
},
|
|
{
|
|
"url": "http://127.0.0.1:18121/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-gateway",
|
|
"framing": "verified-read-only",
|
|
"tcpListener": "discovery-only",
|
|
"publicIngress": "disabled",
|
|
"commandTransport": "disabled",
|
|
},
|
|
},
|
|
)
|
|
selected_services = (
|
|
tuple(services)
|
|
if services is not None
|
|
else component_services(component, entries)
|
|
)
|
|
checks = []
|
|
if "device-control-core" in selected_services:
|
|
checks.append({
|
|
"url": "http://127.0.0.1:18120/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-control-core",
|
|
"database": "ready",
|
|
"discoveryIngest": "disabled",
|
|
"commandTransport": "disabled",
|
|
},
|
|
})
|
|
if "device-gateway" in selected_services:
|
|
checks.append({
|
|
"url": "http://127.0.0.1:18121/healthz",
|
|
"expected_json": {
|
|
"ok": True,
|
|
"service": "nodedc-device-gateway",
|
|
"tcpListener": "disabled",
|
|
"publicIngress": "disabled",
|
|
"commandTransport": "disabled",
|
|
},
|
|
})
|
|
return tuple(checks)
|
|
if (
|
|
(
|
|
is_engine_data_product_publish_grant_slice(component, entries)
|
|
or is_engine_composite_provider_v4_slice(component, entries)
|
|
or is_engine_provider_rotating_slot_slice(component, entries)
|
|
or is_engine_provider_authority_diagnostics_slice(component, entries)
|
|
or is_engine_depttrans_zone_authority_v1_slice(component, entries)
|
|
or is_engine_provider_target_host_policy_slice(component, entries)
|
|
or is_engine_mcp_execution_profile_decoder_slice(component, entries)
|
|
or is_engine_mcp_telemetry_catalog_slice(component, entries)
|
|
or is_engine_mcp_execution_plan_materialization_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_execution_plan_module_ownership_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_normalized_identity_search_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_l1_credential_reuse_slice(component, entries)
|
|
or is_engine_mcp_l1_credential_provenance_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_gelios_items_envelope_slice(component, entries)
|
|
or is_engine_mcp_registered_execution_profiles_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_gelios_units_items_slice(component, entries)
|
|
or is_engine_agent_full_grant_migration_slice(component, entries)
|
|
or is_engine_mcp_control_plane_slice(component, entries)
|
|
or is_engine_mcp_ontology_sdk_slice(component, entries)
|
|
or is_engine_mcp_autonomy_provider_v5_slice(component, entries)
|
|
or is_engine_provider_security_catalog_slice(component, entries)
|
|
)
|
|
and services is not None
|
|
):
|
|
selected_services = set(services)
|
|
checks = []
|
|
if "app" in selected_services:
|
|
checks.append("http://127.0.0.1:8080/")
|
|
if "nodedc-backend" in selected_services:
|
|
checks.append("http://127.0.0.1:3001/health")
|
|
if is_engine_agent_full_grant_migration_slice(component, entries):
|
|
checks.append("http://127.0.0.1:3001/internal/engine-credential-sink/v1/health")
|
|
return tuple(checks)
|
|
if component == "platform" and entries is not None:
|
|
selected_services = tuple(services) if services is not None else component_services(component, entries)
|
|
# A narrow EDP application deploy must prove only the service it
|
|
# recreated. Unrelated Platform routes retain independent lifecycle and
|
|
# must not turn a valid EDP deploy or rollback into a false failure.
|
|
if selected_services == (EXTERNAL_DATA_PLANE_SERVICE,):
|
|
return (external_data_plane_healthcheck(),)
|
|
checks = list(COMPONENTS[component].get("healthchecks", ()))
|
|
if touches_engine_credential_sink(component, entries):
|
|
checks.append("http://127.0.0.1:3001/internal/engine-credential-sink/v1/health")
|
|
if component == "platform" and entries is not None:
|
|
touches_compose = any(rel == "platform/docker-compose.platform-http.yml" for rel in entries)
|
|
touches_ai_workspace_assistant = any(rel == "platform/ai-workspace-assistant" or rel.startswith("platform/ai-workspace-assistant/") for rel in entries)
|
|
touches_ontology = any(rel == "platform/ontology-core" or rel.startswith("platform/ontology-core/") for rel in entries)
|
|
touches_gelios = any(rel == "platform/gelios-gateway" or rel.startswith("platform/gelios-gateway/") for rel in entries)
|
|
touches_map_gateway = any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries)
|
|
touches_external_data_plane = touches_external_data_plane_files(entries)
|
|
map_gateway_only_compose = touches_compose and touches_map_gateway and not any((touches_ai_workspace_assistant, touches_ontology, touches_gelios, touches_external_data_plane))
|
|
healthcheck_all_for_compose = touches_compose and not map_gateway_only_compose
|
|
if healthcheck_all_for_compose or touches_ai_workspace_assistant:
|
|
checks.append("http://127.0.0.1:18082/healthz")
|
|
if healthcheck_all_for_compose or touches_ontology:
|
|
checks.append("http://127.0.0.1:18104/healthz")
|
|
if healthcheck_all_for_compose or touches_gelios:
|
|
checks.append("http://127.0.0.1:18105/healthz")
|
|
if touches_map_gateway:
|
|
checks.append({"url": "http://127.0.0.1:18103/healthz", "headers": {"x-nodedc-user-id": "deploy-healthcheck"}})
|
|
if touches_external_data_plane:
|
|
checks.append(external_data_plane_healthcheck())
|
|
return tuple(checks)
|
|
|
|
|
|
def healthcheck_container(container_name):
|
|
last_status = "unknown"
|
|
for _attempt in range(1, 61):
|
|
result = subprocess.run(
|
|
[str(DOCKER), "inspect", "--format", "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}", container_name],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
status_text = result.stdout.strip().lower()
|
|
last_status = status_text or result.stderr.strip() or f"inspect-exit-{result.returncode}"
|
|
if status_text in ("healthy", "running"):
|
|
return
|
|
if status_text in ("unhealthy", "exited", "dead"):
|
|
break
|
|
time.sleep(5)
|
|
die(f"container healthcheck failed for {container_name}: {last_status}")
|
|
|
|
|
|
def healthcheck_container_with_grace(container_name):
|
|
"""Wait through bounded transient unhealthy/exited states.
|
|
|
|
Device Manager activation and its baseline rollback both recreate a Core
|
|
generation while Docker health probes and restart policy converge. The
|
|
ordinary helper deliberately fails on the first terminal-looking state;
|
|
this exact transition instead requires a healthy result within the same
|
|
bounded five-minute window.
|
|
"""
|
|
last_status = "unknown"
|
|
for attempt in range(1, 61):
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"inspect",
|
|
"--format",
|
|
"{{if .State.Health}}{{.State.Health.Status}}"
|
|
"{{else}}{{.State.Status}}{{end}}",
|
|
container_name,
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
status_text = result.stdout.strip().lower()
|
|
last_status = (
|
|
status_text
|
|
or result.stderr.strip()
|
|
or f"inspect-exit-{result.returncode}"
|
|
)
|
|
if status_text in ("healthy", "running"):
|
|
return
|
|
if attempt < 60:
|
|
time.sleep(5)
|
|
die(
|
|
"container healthcheck grace exhausted for "
|
|
f"{container_name}: {last_status}"
|
|
)
|
|
|
|
|
|
def compose_service_container_id(component, service):
|
|
result = subprocess.run(
|
|
[*compose_base_cmd(component), "ps", "-q", service],
|
|
cwd=str(component_compose_root(component)),
|
|
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"Compose service container lookup failed: {service}")
|
|
return container_ids[0]
|
|
|
|
|
|
def healthcheck_compose_service(component, service):
|
|
healthcheck_container(compose_service_container_id(component, service))
|
|
|
|
|
|
def healthcheck_compose_service_with_grace(component, service):
|
|
healthcheck_container_with_grace(
|
|
compose_service_container_id(component, service)
|
|
)
|
|
|
|
|
|
def container_environment(container, label):
|
|
environment = {}
|
|
for raw in (container.get("Config") or {}).get("Env") or []:
|
|
if not isinstance(raw, str) or "=" not in raw:
|
|
die(f"{label} environment inventory is invalid")
|
|
key, value = raw.split("=", 1)
|
|
if not key or key in environment:
|
|
die(f"{label} environment inventory is invalid")
|
|
environment[key] = value
|
|
return environment
|
|
|
|
|
|
def validate_gitea_runtime_secret_metadata():
|
|
try:
|
|
secret_dir_stat = GITEA_SECRET_DIR.lstat()
|
|
except FileNotFoundError:
|
|
die("Gitea runtime secret directory is missing")
|
|
if (
|
|
stat.S_ISLNK(secret_dir_stat.st_mode)
|
|
or not stat.S_ISDIR(secret_dir_stat.st_mode)
|
|
or secret_dir_stat.st_uid != 0
|
|
or secret_dir_stat.st_gid != GITEA_RUNTIME_GID
|
|
or stat.S_IMODE(secret_dir_stat.st_mode) != 0o710
|
|
):
|
|
die("Gitea runtime secret directory metadata mismatch")
|
|
values = []
|
|
for path, label in (
|
|
(GITEA_SECRET_KEY_FILE, "global"),
|
|
(GITEA_INTERNAL_TOKEN_FILE, "internal token"),
|
|
):
|
|
try:
|
|
path_stat = path.lstat()
|
|
value = path.read_text(encoding="ascii").strip()
|
|
except (FileNotFoundError, UnicodeDecodeError):
|
|
die(f"Gitea {label} secret is unreadable")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISREG(path_stat.st_mode)
|
|
or path_stat.st_uid != GITEA_RUNTIME_UID
|
|
or path_stat.st_gid != GITEA_RUNTIME_GID
|
|
or stat.S_IMODE(path_stat.st_mode) != 0o400
|
|
or path_stat.st_size > 512
|
|
or not GITEA_SECRET_RE.fullmatch(value)
|
|
):
|
|
die(f"Gitea {label} secret metadata mismatch")
|
|
values.append(value)
|
|
if len(set(values)) != len(values):
|
|
die("Gitea runtime secrets must be distinct")
|
|
return tuple(values)
|
|
|
|
|
|
def validate_gitea_runtime_directory_metadata():
|
|
for path, uid, gid, mode, label in (
|
|
(GITEA_ROOT, 0, 0, 0o755, "root"),
|
|
(GITEA_DATA_DIR, GITEA_RUNTIME_UID, GITEA_RUNTIME_GID, 0o750, "data"),
|
|
(
|
|
GITEA_CONFIG_DIR,
|
|
GITEA_RUNTIME_UID,
|
|
GITEA_RUNTIME_GID,
|
|
0o750,
|
|
"config",
|
|
),
|
|
(
|
|
GITEA_SOCKET_DIR,
|
|
GITEA_RUNTIME_UID,
|
|
GITEA_NGINX_GID,
|
|
0o750,
|
|
"socket",
|
|
),
|
|
):
|
|
try:
|
|
path_stat = path.lstat()
|
|
except FileNotFoundError:
|
|
die(f"Gitea runtime {label} directory is missing")
|
|
if (
|
|
stat.S_ISLNK(path_stat.st_mode)
|
|
or not stat.S_ISDIR(path_stat.st_mode)
|
|
or path_stat.st_uid != uid
|
|
or path_stat.st_gid != gid
|
|
or stat.S_IMODE(path_stat.st_mode) != mode
|
|
):
|
|
die(f"Gitea runtime {label} directory metadata mismatch")
|
|
|
|
|
|
def parse_gitea_app_ini_explicit(raw):
|
|
allowed_root_keys = {"APP_NAME", "RUN_USER", "RUN_MODE", "WORK_PATH"}
|
|
root_values = {}
|
|
sections = {}
|
|
section_names = {}
|
|
current = None
|
|
for line_number, raw_line in enumerate(raw.splitlines(), start=1):
|
|
line = raw_line.strip()
|
|
if not line or line.startswith(("#", ";")):
|
|
continue
|
|
section_match = re.fullmatch(r"\[([A-Za-z0-9_.-]+)\]", line)
|
|
if section_match:
|
|
section = section_match.group(1)
|
|
normalized = section.casefold()
|
|
if normalized == "default" or normalized in section_names:
|
|
die("Gitea installed app.ini contains duplicate/unsafe section")
|
|
section_names[normalized] = section
|
|
sections[section] = {}
|
|
current = section
|
|
continue
|
|
setting_match = re.fullmatch(r"([A-Za-z0-9_.-]+)\s*=\s*(.*)", line)
|
|
if setting_match is None:
|
|
die(f"Gitea installed app.ini syntax mismatch at line {line_number}")
|
|
key, value = setting_match.groups()
|
|
destination = root_values if current is None else sections[current]
|
|
normalized_key = key.casefold()
|
|
if any(existing.casefold() == normalized_key for existing in destination):
|
|
die("Gitea installed app.ini contains duplicate option")
|
|
if current is None and key not in allowed_root_keys:
|
|
die("Gitea installed app.ini contains unsafe root-level option")
|
|
destination[key] = value.strip()
|
|
if not sections:
|
|
die("Gitea installed app.ini has no explicit sections")
|
|
return root_values, sections
|
|
|
|
|
|
def validate_gitea_installed_config(secret_values):
|
|
app_ini = GITEA_CONFIG_DIR / "app.ini"
|
|
try:
|
|
app_ini_stat = app_ini.lstat()
|
|
raw = app_ini.read_text(encoding="utf-8")
|
|
except (FileNotFoundError, UnicodeDecodeError):
|
|
die("Gitea installed app.ini is unreadable")
|
|
if (
|
|
stat.S_ISLNK(app_ini_stat.st_mode)
|
|
or not stat.S_ISREG(app_ini_stat.st_mode)
|
|
or app_ini_stat.st_uid != GITEA_RUNTIME_UID
|
|
or app_ini_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)
|
|
or app_ini_stat.st_size < 32
|
|
or app_ini_stat.st_size > 1024 * 1024
|
|
):
|
|
die("Gitea installed app.ini metadata mismatch")
|
|
_root_values, sections = parse_gitea_app_ini_explicit(raw)
|
|
expected = {
|
|
("database", "DB_TYPE"): "sqlite3",
|
|
("database", "PATH"): "/var/lib/gitea/data/gitea.db",
|
|
("server", "ROOT_URL"): "https://git.dcserve.ru/",
|
|
("server", "PROTOCOL"): "http+unix",
|
|
("server", "HTTP_ADDR"): "/run/gitea/gitea.sock",
|
|
("server", "UNIX_SOCKET_PERMISSION"): "0666",
|
|
("server", "LOCAL_ROOT_URL"): "http://unix/",
|
|
("server", "DISABLE_SSH"): "true",
|
|
("server", "START_SSH_SERVER"): "false",
|
|
("server", "LFS_START_SERVER"): "false",
|
|
("server", "LFS_ALLOW_PURE_SSH"): "false",
|
|
("security", "INSTALL_LOCK"): "true",
|
|
("security", "SECRET_KEY_URI"): "file:/run/secrets/gitea_secret_key",
|
|
("security", "INTERNAL_TOKEN_URI"): (
|
|
"file:/run/secrets/gitea_internal_token"
|
|
),
|
|
("security", "REVERSE_PROXY_LIMIT"): "1",
|
|
("security", "REVERSE_PROXY_TRUSTED_PROXIES"): (
|
|
"127.0.0.0/8,::1/128"
|
|
),
|
|
("security", "DISABLE_GIT_HOOKS"): "true",
|
|
("security", "DISABLE_WEBHOOKS"): "true",
|
|
("security", "IMPORT_LOCAL_PATHS"): "false",
|
|
("security", "TWO_FACTOR_AUTH"): "enforced",
|
|
("security", "DISABLE_QUERY_AUTH_TOKEN"): "true",
|
|
("security", "ALLOWED_HOST_LIST"): "loopback",
|
|
("service", "DISABLE_REGISTRATION"): "true",
|
|
("service", "REQUIRE_SIGNIN_VIEW"): "true",
|
|
("service", "DEFAULT_USER_IS_RESTRICTED"): "true",
|
|
("service", "ENABLE_REVERSE_PROXY_AUTHENTICATION"): "false",
|
|
("service", "ENABLE_REVERSE_PROXY_AUTHENTICATION_API"): "false",
|
|
("service", "ENABLE_REVERSE_PROXY_AUTO_REGISTRATION"): "false",
|
|
("service", "ENABLE_BASIC_AUTHENTICATION"): "false",
|
|
("admin", "DISABLE_REGULAR_ORG_CREATION"): "true",
|
|
("admin", "USER_DISABLED_FEATURES"): (
|
|
"deletion,manage_ssh_keys,manage_gpg_keys,change_username"
|
|
),
|
|
("repository", "FORCE_PRIVATE"): "true",
|
|
("repository", "USER_MAX_CREATION_LIMIT"): "0",
|
|
("repository", "ORG_MAX_CREATION_LIMIT"): "0",
|
|
("repository", "DISABLE_MIGRATIONS"): "true",
|
|
("actions", "ENABLED"): "false",
|
|
("packages", "ENABLED"): "false",
|
|
("oauth2", "ENABLED"): "false",
|
|
("openid", "ENABLE_OPENID_SIGNIN"): "false",
|
|
("openid", "ENABLE_OPENID_SIGNUP"): "false",
|
|
("federation", "ENABLED"): "false",
|
|
}
|
|
for (section, key), value in expected.items():
|
|
if sections.get(section, {}).get(key) != value:
|
|
die(f"Gitea installed config mismatch: {section}.{key}")
|
|
for section, key in (
|
|
("security", "SECRET_KEY"),
|
|
("security", "INTERNAL_TOKEN"),
|
|
("server", "LFS_JWT_SECRET"),
|
|
("server", "LFS_JWT_SECRET_URI"),
|
|
):
|
|
if (sections.get(section, {}).get(key, "") or "").strip():
|
|
die(f"Gitea installed config contains plaintext secret: {section}.{key}")
|
|
if any(value in raw for value in secret_values):
|
|
die("Gitea installed config contains runner-managed secret bytes")
|
|
|
|
|
|
def validate_gitea_fresh_sqlite():
|
|
database = GITEA_DATA_DIR / "data" / "gitea.db"
|
|
try:
|
|
database_stat = database.lstat()
|
|
except FileNotFoundError:
|
|
die("Gitea fresh SQLite database is missing")
|
|
if (
|
|
stat.S_ISLNK(database_stat.st_mode)
|
|
or not stat.S_ISREG(database_stat.st_mode)
|
|
or database_stat.st_uid != GITEA_RUNTIME_UID
|
|
or database_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)
|
|
or database_stat.st_size < 4096
|
|
):
|
|
die("Gitea fresh SQLite database metadata mismatch")
|
|
connection = None
|
|
try:
|
|
connection = sqlite3.connect(
|
|
f"file:{database}?mode=ro",
|
|
uri=True,
|
|
timeout=10,
|
|
)
|
|
connection.execute("PRAGMA query_only = ON")
|
|
integrity = connection.execute("PRAGMA quick_check(1)").fetchone()
|
|
tables = {
|
|
row[0]
|
|
for row in connection.execute(
|
|
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
|
)
|
|
}
|
|
if integrity != ("ok",) or not {"user", "repository"}.issubset(tables):
|
|
die("Gitea fresh SQLite schema/integrity mismatch")
|
|
counts = {
|
|
table: connection.execute(
|
|
f'SELECT COUNT(*) FROM "{table}"'
|
|
).fetchone()[0]
|
|
for table in ("user", "repository")
|
|
}
|
|
except sqlite3.Error as exc:
|
|
die(f"Gitea fresh SQLite acceptance failed: {exc}")
|
|
finally:
|
|
if connection is not None:
|
|
connection.close()
|
|
if counts != {"user": 0, "repository": 0}:
|
|
die("Gitea fresh SQLite unexpectedly contains users or repositories")
|
|
return counts
|
|
|
|
|
|
def accept_gitea_fresh_install_runtime():
|
|
container_id = compose_service_container_id("gitea", GITEA_SERVICE)
|
|
healthcheck_container(container_id)
|
|
validate_gitea_socket_boundary()
|
|
healthcheck_gitea_uds_json()
|
|
nginx_bridge = validate_gitea_nginx_bridge_prerequisite()
|
|
healthcheck_gitea_json()
|
|
validate_gitea_no_docker_port_publications()
|
|
assert_loopback_tcp_port_closed(GITEA_DISABLED_SSH_HOST_PORT)
|
|
legacy_container = validate_legacy_gitea_container_isolation()
|
|
|
|
image_id = inspect_gitea_local_image()
|
|
container = inspect_container(container_id)
|
|
config = container.get("Config") or {}
|
|
host = container.get("HostConfig") or {}
|
|
labels = config.get("Labels") or {}
|
|
if (
|
|
container.get("Image") != image_id
|
|
or config.get("Image") != GITEA_IMAGE
|
|
or config.get("User") != "1000:1000"
|
|
or labels.get("com.docker.compose.project") != GITEA_COMPOSE_PROJECT
|
|
or labels.get("com.docker.compose.service") != GITEA_SERVICE
|
|
):
|
|
die("Gitea runtime identity mismatch")
|
|
environment = container_environment(container, "Gitea runtime")
|
|
for key, expected in GITEA_EXPECTED_ENVIRONMENT.items():
|
|
if environment.get(key) != expected:
|
|
die(f"Gitea runtime environment mismatch: {key}")
|
|
for forbidden_key in (
|
|
"GITEA__security__SECRET_KEY",
|
|
"GITEA__security__INTERNAL_TOKEN",
|
|
"GITEA__server__LFS_JWT_SECRET",
|
|
"GITEA__server__LFS_JWT_SECRET_URI",
|
|
"GITEA__security__SECRET_KEY__FILE",
|
|
"GITEA__security__INTERNAL_TOKEN__FILE",
|
|
"GITEA__server__LFS_JWT_SECRET__FILE",
|
|
):
|
|
if forbidden_key in environment:
|
|
die(f"Gitea runtime plaintext secret environment rejected: {forbidden_key}")
|
|
|
|
if (
|
|
host.get("Privileged") is not False
|
|
or host.get("ReadonlyRootfs") is not True
|
|
or host.get("NetworkMode") != "none"
|
|
or (host.get("RestartPolicy") or {}).get("Name") != "unless-stopped"
|
|
or host.get("PidsLimit") != 512
|
|
or host.get("Init") is not True
|
|
or config.get("StopTimeout") != 30
|
|
or "ALL" not in set(host.get("CapDrop") or ())
|
|
or not any(
|
|
value in ("no-new-privileges", "no-new-privileges:true")
|
|
for value in (host.get("SecurityOpt") or ())
|
|
)
|
|
):
|
|
die("Gitea runtime hardening mismatch")
|
|
if host.get("LogConfig") != {
|
|
"Type": "json-file",
|
|
"Config": {"max-file": "3", "max-size": "10m"},
|
|
}:
|
|
die("Gitea runtime bounded logging mismatch")
|
|
if (host.get("PortBindings") or {}) != {}:
|
|
die("Gitea runtime port publication mismatch")
|
|
tmpfs = host.get("Tmpfs") or {}
|
|
tmpfs_options = tmpfs.get("/tmp") or ""
|
|
if not all(
|
|
option in tmpfs_options
|
|
for option in ("rw", "noexec", "nosuid", "nodev", "size=268435456")
|
|
):
|
|
die("Gitea runtime tmpfs boundary mismatch")
|
|
|
|
expected_bind_mounts = {
|
|
(str(GITEA_DATA_DIR), "/var/lib/gitea", True),
|
|
(str(GITEA_CONFIG_DIR), "/etc/gitea", True),
|
|
(str(GITEA_SECRET_KEY_FILE), "/run/secrets/gitea_secret_key", False),
|
|
(
|
|
str(GITEA_INTERNAL_TOKEN_FILE),
|
|
"/run/secrets/gitea_internal_token",
|
|
False,
|
|
),
|
|
(str(GITEA_SOCKET_DIR), "/run/gitea", True),
|
|
}
|
|
bind_mounts = {
|
|
(mount.get("Source"), mount.get("Destination"), mount.get("RW"))
|
|
for mount in (container.get("Mounts") or ())
|
|
if mount.get("Type") == "bind"
|
|
}
|
|
if bind_mounts != expected_bind_mounts:
|
|
die("Gitea runtime mount boundary mismatch")
|
|
|
|
network_settings = container.get("NetworkSettings") or {}
|
|
attached_networks = network_settings.get("Networks") or {}
|
|
if set(attached_networks) != {"none"}:
|
|
die("Gitea runtime must not attach to a Docker network")
|
|
none_network = attached_networks["none"]
|
|
builtin_none = inspect_gitea_builtin_none_network()
|
|
if (
|
|
not isinstance(none_network, dict)
|
|
or none_network.get("NetworkID") != builtin_none["Id"]
|
|
or not re.fullmatch(
|
|
r"[a-f0-9]{64}",
|
|
str(none_network.get("EndpointID") or ""),
|
|
)
|
|
or any(
|
|
none_network.get(key) not in (None, "", 0, [], {})
|
|
for key in (
|
|
"IPAMConfig",
|
|
"Links",
|
|
"Aliases",
|
|
"DriverOpts",
|
|
"GwPriority",
|
|
"Gateway",
|
|
"IPAddress",
|
|
"MacAddress",
|
|
"IPPrefixLen",
|
|
"IPv6Gateway",
|
|
"GlobalIPv6Address",
|
|
"GlobalIPv6PrefixLen",
|
|
"DNSNames",
|
|
)
|
|
)
|
|
):
|
|
die("Gitea Docker none-network endpoint is not isolated")
|
|
full_container_id = container.get("Id")
|
|
none_container = (builtin_none.get("Containers") or {}).get(full_container_id)
|
|
if (
|
|
not isinstance(full_container_id, str)
|
|
or not re.fullmatch(r"[a-f0-9]{64}", full_container_id)
|
|
or not isinstance(none_container, dict)
|
|
or none_container.get("EndpointID") != none_network.get("EndpointID")
|
|
or any(
|
|
none_container.get(key) not in (None, "")
|
|
for key in ("MacAddress", "IPv4Address", "IPv6Address")
|
|
)
|
|
):
|
|
die("Gitea built-in none network membership mismatch")
|
|
validate_gitea_legacy_candidate_network_absent()
|
|
|
|
validate_gitea_runtime_directory_metadata()
|
|
secret_values = validate_gitea_runtime_secret_metadata()
|
|
validate_gitea_installed_config(secret_values)
|
|
database_counts = validate_gitea_fresh_sqlite()
|
|
return {
|
|
"container_id": container_id,
|
|
"image_id": image_id,
|
|
"transport": str(GITEA_SOCKET_FILE),
|
|
"network_mode": "none",
|
|
"nginx_bridge": nginx_bridge,
|
|
"database": "fresh-sqlite",
|
|
"database_counts": database_counts,
|
|
"legacy_container": legacy_container,
|
|
}
|
|
|
|
|
|
def validate_engine_node_intelligence_secret_mount(container, label):
|
|
matches = [
|
|
mount
|
|
for mount in (container.get("Mounts") or [])
|
|
if isinstance(mount, dict)
|
|
and mount.get("Destination") == ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH
|
|
]
|
|
if len(matches) != 1:
|
|
die(f"{label} secret mount inventory mismatch")
|
|
mount = matches[0]
|
|
if (
|
|
mount.get("Type") != "bind"
|
|
or mount.get("Source") != str(ENGINE_NODE_INTELLIGENCE_SECRET_FILE)
|
|
or mount.get("RW") is not False
|
|
):
|
|
die(f"{label} secret mount boundary mismatch")
|
|
|
|
|
|
def assert_engine_node_intelligence_container_absent():
|
|
result = subprocess.run(
|
|
[
|
|
str(DOCKER),
|
|
"container",
|
|
"ls",
|
|
"--all",
|
|
"--filter",
|
|
f"label=com.docker.compose.service={ENGINE_NODE_INTELLIGENCE_SERVICE}",
|
|
"--format",
|
|
"{{.ID}}",
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0 or result.stdout.strip():
|
|
die("Engine node-intelligence inactive runtime still has a sidecar container")
|
|
|
|
|
|
def engine_node_intelligence_live_probe_script():
|
|
return """
|
|
const {createNodeIntelligenceMcpClient}=await import('file:///app/server/nodeIntelligence/upstreamMcpClient.js');
|
|
const client=createNodeIntelligenceMcpClient();
|
|
const status=await client.status();
|
|
if(!status.ok||status.status!=='ready'||status.authSource!=='file'||status.server?.version!=='2.33.2'||!Object.values(status.capabilities||{}).every(Boolean))throw new Error('status');
|
|
const guidance=await client.callTool('get_node',{nodeType:'nodes-base.httpRequest',detail:'minimal'});
|
|
if(!guidance||typeof guidance!=='object')throw new Error('guidance');
|
|
const nodeValidation=await client.callTool('validate_node',{nodeType:'nodes-base.httpRequest',config:{url:'https://example.com'},mode:'minimal',profile:'ai-friendly'});
|
|
if(!nodeValidation||typeof nodeValidation!=='object'||nodeValidation.valid!==true)throw new Error('node-validation');
|
|
const workflowValidation=await client.callTool('validate_workflow',{workflow:{name:'NDC acceptance',nodes:[{id:'probe-1',name:'Start',type:'nodes-base.manualTrigger',typeVersion:1,position:[0,0],parameters:{}},{id:'probe-2',name:'Output',type:'nodes-base.set',typeVersion:3.4,position:[240,0],parameters:{}}],connections:{Start:{main:[[{node:'Output',type:'main',index:0}]]}},settings:{}},options:{validateNodes:true,validateConnections:true,validateExpressions:true,profile:'runtime'}});
|
|
if(!workflowValidation||typeof workflowValidation!=='object'||workflowValidation.valid!==true)throw new Error('workflow-validation');
|
|
process.stdout.write('node-intelligence-live:2.33.2:guidance+node+workflow');
|
|
""".strip()
|
|
|
|
|
|
def accept_engine_node_intelligence_runtime(descriptor):
|
|
if descriptor is None or descriptor.get("action") != "activate":
|
|
die("Engine node-intelligence activation descriptor is required")
|
|
validate_installed_engine_node_intelligence_source(descriptor)
|
|
ensure_engine_node_intelligence_secret()
|
|
image = inspect_engine_node_intelligence_image(descriptor)
|
|
|
|
sidecar_id = compose_service_container_id(
|
|
"engine",
|
|
ENGINE_NODE_INTELLIGENCE_SERVICE,
|
|
)
|
|
backend_id = compose_service_container_id("engine", "nodedc-backend")
|
|
healthcheck_container(sidecar_id)
|
|
healthcheck_container(backend_id)
|
|
sidecar = inspect_container(sidecar_id)
|
|
backend = inspect_container(backend_id)
|
|
|
|
sidecar_config = sidecar.get("Config") or {}
|
|
sidecar_host = sidecar.get("HostConfig") or {}
|
|
sidecar_environment = container_environment(
|
|
sidecar,
|
|
"Engine node-intelligence sidecar",
|
|
)
|
|
if (
|
|
sidecar.get("Image") != image["Id"]
|
|
or sidecar_config.get("Image") != ENGINE_NODE_INTELLIGENCE_IMAGE
|
|
or sidecar_config.get("User")
|
|
!= f"{ENGINE_NODE_INTELLIGENCE_RUNTIME_UID}:{ENGINE_NODE_INTELLIGENCE_RUNTIME_GID}"
|
|
or sidecar_host.get("ReadonlyRootfs") is not True
|
|
or sidecar_host.get("PortBindings") not in (None, {})
|
|
or set(sidecar_host.get("CapDrop") or []) != {"ALL"}
|
|
or "no-new-privileges:true" not in (sidecar_host.get("SecurityOpt") or [])
|
|
or sidecar_environment.get("AUTH_TOKEN_FILE")
|
|
!= ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH
|
|
or sidecar_environment.get("NODE_DB_PATH") != "/app/data/nodes.db"
|
|
or sidecar_environment.get("REBUILD_ON_START") != "false"
|
|
or sidecar_environment.get("N8N_MCP_TELEMETRY_DISABLED") != "true"
|
|
):
|
|
die("Engine node-intelligence sidecar runtime contract mismatch")
|
|
if any(
|
|
key in sidecar_environment
|
|
for key in ("AUTH_TOKEN", "N8N_API_URL", "N8N_API_KEY")
|
|
):
|
|
die("Engine node-intelligence sidecar runtime authority leak")
|
|
validate_engine_node_intelligence_secret_mount(
|
|
sidecar,
|
|
"Engine node-intelligence sidecar",
|
|
)
|
|
|
|
backend_environment = container_environment(backend, "Engine backend")
|
|
if (
|
|
backend_environment.get("ENGINE_NODE_INTELLIGENCE_MCP_URL")
|
|
!= f"http://{ENGINE_NODE_INTELLIGENCE_SERVICE}:3000/mcp"
|
|
or backend_environment.get("ENGINE_NODE_INTELLIGENCE_AUTH_TOKEN_FILE")
|
|
!= ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH
|
|
or backend_environment.get("ENGINE_NODE_INTELLIGENCE_ASSET_ID")
|
|
!= f"nodedc-node-intelligence@{ENGINE_NODE_INTELLIGENCE_RELEASE_ID.replace('-', '+', 1)}"
|
|
or "ENGINE_NODE_INTELLIGENCE_AUTH_TOKEN" in backend_environment
|
|
):
|
|
die("Engine backend node-intelligence boundary mismatch")
|
|
validate_engine_node_intelligence_secret_mount(
|
|
backend,
|
|
"Engine backend node-intelligence",
|
|
)
|
|
|
|
result = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
engine_node_intelligence_live_probe_script(),
|
|
),
|
|
"node-intelligence live capability",
|
|
container_id=backend_id,
|
|
)
|
|
expected = "node-intelligence-live:2.33.2:guidance+node+workflow"
|
|
if result != expected:
|
|
die("Engine node-intelligence live capability acceptance mismatch")
|
|
backend_runtime = preflight_engine_credential_backend_runtime()
|
|
if backend_runtime["mode"] != "verified-derived-retry":
|
|
die("Engine node-intelligence backend immutable runtime acceptance failed")
|
|
return result
|
|
|
|
|
|
def accept_engine_agent_full_grant_migration_runtime():
|
|
expected_scopes = (
|
|
"engine:l2:context:read",
|
|
"engine:l2:graph:read",
|
|
"engine:l2:node-schema:read",
|
|
"engine:l2:graph:plan",
|
|
"engine:l2:graph:write",
|
|
"engine:l2:data-product-publish-grant:plan",
|
|
"engine:l2:data-product-publish-grant:write",
|
|
"engine:l2:validate",
|
|
"engine:l2:runtime:read",
|
|
"engine:l2:deploy-run",
|
|
"engine:l2:execution:stop",
|
|
)
|
|
script = """
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
const file='/app/server/engineAgents/store.js';
|
|
const digest=crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
const module=await import('file:///app/server/engineAgents/store.js');
|
|
const expected=JSON.parse(process.argv[1]);
|
|
if(digest!==process.argv[2]||JSON.stringify(module.ENGINE_AGENT_SCOPES)!==JSON.stringify(expected)||module.ENGINE_AGENT_FULL_DEVELOPER_PROFILE!=='full-developer')process.exit(2);
|
|
process.stdout.write('store-v2-full-developer:'+digest);
|
|
""".strip()
|
|
result = run_engine_backend_probe(
|
|
(
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
script,
|
|
json.dumps(expected_scopes, separators=(",", ":")),
|
|
ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256,
|
|
),
|
|
"agent full grant migration",
|
|
container_id=engine_backend_container_id(),
|
|
)
|
|
expected = f"store-v2-full-developer:{ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256}"
|
|
if result != expected:
|
|
die("Engine agent full grant migration runtime acceptance mismatch")
|
|
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):
|
|
if component == "mission-core-map-access":
|
|
accept_map_access(component_root(component), entries)
|
|
return
|
|
if is_gitea_fresh_install_slice(component, entries):
|
|
if tuple(services or ()) != (GITEA_SERVICE,):
|
|
die("Gitea fresh-install service set mismatch")
|
|
accept_gitea_fresh_install_runtime()
|
|
return
|
|
|
|
if is_device_plane_control_core_v3_reconciliation_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if tuple(services or ()) != ("device-control-core",):
|
|
die("Device Control Core v3 reconciliation service set mismatch")
|
|
for service in (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
):
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
recovery_backup = (
|
|
validate_device_plane_control_core_v3_reconciliation_backup()
|
|
)
|
|
validate_device_plane_control_core_v3_restored_source(
|
|
recovery_backup,
|
|
marker_installed=True,
|
|
)
|
|
validate_device_plane_control_core_v3_reconciliation_runtime(
|
|
recovery_backup,
|
|
require_recovered=True,
|
|
)
|
|
return
|
|
|
|
if is_device_plane_control_core_migration_replay_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if tuple(services or ()) != ("device-control-core",):
|
|
die("Device Control Core migration recovery service set mismatch")
|
|
try:
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
"device-control-core",
|
|
)
|
|
except DeployError:
|
|
emit_bounded_device_control_core_failure_logs()
|
|
raise
|
|
for service in (
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
):
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
accept_device_plane_control_core_migration_replay_recovery()
|
|
return
|
|
|
|
if is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if tuple(services or ()) != ("device-control-core",):
|
|
die(
|
|
"Device Control Core migration replay checkpoint recovery "
|
|
"service set mismatch"
|
|
)
|
|
try:
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
"device-control-core",
|
|
)
|
|
except DeployError:
|
|
emit_bounded_device_control_core_failure_logs()
|
|
raise
|
|
for service in (
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
):
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
accept_device_plane_control_core_migration_replay_checkpoint_recovery()
|
|
return
|
|
|
|
if is_device_plane_control_core_release_slice(component, entries):
|
|
if tuple(services or ()) != ("device-control-core",):
|
|
die("Device Control Core release service set mismatch")
|
|
for service in (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
"device-backhaul-target",
|
|
):
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
return
|
|
|
|
if is_device_plane_manager_reconciliation_slice(component, entries):
|
|
if tuple(services or ()) != ():
|
|
die("Device Manager reconciliation service set mismatch")
|
|
for service in DEVICE_PLANE_RUNTIME_SERVICES:
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
backup_dir = validate_device_plane_manager_reconciliation_backup()
|
|
validate_device_plane_manager_reconciled_baseline(
|
|
backup_dir,
|
|
marker_installed=True,
|
|
)
|
|
return
|
|
|
|
if is_device_plane_manager_v2_reconciliation_slice(component, entries):
|
|
if tuple(services or ()) != ():
|
|
die("Device Manager v2 reconciliation service set mismatch")
|
|
for service in DEVICE_PLANE_RUNTIME_SERVICES:
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
backup_dir = validate_device_plane_manager_v2_reconciliation_backup()
|
|
validate_device_plane_manager_v2_reconciled_baseline(
|
|
backup_dir,
|
|
marker_installed=True,
|
|
)
|
|
return
|
|
|
|
if is_device_plane_backhaul_vps_enrollment_slice(component, entries):
|
|
if tuple(services or ()) != (DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,):
|
|
die("Device Plane VPS enrollment service set mismatch")
|
|
for service in DEVICE_PLANE_RUNTIME_SERVICES:
|
|
healthcheck_compose_service("device-plane", service)
|
|
healthcheck_compose_service(
|
|
"device-plane",
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,
|
|
)
|
|
return
|
|
|
|
if is_device_plane_backhaul_target_slice(component, entries):
|
|
if tuple(services or ()) != (DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,):
|
|
die("Device Plane backhaul target service set mismatch")
|
|
for service in DEVICE_PLANE_RUNTIME_SERVICES:
|
|
healthcheck_compose_service("device-plane", service)
|
|
healthcheck_compose_service(
|
|
"device-plane",
|
|
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,
|
|
)
|
|
return
|
|
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if tuple(services or ()) != ():
|
|
die("Device Plane B2 rollback recovery service set mismatch")
|
|
for service in DEVICE_PLANE_RUNTIME_SERVICES:
|
|
healthcheck_compose_service("device-plane", service)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
validate_device_plane_foundation_network_publication_installed_source()
|
|
installed_descriptor = read_strict_json(
|
|
component_root("device-plane")
|
|
/ DEVICE_PLANE_B2_DISCOVERY_ROLLBACK_RECOVERY_REL,
|
|
"installed Device Plane B2 rollback recovery descriptor",
|
|
max_bytes=16 * 1024,
|
|
)
|
|
if (
|
|
installed_descriptor
|
|
!= expected_device_plane_b2_discovery_rollback_recovery_descriptor()
|
|
):
|
|
die("installed Device Plane B2 rollback recovery mismatch")
|
|
validate_device_plane_foundation_runtime(
|
|
network_publication=True
|
|
)
|
|
assert_loopback_tcp_port_closed(9921)
|
|
return
|
|
if is_device_plane_postgres_bootstrap_slice(component, entries):
|
|
if tuple(services or ()) != ("device-postgres",):
|
|
die("Device Plane PostgreSQL bootstrap service set mismatch")
|
|
healthcheck_compose_service("device-plane", "device-postgres")
|
|
return
|
|
if is_device_plane_foundation_recovery_slice(component, entries):
|
|
if tuple(services or ()) != ():
|
|
die("Device Plane foundation recovery service set mismatch")
|
|
for service in (
|
|
"device-control-core",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service("device-plane", service)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
validate_device_plane_foundation_installed_source()
|
|
validate_device_plane_foundation_runtime()
|
|
return
|
|
if is_device_plane_foundation_network_publication_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if tuple(services or ()) != (
|
|
"device-control-core",
|
|
"device-gateway",
|
|
):
|
|
die(
|
|
"Device Plane network-publication service set mismatch"
|
|
)
|
|
for service in (
|
|
"device-control-core",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service("device-plane", service)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
validate_device_plane_foundation_network_publication_installed_source()
|
|
validate_device_plane_foundation_runtime(
|
|
network_publication=True
|
|
)
|
|
assert_loopback_tcp_port_closed(9921)
|
|
return
|
|
if is_device_plane_b2_discovery_ingress_slice(component, entries):
|
|
if tuple(services or ()) != (
|
|
"device-control-core",
|
|
"device-gateway",
|
|
):
|
|
die("Device Plane B2 discovery ingress service set mismatch")
|
|
for service in (
|
|
"device-control-core",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service("device-plane", service)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
assert_loopback_tcp_port_open(9921)
|
|
return
|
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
|
expected_services = (
|
|
("device-manager",)
|
|
if is_device_plane_manager_only_release_slice(component, entries)
|
|
else ("device-control-core", "device-manager")
|
|
)
|
|
if tuple(services or ()) != expected_services:
|
|
die("Device Manager control-plane service set mismatch")
|
|
for service in services:
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
if is_device_plane_manager_persistent_release_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
require_persistent_data=True,
|
|
)
|
|
elif is_device_plane_manager_release_v3_slice(component, entries):
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
else:
|
|
validate_device_manager_control_plane_runtime()
|
|
return
|
|
if is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
|
|
if tuple(services or ()) != ("device-control-core",):
|
|
die("Device Edge Core channel bootstrap service set mismatch")
|
|
for service in (
|
|
"device-control-core",
|
|
"device-manager",
|
|
"device-gateway",
|
|
"device-postgres",
|
|
):
|
|
healthcheck_compose_service_with_grace(
|
|
"device-plane",
|
|
service,
|
|
)
|
|
for check in component_healthchecks(component, entries, services):
|
|
healthcheck_url(check)
|
|
if is_device_plane_edge_core_channel_upgrade_v4_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
core_network_mode="private-egress",
|
|
)
|
|
else:
|
|
validate_device_manager_control_plane_runtime(
|
|
require_edge_channel=True,
|
|
)
|
|
return
|
|
if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
|
|
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):
|
|
descriptor = current_engine_n8n_transition_descriptor()
|
|
if descriptor is None:
|
|
die("installed Engine n8n transition descriptor is missing during acceptance")
|
|
accept_engine_n8n_runtime(descriptor)
|
|
return
|
|
if is_engine_node_intelligence_transition(component, entries):
|
|
descriptor = current_engine_node_intelligence_descriptor()
|
|
if descriptor is None:
|
|
die(
|
|
"installed Engine node-intelligence transition descriptor "
|
|
"is missing during acceptance"
|
|
)
|
|
if descriptor["action"] == "activate":
|
|
accept_engine_node_intelligence_runtime(descriptor)
|
|
healthcheck_url("http://127.0.0.1:3001/health")
|
|
return
|
|
healthcheck_compose_service("engine", "nodedc-backend")
|
|
healthcheck_url("http://127.0.0.1:3001/health")
|
|
backend = preflight_engine_credential_backend_runtime()
|
|
if backend["mode"] != "verified-derived-retry":
|
|
die("Engine node-intelligence rollback backend acceptance failed")
|
|
assert_engine_node_intelligence_container_absent()
|
|
return
|
|
touches_publish_grant = is_engine_data_product_publish_grant_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_composite_provider_v4 = is_engine_composite_provider_v4_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_provider_rotating_slot = is_engine_provider_rotating_slot_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_provider_authority_diagnostics = is_engine_provider_authority_diagnostics_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_depttrans_zone_authority_v1 = is_engine_depttrans_zone_authority_v1_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_provider_target_host_policy = is_engine_provider_target_host_policy_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_mcp_execution_profile_decoder = (
|
|
is_engine_mcp_execution_profile_decoder_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_telemetry_catalog = is_engine_mcp_telemetry_catalog_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_mcp_execution_plan_materialization = (
|
|
is_engine_mcp_execution_plan_materialization_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_execution_plan_telemetry_runtime = (
|
|
is_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_execution_plan_module_ownership = (
|
|
is_engine_mcp_execution_plan_module_ownership_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_normalized_identity_search = (
|
|
is_engine_mcp_normalized_identity_search_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
)
|
|
touches_mcp_l1_credential_reuse = (
|
|
is_engine_mcp_l1_credential_reuse_slice(component, entries)
|
|
)
|
|
touches_mcp_l1_credential_provenance = (
|
|
is_engine_mcp_l1_credential_provenance_slice(component, entries)
|
|
)
|
|
touches_mcp_execution_plan_sandbox_runtime = (
|
|
is_engine_mcp_execution_plan_sandbox_runtime_slice(component, entries)
|
|
)
|
|
touches_mcp_gelios_items_envelope = (
|
|
is_engine_mcp_gelios_items_envelope_slice(component, entries)
|
|
)
|
|
touches_mcp_registered_execution_profiles = (
|
|
is_engine_mcp_registered_execution_profiles_slice(component, entries)
|
|
)
|
|
touches_mcp_gelios_units_items = (
|
|
is_engine_mcp_gelios_units_items_slice(component, entries)
|
|
)
|
|
touches_agent_grant_migration = is_engine_agent_full_grant_migration_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
touches_mcp_control_plane = is_engine_mcp_control_plane_slice(component, entries)
|
|
touches_mcp_ontology_sdk = is_engine_mcp_ontology_sdk_slice(component, entries)
|
|
touches_mcp_autonomy_provider_v5 = is_engine_mcp_autonomy_provider_v5_slice(component, entries)
|
|
touches_provider_catalog = is_engine_provider_security_catalog_slice(component, entries)
|
|
if (
|
|
touches_engine_credential_sink(component, entries)
|
|
or touches_publish_grant
|
|
or touches_composite_provider_v4
|
|
or touches_provider_rotating_slot
|
|
or touches_provider_authority_diagnostics
|
|
or touches_depttrans_zone_authority_v1
|
|
or touches_provider_target_host_policy
|
|
or touches_mcp_execution_profile_decoder
|
|
or touches_mcp_telemetry_catalog
|
|
or touches_mcp_execution_plan_materialization
|
|
or touches_mcp_execution_plan_telemetry_runtime
|
|
or touches_mcp_execution_plan_module_ownership
|
|
or touches_mcp_normalized_identity_search
|
|
or touches_mcp_l1_credential_reuse
|
|
or touches_mcp_l1_credential_provenance
|
|
or touches_mcp_execution_plan_sandbox_runtime
|
|
or touches_mcp_gelios_items_envelope
|
|
or touches_mcp_registered_execution_profiles
|
|
or touches_mcp_gelios_units_items
|
|
or touches_agent_grant_migration
|
|
or touches_mcp_control_plane
|
|
or touches_mcp_ontology_sdk
|
|
or touches_mcp_autonomy_provider_v5
|
|
or touches_provider_catalog
|
|
):
|
|
# The HTTP endpoint can become ready before Docker publishes the first
|
|
# successful health probe. Wait for the Compose health barrier before
|
|
# the strict immutable-runtime preflight, both on apply and retry.
|
|
healthcheck_compose_service("engine", "nodedc-backend")
|
|
for url in component_healthchecks(component, entries, services):
|
|
healthcheck_url(url)
|
|
if (
|
|
touches_engine_credential_sink(component, entries)
|
|
or touches_publish_grant
|
|
or touches_composite_provider_v4
|
|
or touches_provider_rotating_slot
|
|
or touches_provider_authority_diagnostics
|
|
or touches_depttrans_zone_authority_v1
|
|
or touches_provider_target_host_policy
|
|
or touches_mcp_execution_profile_decoder
|
|
or touches_mcp_telemetry_catalog
|
|
or touches_mcp_execution_plan_materialization
|
|
or touches_mcp_execution_plan_telemetry_runtime
|
|
or touches_mcp_execution_plan_module_ownership
|
|
or touches_mcp_normalized_identity_search
|
|
or touches_mcp_l1_credential_reuse
|
|
or touches_mcp_l1_credential_provenance
|
|
or touches_mcp_execution_plan_sandbox_runtime
|
|
or touches_mcp_gelios_items_envelope
|
|
or touches_mcp_registered_execution_profiles
|
|
or touches_mcp_gelios_units_items
|
|
or touches_agent_grant_migration
|
|
or touches_mcp_control_plane
|
|
or touches_mcp_ontology_sdk
|
|
or touches_mcp_autonomy_provider_v5
|
|
or touches_provider_catalog
|
|
):
|
|
runtime = preflight_engine_credential_backend_runtime()
|
|
if runtime["mode"] != "verified-derived-retry":
|
|
die("Engine backend immutable runtime activation acceptance failed")
|
|
if touches_agent_grant_migration:
|
|
accept_engine_agent_full_grant_migration_runtime()
|
|
if touches_mcp_control_plane:
|
|
accept_engine_mcp_control_plane_runtime()
|
|
if touches_mcp_ontology_sdk:
|
|
accept_engine_mcp_ontology_sdk_runtime()
|
|
if touches_mcp_autonomy_provider_v5:
|
|
state = engine_mcp_autonomy_provider_v5_installed_state()
|
|
if state == "target":
|
|
accept_engine_mcp_autonomy_provider_v5_runtime()
|
|
if touches_provider_catalog:
|
|
accept_engine_provider_security_catalog_runtime()
|
|
if touches_publish_grant:
|
|
accept_engine_composite_provider_runtime()
|
|
if touches_composite_provider_v4:
|
|
root = component_root("engine")
|
|
installed_sha256 = {
|
|
rel: sha256_file(root / rel)
|
|
for rel in ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES
|
|
}
|
|
if installed_sha256 == ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256:
|
|
if accept_engine_composite_provider_runtime() is None:
|
|
die("Engine composite provider v4 live acceptance was not executed")
|
|
elif installed_sha256 != ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256:
|
|
die("Engine composite provider v4 installed source state is neither target nor rollback predecessor")
|
|
if touches_provider_rotating_slot:
|
|
root = component_root("engine")
|
|
installed_sha256 = {
|
|
rel: sha256_file(root / rel)
|
|
for rel in ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES
|
|
}
|
|
if installed_sha256 == ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256:
|
|
accept_engine_provider_rotating_slot_runtime()
|
|
elif installed_sha256 != ENGINE_PROVIDER_ROTATING_SLOT_PREDECESSOR_SHA256:
|
|
die("Engine provider rotating slot installed state is neither target nor rollback predecessor")
|
|
if touches_provider_authority_diagnostics:
|
|
root = component_root("engine")
|
|
installed_sha256 = {
|
|
rel: sha256_file(root / rel)
|
|
for rel in ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES
|
|
}
|
|
if installed_sha256 == ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256:
|
|
accept_engine_provider_authority_diagnostics_runtime()
|
|
elif installed_sha256 != ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256:
|
|
die("Engine provider authority diagnostics installed state is neither target nor rollback predecessor")
|
|
if touches_depttrans_zone_authority_v1:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256.items()
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256.items()
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_depttrans_zone_authority_v1_runtime()
|
|
elif not predecessor_state:
|
|
die("Engine Depttrans zone authority v1 installed state is neither target nor rollback predecessor")
|
|
if touches_provider_target_host_policy:
|
|
root = component_root("engine")
|
|
installed_sha256 = {
|
|
rel: sha256_file(root / rel)
|
|
for rel in ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES
|
|
}
|
|
if installed_sha256 == ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256:
|
|
accept_engine_provider_target_host_policy_runtime()
|
|
elif installed_sha256 != ENGINE_PROVIDER_TARGET_HOST_POLICY_PREDECESSOR_SHA256:
|
|
die("Engine provider target host policy installed state is neither target nor rollback predecessor")
|
|
if touches_mcp_execution_profile_decoder:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_MCP_EXECUTION_PROFILE_DECODER_TARGET_SHA256.items()
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PROFILE_DECODER_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_EXECUTION_PROFILE_DECODER_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_execution_profile_decoder_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP execution profile decoder installed state is neither "
|
|
"target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_telemetry_catalog:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256.items()
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_MCP_TELEMETRY_CATALOG_FOUNDATION_SHA256.items()
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_MCP_TELEMETRY_CATALOG_PREDECESSOR_SHA256.items()
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in ENGINE_MCP_TELEMETRY_CATALOG_FOUNDATION_SHA256.items()
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_TELEMETRY_CATALOG_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_telemetry_catalog_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP telemetry catalog installed state is neither target "
|
|
"nor rollback predecessor"
|
|
)
|
|
if touches_mcp_execution_plan_materialization:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_EXECUTION_PLAN_MATERIALIZATION_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_execution_plan_materialization_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP execution plan materialization installed state is "
|
|
"neither target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_execution_plan_telemetry_runtime:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_EXECUTION_PLAN_TELEMETRY_RUNTIME_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_execution_plan_telemetry_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime installed state "
|
|
"is neither target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_execution_plan_module_ownership:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_EXECUTION_PLAN_MODULE_OWNERSHIP_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_execution_plan_module_ownership_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP execution plan module ownership installed state "
|
|
"is neither target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_normalized_identity_search:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_NORMALIZED_IDENTITY_SEARCH_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_normalized_identity_search_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP normalized identity search installed state is "
|
|
"neither target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_l1_credential_reuse:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_L1_CREDENTIAL_REUSE_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_L1_CREDENTIAL_REUSE_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_l1_credential_reuse_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP L1 credential reuse installed state is neither "
|
|
"target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_l1_credential_provenance:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_L1_CREDENTIAL_PROVENANCE_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_l1_credential_provenance_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP L1 credential provenance installed state is "
|
|
"neither target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_execution_plan_sandbox_runtime:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_EXECUTION_PLAN_SANDBOX_RUNTIME_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_execution_plan_sandbox_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime installed state is "
|
|
"neither target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_gelios_items_envelope:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_gelios_items_envelope_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP Gelios items envelope installed state is neither "
|
|
"target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_registered_execution_profiles:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_registered_execution_profiles_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP registered execution profiles installed state is "
|
|
"neither target nor rollback predecessor"
|
|
)
|
|
if touches_mcp_gelios_units_items:
|
|
root = component_root("engine")
|
|
target_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256.items()
|
|
)
|
|
)
|
|
predecessor_state = all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256.items()
|
|
)
|
|
) and all(
|
|
(root / rel).is_file()
|
|
and not (root / rel).is_symlink()
|
|
and sha256_file(root / rel) == expected
|
|
for rel, expected in (
|
|
ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256.items()
|
|
)
|
|
) and all(
|
|
not (root / rel).exists() and not (root / rel).is_symlink()
|
|
for rel in ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS
|
|
)
|
|
if target_state:
|
|
accept_engine_mcp_gelios_units_items_runtime()
|
|
elif not predecessor_state:
|
|
die(
|
|
"Engine MCP Gelios units items installed state is neither "
|
|
"target nor rollback predecessor"
|
|
)
|
|
container_name = COMPONENTS[component].get("health_container")
|
|
if container_name:
|
|
healthcheck_container(container_name)
|
|
|
|
|
|
def move_artifact(src, target_dir, suffix=""):
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
name = src.name + suffix
|
|
dst = target_dir / name
|
|
if dst.exists():
|
|
dst = target_dir / f"{src.name}.{stamp()}"
|
|
src.rename(dst)
|
|
return dst
|
|
|
|
|
|
def apply_artifact(artifact):
|
|
require_root()
|
|
validate_artifact_location(artifact)
|
|
ensure_layout()
|
|
|
|
sha = sha256_file(artifact)
|
|
backup_id = None
|
|
backup_dir = None
|
|
manifest = None
|
|
entries = None
|
|
component = None
|
|
root = None
|
|
services = None
|
|
transition_descriptor = None
|
|
node_intelligence_descriptor = None
|
|
l2_closed_loop_preflight = None
|
|
device_plane_backhaul_preflight = None
|
|
device_plane_backhaul_vps_enrollment_preflight = None
|
|
node_intelligence_service_stopped = False
|
|
apply_started = False
|
|
engine_backend_recreated = False
|
|
engine_backend_initial_mode = None
|
|
runtime_started = False
|
|
current_stamp = stamp()
|
|
|
|
with DeployLock():
|
|
try:
|
|
with tempfile.TemporaryDirectory(prefix=f"apply-{current_stamp}-", dir=TMP_DIR) as tmp:
|
|
work = Path(tmp)
|
|
manifest, entries, payload_dir = load_artifact(artifact, work)
|
|
if is_gitea_fresh_install_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_gitea_compose_schema(
|
|
payload_dir / GITEA_COMPOSE_REL
|
|
)
|
|
if is_gitea_incident_salvage_slice(
|
|
manifest["component"],
|
|
entries,
|
|
):
|
|
validate_gitea_compose_schema(
|
|
payload_dir / GITEA_COMPOSE_REL
|
|
)
|
|
reject_failed_artifact_replay(manifest, sha)
|
|
reject_terminal_engine_l2_failed_artifact(manifest, sha)
|
|
reject_terminal_device_plane_foundation_artifact(
|
|
manifest,
|
|
sha,
|
|
)
|
|
reject_terminal_device_plane_manager_artifact(
|
|
manifest,
|
|
sha,
|
|
entries,
|
|
)
|
|
reject_terminal_device_plane_backhaul_artifact(
|
|
manifest,
|
|
sha,
|
|
)
|
|
patch_id = manifest["id"]
|
|
component = manifest["component"]
|
|
root = component_root(component)
|
|
compose_root = component_compose_root(component)
|
|
bootstrap_root = bool(COMPONENTS[component].get("bootstrap_root"))
|
|
services = component_services(component, entries)
|
|
if component == "mission-core-map-access":
|
|
preflight_map_access(payload_dir, entries)
|
|
artifact_only = component_artifact_only(component)
|
|
defer_bootstrap_root = is_gitea_bootstrap_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
|
|
if is_device_plane_control_core_incident_audit_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_control_core_incident_audit_evidence(
|
|
payload_dir
|
|
)
|
|
die(
|
|
"Device Control Core incident audit is canonical-plan-only; "
|
|
"apply is forbidden"
|
|
)
|
|
|
|
if is_device_plane_control_core_migration_replay_audit_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_control_core_migration_replay_audit_payload(
|
|
payload_dir
|
|
)
|
|
die(
|
|
"Device Control Core migration replay audit is "
|
|
"canonical-plan-only; apply is forbidden"
|
|
)
|
|
|
|
if state_has_sha(sha):
|
|
die(f"artifact sha already applied: {sha}")
|
|
if state_has_patch_id(patch_id):
|
|
die(f"patch id already applied: {patch_id}")
|
|
if is_gitea_fresh_install_slice(component, entries):
|
|
preflight_gitea_fresh_install()
|
|
if is_gitea_incident_salvage_slice(component, entries):
|
|
preflight_gitea_incident_salvage(
|
|
payload_dir,
|
|
enforce_apply=True,
|
|
)
|
|
if is_device_plane_postgres_bootstrap_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_device_plane_postgres_bootstrap()
|
|
if is_device_plane_foundation_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
device_plane_foundation_recovery_preflight = (
|
|
validate_device_plane_foundation_recovery_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_foundation_network_publication_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_foundation_network_publication_evidence(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_b2_discovery_ingress_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_b2_discovery_ingress_evidence(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_b2_discovery_rollback_recovery_evidence(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_manager_control_plane_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_manager_activation_predecessor(
|
|
payload_dir,
|
|
preflight_phase="apply",
|
|
)
|
|
if is_device_plane_edge_core_channel_bootstrap_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_edge_core_channel_bootstrap_predecessor(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_control_core_release_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_control_core_release_predecessor(
|
|
payload_dir,
|
|
preflight_phase="apply",
|
|
)
|
|
if is_device_plane_manager_reconciliation_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_manager_reconciliation_evidence(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_manager_v2_reconciliation_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_manager_v2_reconciliation_evidence(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_control_core_v3_reconciliation_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
validate_device_plane_control_core_v3_reconciliation_evidence(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_control_core_migration_replay_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if (
|
|
patch_id
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID
|
|
):
|
|
die(
|
|
"Device Control Core migration recovery patch id "
|
|
"mismatch"
|
|
)
|
|
validate_device_plane_control_core_migration_replay_recovery_evidence(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if (
|
|
patch_id
|
|
!= DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_PATCH_ID
|
|
):
|
|
die(
|
|
"Device Control Core migration replay checkpoint "
|
|
"recovery patch id mismatch"
|
|
)
|
|
validate_device_plane_control_core_migration_replay_checkpoint_recovery_evidence(
|
|
payload_dir
|
|
)
|
|
if is_device_plane_backhaul_target_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
device_plane_backhaul_preflight = (
|
|
validate_device_plane_backhaul_target_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if is_device_plane_backhaul_vps_enrollment_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
device_plane_backhaul_vps_enrollment_preflight = (
|
|
validate_device_plane_backhaul_vps_enrollment_evidence(
|
|
payload_dir
|
|
)
|
|
)
|
|
if not root.is_dir():
|
|
if bootstrap_root and defer_bootstrap_root:
|
|
# The fresh-install root itself is a mutation. Delay it
|
|
# until after backup and apply_started so every created
|
|
# path is covered by Gitea's automatic rollback.
|
|
pass
|
|
elif bootstrap_root:
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
else:
|
|
die(f"component payload root not found: {root}")
|
|
if is_engine_data_product_publish_grant_slice(component, entries):
|
|
preflight_engine_data_product_publish_grant_predecessor(payload_dir)
|
|
publish_backend_preflight = preflight_engine_credential_backend_runtime()
|
|
if publish_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine data product publish grant requires the active immutable credential backend")
|
|
if is_engine_composite_provider_v4_slice(component, entries):
|
|
preflight_engine_composite_provider_v4_predecessor()
|
|
composite_backend_preflight = preflight_engine_credential_backend_runtime()
|
|
if composite_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine composite provider v4 requires the active immutable credential backend")
|
|
if is_engine_provider_rotating_slot_slice(component, entries):
|
|
preflight_engine_provider_rotating_slot_predecessor()
|
|
rotating_slot_backend_preflight = preflight_engine_credential_backend_runtime()
|
|
if rotating_slot_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine provider rotating slot requires the active immutable credential backend")
|
|
if is_engine_provider_authority_diagnostics_slice(component, entries):
|
|
preflight_engine_provider_authority_diagnostics_predecessor()
|
|
authority_diagnostics_backend_preflight = preflight_engine_credential_backend_runtime()
|
|
if authority_diagnostics_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine provider authority diagnostics requires the active immutable credential backend")
|
|
if is_engine_depttrans_zone_authority_v1_slice(component, entries):
|
|
preflight_engine_depttrans_zone_authority_v1_predecessor()
|
|
depttrans_authority_backend_preflight = preflight_engine_credential_backend_runtime()
|
|
if depttrans_authority_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine Depttrans zone authority v1 requires the active immutable credential backend")
|
|
if is_engine_provider_target_host_policy_slice(component, entries):
|
|
preflight_engine_provider_target_host_policy_predecessor()
|
|
target_host_policy_backend_preflight = preflight_engine_credential_backend_runtime()
|
|
if target_host_policy_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine provider target host policy requires the active immutable credential backend")
|
|
if is_engine_mcp_execution_profile_decoder_slice(component, entries):
|
|
preflight_engine_mcp_execution_profile_decoder_predecessor()
|
|
profile_decoder_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
profile_decoder_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP execution profile decoder requires the "
|
|
"active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_telemetry_catalog_slice(component, entries):
|
|
preflight_engine_mcp_telemetry_catalog_predecessor()
|
|
telemetry_catalog_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
telemetry_catalog_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP telemetry catalog requires the active "
|
|
"immutable credential backend"
|
|
)
|
|
if is_engine_mcp_execution_plan_materialization_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_execution_plan_materialization_predecessor()
|
|
execution_plan_materialization_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
execution_plan_materialization_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP execution plan materialization requires "
|
|
"the active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_execution_plan_telemetry_runtime_predecessor()
|
|
execution_plan_telemetry_runtime_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
execution_plan_telemetry_runtime_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP execution plan telemetry runtime "
|
|
"requires the active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_execution_plan_module_ownership_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_execution_plan_module_ownership_predecessor()
|
|
execution_plan_module_ownership_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
execution_plan_module_ownership_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP execution plan module ownership "
|
|
"requires the active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_normalized_identity_search_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_normalized_identity_search_predecessor()
|
|
identity_search_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
identity_search_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP normalized identity search requires "
|
|
"the active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_l1_credential_reuse_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_l1_credential_reuse_predecessor()
|
|
l1_credential_reuse_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
l1_credential_reuse_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP L1 credential reuse requires the "
|
|
"active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_l1_credential_provenance_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_l1_credential_provenance_predecessor()
|
|
l1_credential_provenance_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
l1_credential_provenance_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP L1 credential provenance requires the "
|
|
"active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_execution_plan_sandbox_runtime_predecessor()
|
|
execution_plan_sandbox_runtime_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
execution_plan_sandbox_runtime_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP execution plan sandbox runtime requires "
|
|
"the active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_gelios_items_envelope_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_gelios_items_envelope_predecessor()
|
|
gelios_items_envelope_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
gelios_items_envelope_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP Gelios items envelope requires the "
|
|
"active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_registered_execution_profiles_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_registered_execution_profiles_predecessor()
|
|
registered_profiles_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
registered_profiles_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP registered execution profiles requires "
|
|
"the active immutable credential backend"
|
|
)
|
|
if is_engine_mcp_gelios_units_items_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
preflight_engine_mcp_gelios_units_items_predecessor()
|
|
gelios_units_items_backend_preflight = (
|
|
preflight_engine_credential_backend_runtime()
|
|
)
|
|
if (
|
|
gelios_units_items_backend_preflight["mode"]
|
|
!= "verified-derived-retry"
|
|
):
|
|
die(
|
|
"Engine MCP Gelios units items requires the active "
|
|
"immutable credential backend"
|
|
)
|
|
if is_engine_agent_full_grant_migration_slice(component, entries):
|
|
preflight_engine_agent_full_grant_migration_predecessor()
|
|
migration_backend_preflight = preflight_engine_credential_backend_runtime()
|
|
if migration_backend_preflight["mode"] != "verified-derived-retry":
|
|
die("Engine agent full grant migration requires the active immutable credential backend")
|
|
if is_engine_node_intelligence_transition(component, entries):
|
|
node_intelligence_preflight = preflight_engine_node_intelligence_predecessor(
|
|
payload_dir
|
|
)
|
|
node_intelligence_descriptor = node_intelligence_preflight["descriptor"]
|
|
if is_engine_mcp_control_plane_slice(component, entries):
|
|
preflight_engine_mcp_control_plane_predecessor(payload_dir)
|
|
if is_engine_mcp_ontology_sdk_slice(component, entries):
|
|
preflight_engine_mcp_ontology_sdk_predecessor(payload_dir)
|
|
if is_engine_mcp_autonomy_provider_v5_slice(component, entries):
|
|
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):
|
|
preflight_engine_provider_security_catalog_predecessor()
|
|
if not artifact_only and not compose_root.is_dir():
|
|
if bootstrap_root and defer_bootstrap_root:
|
|
pass
|
|
elif bootstrap_root and is_relative_to(compose_root.resolve(strict=False), root.resolve(strict=False)):
|
|
compose_root.mkdir(parents=True, exist_ok=True)
|
|
else:
|
|
die(f"component compose root not found: {compose_root}")
|
|
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:
|
|
for compose_file in compose_files:
|
|
if not compose_file.is_file():
|
|
compose_entry = None
|
|
if is_relative_to(compose_file.resolve(strict=False), root.resolve(strict=False)):
|
|
compose_entry = compose_file.resolve(strict=False).relative_to(root.resolve(strict=False)).as_posix()
|
|
if compose_entry not in entries:
|
|
die(f"compose file not found: {compose_file}")
|
|
elif not artifact_only and not (compose_root / "docker-compose.yml").is_file():
|
|
# A bootstrap component begins with no payload root. Its
|
|
# compose file may therefore be supplied by the same
|
|
# reviewed overlay that is about to create that root.
|
|
compose_entry = None
|
|
if is_relative_to(compose_root.resolve(strict=False), root.resolve(strict=False)):
|
|
compose_entry = compose_root.resolve(strict=False).relative_to(root.resolve(strict=False)).joinpath("docker-compose.yml").as_posix()
|
|
if not (bootstrap_root and compose_entry in entries):
|
|
die(f"docker-compose.yml not found in component compose root: {compose_root}")
|
|
compose_env_file = component_compose_env_file(component)
|
|
if not artifact_only and compose_env_file and not compose_env_file.is_file():
|
|
die(f"compose env file not found: {compose_env_file}")
|
|
if not artifact_only and not DOCKER.is_file():
|
|
die(f"docker not found: {DOCKER}")
|
|
if component == "device-plane":
|
|
inventory_services = DEVICE_PLANE_RUNTIME_SERVICES
|
|
if is_device_plane_manager_control_plane_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
inventory_services = (
|
|
*inventory_services,
|
|
"device-manager",
|
|
)
|
|
if (
|
|
is_device_plane_control_core_release_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_device_plane_control_core_v3_reconciliation_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_device_plane_control_core_migration_replay_recovery_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_device_plane_control_core_migration_replay_checkpoint_recovery_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
):
|
|
inventory_services = (
|
|
*inventory_services,
|
|
"device-manager",
|
|
"device-backhaul-target",
|
|
)
|
|
elif is_device_plane_edge_core_channel_bootstrap_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
inventory_services = (
|
|
*inventory_services,
|
|
"device-manager",
|
|
)
|
|
device_plane_runtime_before = (
|
|
device_plane_runtime_inventory(
|
|
inventory_services
|
|
)
|
|
)
|
|
|
|
if is_engine_n8n_transition(component, entries):
|
|
transition_descriptor = read_engine_n8n_transition_descriptor(
|
|
payload_dir / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL
|
|
)
|
|
preflight_engine_n8n_transition(
|
|
transition_descriptor,
|
|
enforce_expected_current=True,
|
|
)
|
|
if touches_engine_credential_sink(component, entries):
|
|
engine_backend_initial_mode = preflight_engine_credential_backend_runtime()["mode"]
|
|
|
|
if COMPONENTS[component].get("immutable_payload"):
|
|
for rel in entries:
|
|
destination = root / rel
|
|
if destination.exists() or destination.is_symlink():
|
|
die(f"immutable release already exists: {rel}")
|
|
|
|
backup_id = safe_name(f"{component}-{patch_id}-{current_stamp}")
|
|
backup_dir = BACKUPS_DIR / backup_id
|
|
backup_dir.mkdir(parents=True, exist_ok=False)
|
|
(backup_dir / "manifest.env").write_text((work / "manifest.env").read_text(encoding="utf-8"), encoding="utf-8")
|
|
(backup_dir / "files.txt").write_text((work / "files.txt").read_text(encoding="utf-8"), encoding="utf-8")
|
|
|
|
include_nginx_html = component == "engine" and component_publish_dist(component, entries)
|
|
create_backup(root, backup_dir, entries, include_nginx_html)
|
|
if component == "mission-core-map-access":
|
|
backup_map_access(backup_dir)
|
|
if component == "device-plane":
|
|
if device_plane_runtime_before is None:
|
|
die("Device Plane pre-apply runtime inventory is missing")
|
|
runtime_inventory_path = (
|
|
backup_dir / "runtime-before.json"
|
|
)
|
|
runtime_inventory_path.write_text(
|
|
json.dumps(
|
|
device_plane_runtime_before,
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
runtime_inventory_path.chmod(0o600)
|
|
if (
|
|
device_plane_backhaul_vps_enrollment_preflight
|
|
is not None
|
|
):
|
|
backup_device_plane_backhaul_authorized_keys(
|
|
backup_dir
|
|
)
|
|
if device_plane_backhaul_preflight is not None:
|
|
tailscale_before_path = (
|
|
backup_dir / "tailscale-serve-before.json"
|
|
)
|
|
tailscale_before_path.write_text(
|
|
json.dumps(
|
|
device_plane_backhaul_preflight[
|
|
"tailscaleServeBefore"
|
|
],
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
tailscale_before_path.chmod(0o600)
|
|
if component == "platform" and touches_external_data_plane_files(entries):
|
|
validate_backup_partition(
|
|
entries,
|
|
read_backup_path_list(backup_dir / "existing-files.txt"),
|
|
read_backup_path_list(backup_dir / "missing-files.txt"),
|
|
"Platform pre-apply",
|
|
)
|
|
|
|
apply_started = True
|
|
if (
|
|
node_intelligence_descriptor is not None
|
|
and node_intelligence_descriptor["action"] == "rollback-inactive"
|
|
):
|
|
stop_and_remove_compose_services(
|
|
"engine",
|
|
(ENGINE_NODE_INTELLIGENCE_SERVICE,),
|
|
)
|
|
node_intelligence_service_stopped = True
|
|
for rel in entries:
|
|
copy_payload_path(payload_dir, root, rel, current_stamp)
|
|
|
|
if component == "n8n-private-extension":
|
|
for rel in entries:
|
|
seal_n8n_private_extension_release(root, rel)
|
|
|
|
if component == "mission-core-map-access":
|
|
apply_map_access(root, entries)
|
|
|
|
if component_publish_dist(component, entries):
|
|
publish_engine_dist(root, current_stamp)
|
|
|
|
if touches_engine_credential_sink(component, entries):
|
|
run_build(component, entries)
|
|
prepare_component_runtime(component, entries)
|
|
# From this point the exact immutable override/image are
|
|
# verified, so even an ambiguous partial Compose failure
|
|
# can be safely reconciled by recreating on that runtime.
|
|
engine_backend_recreated = True
|
|
run_compose(component, services, entries)
|
|
elif component == "device-plane":
|
|
def mark_device_plane_runtime_started():
|
|
nonlocal runtime_started
|
|
runtime_started = bool(services)
|
|
|
|
run_device_plane_runtime_for_apply(
|
|
entries,
|
|
services,
|
|
mark_device_plane_runtime_started,
|
|
backhaul_serve_before=(
|
|
device_plane_backhaul_preflight[
|
|
"tailscaleServeBefore"
|
|
]
|
|
if device_plane_backhaul_preflight is not None
|
|
else None
|
|
),
|
|
)
|
|
else:
|
|
# Mark the generic runtime before Compose so a partial
|
|
# candidate start is always eligible for its domain rollback.
|
|
runtime_started = bool(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)
|
|
if is_device_plane_control_core_release_v4_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if device_plane_runtime_before is None:
|
|
die(
|
|
"Device Control Core release v4 predecessor "
|
|
"runtime inventory is missing"
|
|
)
|
|
accept_device_plane_control_core_release_v4(
|
|
payload_dir,
|
|
device_plane_runtime_before,
|
|
)
|
|
if is_device_plane_b2_discovery_ingress_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if device_plane_runtime_before is None:
|
|
die(
|
|
"Device Plane B2 ingress predecessor runtime "
|
|
"inventory is missing"
|
|
)
|
|
validate_device_plane_b2_discovery_ingress_runtime(
|
|
device_plane_runtime_before
|
|
)
|
|
if is_device_plane_backhaul_target_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if device_plane_runtime_before is None:
|
|
die(
|
|
"Device Plane backhaul predecessor runtime "
|
|
"inventory is missing"
|
|
)
|
|
validate_device_plane_backhaul_target_runtime(
|
|
device_plane_runtime_before
|
|
)
|
|
if is_device_plane_backhaul_vps_enrollment_slice(
|
|
component,
|
|
entries,
|
|
):
|
|
if device_plane_runtime_before is None:
|
|
die(
|
|
"Device Plane VPS enrollment predecessor runtime "
|
|
"inventory is missing"
|
|
)
|
|
validate_device_plane_backhaul_target_runtime(
|
|
device_plane_runtime_before,
|
|
expected_enrollment=(
|
|
read_device_plane_backhaul_vps_enrollment_public_key()
|
|
),
|
|
)
|
|
|
|
applied_path = move_artifact(artifact, APPLIED_DIR)
|
|
append_jsonl(STATE_FILE, {
|
|
"applied_at": utc_now(),
|
|
"artifact": applied_path.name,
|
|
"backup_id": backup_id,
|
|
"component": component,
|
|
"id": patch_id,
|
|
"sha256": sha,
|
|
"status": "ok",
|
|
})
|
|
print(f"deploy-ok patch={patch_id} component={component} backup={backup_id}")
|
|
except Exception as exc:
|
|
rollback_status = "not-required"
|
|
if (
|
|
apply_started
|
|
and backup_dir is not None
|
|
and root is not None
|
|
):
|
|
if component == "mission-core-map-access":
|
|
try:
|
|
rollback_map_access(root, backup_dir, entries, current_stamp)
|
|
rollback_status = "ok:mission-core-map-access"
|
|
except Exception as rollback_exc:
|
|
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
|
elif is_gitea_fresh_install_slice(component, entries):
|
|
try:
|
|
restored_action = rollback_gitea_fresh_install(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started=runtime_started,
|
|
)
|
|
rollback_status = f"ok:gitea-fresh-install:{restored_action}"
|
|
print(
|
|
f"gitea-automatic-rollback={rollback_status}",
|
|
file=sys.stderr,
|
|
)
|
|
except ReconciliationRequired as rollback_exc:
|
|
rollback_status = "deferred:reconciliation-required"
|
|
print(
|
|
"gitea-automatic-rollback=deferred:"
|
|
f"{rollback_exc}",
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as rollback_exc:
|
|
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
|
print(
|
|
"gitea-automatic-rollback=failed",
|
|
file=sys.stderr,
|
|
)
|
|
elif transition_descriptor is not None and is_engine_n8n_transition(component, entries):
|
|
try:
|
|
restored_action = restore_engine_n8n_transition(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
transition_descriptor,
|
|
current_stamp,
|
|
)
|
|
rollback_status = f"ok:{restored_action}"
|
|
print(f"engine-n8n-automatic-rollback={rollback_status}", file=sys.stderr)
|
|
except Exception as rollback_exc:
|
|
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
|
print("engine-n8n-automatic-rollback=failed", file=sys.stderr)
|
|
elif (
|
|
node_intelligence_descriptor is not None
|
|
and is_engine_node_intelligence_transition(component, entries)
|
|
):
|
|
try:
|
|
restored_action = rollback_engine_node_intelligence(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started=runtime_started,
|
|
node_intelligence_service_stopped=node_intelligence_service_stopped,
|
|
)
|
|
rollback_status = f"ok:{restored_action}"
|
|
print(
|
|
f"engine-node-intelligence-automatic-rollback={rollback_status}",
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as rollback_exc:
|
|
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
|
print(
|
|
"engine-node-intelligence-automatic-rollback=failed",
|
|
file=sys.stderr,
|
|
)
|
|
elif touches_engine_credential_sink(component, entries):
|
|
try:
|
|
restored_action = rollback_engine_credential_bridge(
|
|
component,
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
engine_backend_recreated=engine_backend_recreated,
|
|
engine_backend_initial_mode=engine_backend_initial_mode,
|
|
)
|
|
rollback_status = f"ok:{restored_action}"
|
|
print(f"engine-credential-automatic-rollback={rollback_status}", file=sys.stderr)
|
|
except ReconciliationRequired:
|
|
rollback_status = "deferred:reconciliation-required"
|
|
print("engine-credential-automatic-rollback=deferred:reconciliation-required", file=sys.stderr)
|
|
except Exception as rollback_exc:
|
|
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
|
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 (
|
|
(
|
|
is_engine_data_product_publish_grant_slice(component, entries)
|
|
or is_engine_composite_provider_v4_slice(component, entries)
|
|
or is_engine_provider_rotating_slot_slice(component, entries)
|
|
or is_engine_provider_authority_diagnostics_slice(component, entries)
|
|
or is_engine_depttrans_zone_authority_v1_slice(component, entries)
|
|
or is_engine_provider_target_host_policy_slice(component, entries)
|
|
or is_engine_mcp_execution_profile_decoder_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_telemetry_catalog_slice(component, entries)
|
|
or is_engine_mcp_execution_plan_materialization_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_execution_plan_telemetry_runtime_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_execution_plan_module_ownership_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_normalized_identity_search_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_l1_credential_reuse_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_l1_credential_provenance_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_execution_plan_sandbox_runtime_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_gelios_items_envelope_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_registered_execution_profiles_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_mcp_gelios_units_items_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
or is_engine_agent_full_grant_migration_slice(component, entries)
|
|
or is_engine_mcp_control_plane_slice(component, entries)
|
|
or is_engine_mcp_ontology_sdk_slice(component, entries)
|
|
or is_engine_mcp_autonomy_provider_v5_slice(component, entries)
|
|
or is_engine_provider_security_catalog_slice(component, entries)
|
|
)
|
|
and services is not None
|
|
):
|
|
try:
|
|
restored_state = rollback_engine_apply(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started,
|
|
services,
|
|
)
|
|
rollback_status = f"ok:engine-overlay:{restored_state}"
|
|
print(f"engine-automatic-rollback={rollback_status}", file=sys.stderr)
|
|
except Exception as rollback_exc:
|
|
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
|
print("engine-automatic-rollback=failed", file=sys.stderr)
|
|
elif (
|
|
component == "platform"
|
|
and entries is not None
|
|
and services is not None
|
|
and touches_external_data_plane_files(entries)
|
|
):
|
|
try:
|
|
restored_state = rollback_platform_apply(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started,
|
|
services,
|
|
)
|
|
rollback_status = f"ok:platform-overlay:{restored_state}"
|
|
print(f"platform-automatic-rollback={rollback_status}", file=sys.stderr)
|
|
except Exception as rollback_exc:
|
|
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
|
print("platform-automatic-rollback=failed", file=sys.stderr)
|
|
elif (
|
|
is_device_plane_foundation_network_publication_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
and services is not None
|
|
):
|
|
try:
|
|
restored_state = (
|
|
rollback_device_plane_network_publication_apply(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started,
|
|
services,
|
|
)
|
|
)
|
|
rollback_status = (
|
|
"ok:device-plane-network-publication:"
|
|
f"{restored_state}"
|
|
)
|
|
print(
|
|
"device-plane-network-publication-"
|
|
f"automatic-rollback={rollback_status}",
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as rollback_exc:
|
|
rollback_status = (
|
|
f"failed:{type(rollback_exc).__name__}"
|
|
)
|
|
print(
|
|
"device-plane-network-publication-"
|
|
"automatic-rollback=failed",
|
|
file=sys.stderr,
|
|
)
|
|
elif (
|
|
is_device_plane_backhaul_vps_enrollment_slice(
|
|
component,
|
|
entries,
|
|
)
|
|
and device_plane_runtime_before is not None
|
|
):
|
|
try:
|
|
restored_state = (
|
|
rollback_device_plane_backhaul_vps_enrollment(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
device_plane_runtime_before,
|
|
)
|
|
)
|
|
rollback_status = (
|
|
"ok:device-plane-vps-enrollment:"
|
|
f"{restored_state}"
|
|
)
|
|
print(
|
|
"device-plane-vps-enrollment-"
|
|
f"automatic-rollback={rollback_status}",
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as rollback_exc:
|
|
rollback_status = (
|
|
f"failed:{type(rollback_exc).__name__}"
|
|
)
|
|
print(
|
|
"device-plane-vps-enrollment-"
|
|
"automatic-rollback=failed",
|
|
file=sys.stderr,
|
|
)
|
|
elif (
|
|
component == "device-plane"
|
|
and entries is not None
|
|
and services is not None
|
|
):
|
|
try:
|
|
restored_state = rollback_device_plane_apply(
|
|
root,
|
|
backup_dir,
|
|
entries,
|
|
current_stamp,
|
|
runtime_started,
|
|
services,
|
|
)
|
|
rollback_status = f"ok:device-plane-overlay:{restored_state}"
|
|
print(
|
|
f"device-plane-automatic-rollback={rollback_status}",
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as rollback_exc:
|
|
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
|
print(
|
|
"device-plane-automatic-rollback=failed",
|
|
file=sys.stderr,
|
|
)
|
|
failed_path = None
|
|
if artifact.exists() and rollback_status != "deferred:reconciliation-required":
|
|
try:
|
|
failed_path = move_artifact(artifact, FAILED_DIR, suffix=f".{current_stamp}")
|
|
except Exception:
|
|
failed_path = None
|
|
|
|
append_jsonl(FAILED_STATE_FILE, {
|
|
"artifact": str(failed_path.name if failed_path else artifact.name),
|
|
"backup_id": backup_id,
|
|
"component": manifest.get("component") if manifest else None,
|
|
"failed_at": utc_now(),
|
|
"id": manifest.get("id") if manifest else None,
|
|
"message": str(exc),
|
|
"rollback_status": rollback_status,
|
|
"sha256": sha,
|
|
"started_apply": apply_started,
|
|
"status": (
|
|
"reconciliation-required"
|
|
if rollback_status == "deferred:reconciliation-required"
|
|
else "failed"
|
|
),
|
|
})
|
|
if backup_id:
|
|
print(f"backup={backup_id}", file=sys.stderr)
|
|
raise
|
|
|
|
|
|
def main(argv):
|
|
if sys.version_info < (3, 8):
|
|
die("Python 3.8 or newer is required")
|
|
parser = argparse.ArgumentParser(prog="nodedc-deploy")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
|
|
plan = sub.add_parser("plan")
|
|
plan.add_argument("artifact")
|
|
|
|
apply = sub.add_parser("apply")
|
|
apply.add_argument("artifact")
|
|
|
|
sub.add_parser("verify-install")
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.cmd == "verify-install":
|
|
verify_install()
|
|
return 0
|
|
|
|
artifact = Path(args.artifact).absolute()
|
|
if args.cmd == "plan":
|
|
plan_artifact(artifact)
|
|
return 0
|
|
if args.cmd == "apply":
|
|
apply_artifact(artifact)
|
|
return 0
|
|
|
|
parser.print_help()
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main(sys.argv[1:]))
|
|
except DeployError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
raise SystemExit(1)
|