fix(deploy): reconcile device plane foundation

This commit is contained in:
Codex
2026-07-25 23:11:14 +03:00
parent 1102e25e6e
commit aca47c6143
8 changed files with 1692 additions and 15 deletions
+836 -5
View File
@@ -52,6 +52,75 @@ DEVICE_PLANE_POSTGRES_BOOTSTRAP_ENTRIES = (
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_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_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_RUNTIME_SERVICES = (
"device-control-core",
"device-gateway",
"device-postgres",
)
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"
@@ -2736,6 +2805,7 @@ def allowed_payload_path(component, rel):
"package-lock.json",
"docker-compose.device-plane.yml",
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
DEVICE_PLANE_FOUNDATION_RECOVERY_REL,
"packages/device-protocol-contract",
"packages/arusnavi-b2-adapter",
"services/device-control-core",
@@ -7760,6 +7830,11 @@ def load_artifact(artifact, work_dir):
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 manifest["component"] == "n8n-private-extension":
validate_n8n_private_extension_release(payload_dir, entries)
if manifest["component"] == "engine":
@@ -8034,6 +8109,17 @@ def reject_terminal_engine_l2_failed_artifact(manifest, sha256):
)
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"
)
class DeployLock:
def __enter__(self):
try:
@@ -8059,6 +8145,325 @@ def is_device_plane_postgres_bootstrap_slice(component, 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 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 device_plane_service_container_ids(service):
if service not in DEVICE_PLANE_RUNTIME_SERVICES:
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
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_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 validate_device_plane_postgres_bootstrap_payload(payload_dir):
descriptor = read_strict_json(
payload_dir / DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
@@ -8132,6 +8537,303 @@ def device_plane_postgres_plan_selection(postgres_preflight):
return "bootstrap-selected:create-if-absent"
def validate_device_plane_foundation_runtime():
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",
}],
},
},
"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",
}],
},
},
"device-postgres": {
"image": "postgres:16-alpine",
"imageId": DEVICE_PLANE_FOUNDATION_RECOVERY_IMAGE_IDS[
"device-postgres"
],
"user": "",
"ports": {},
},
}
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 {}
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) != {"nodedc-device-plane-private"}
):
die(
"Device Plane foundation runtime boundary mismatch: "
f"{service}"
)
accepted[service] = {
"containerId": container.get("Id"),
"imageId": container.get("Image"),
"health": "healthy",
"restartCount": 0,
}
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")
return accepted
def validate_device_plane_runtime_secret_metadata():
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")
for path, label in (
(DEVICE_PLANE_POSTGRES_PASSWORD_FILE, "PostgreSQL"),
(DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE, "Gateway to Core"),
(DEVICE_PLANE_IDENTIFIER_PEPPER_FILE, "identifier pepper"),
):
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_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 component_compose_root(component):
return COMPONENTS[component].get("compose_root", component_root(component))
@@ -9658,6 +10360,12 @@ def component_services(component, entries=None):
# 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_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
@@ -9992,6 +10700,9 @@ def component_builds(component, entries=None):
if is_device_plane_postgres_bootstrap_slice(component, entries):
return ()
if is_device_plane_foundation_recovery_slice(component, entries):
return ()
if component == "device-plane" and entries is not None:
selected_services = component_services(component, entries)
builds = []
@@ -11963,6 +12674,8 @@ def plan_artifact(artifact):
mcp_ontology_sdk_preflight = None
mcp_autonomy_provider_v5_preflight = None
l2_closed_loop_preflight = None
device_plane_foundation_recovery_preflight = None
device_plane_runtime_before = None
composite_provider_v4_preflight = None
provider_rotating_slot_preflight = None
provider_authority_diagnostics_preflight = None
@@ -11982,9 +12695,11 @@ def plan_artifact(artifact):
mcp_gelios_units_items_preflight = None
provider_catalog_preflight = None
device_plane_postgres_preflight = None
device_plane_foundation_recovery_preflight = None
with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp:
manifest, entries, payload_dir = load_artifact(artifact, Path(tmp))
reject_terminal_engine_l2_failed_artifact(manifest, sha)
reject_terminal_device_plane_foundation_artifact(manifest, sha)
transition_descriptor = None
transition_preflight = None
if is_engine_n8n_transition(manifest["component"], entries):
@@ -12125,6 +12840,15 @@ def plan_artifact(artifact):
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
)
)
component = manifest["component"]
root = component_root(component)
@@ -13508,6 +14232,31 @@ def plan_artifact(artifact):
print("device_gateway_public_ingress=disabled")
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_postgres_preflight is not None:
print(
"device_postgres_bootstrap="
@@ -13813,11 +14562,18 @@ def rollback_device_plane_apply(
"Device Plane runtime rollback",
)
baseline_entries = [rel for rel in entries if rel in existing_set]
compose_was_installed = "docker-compose.device-plane.yml" in existing_set
baseline_services = (
component_services("device-plane", baseline_entries)
if compose_was_installed
else ()
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
@@ -14555,6 +15311,8 @@ def run_component_runtime(component, entries, services):
return
if component_artifact_only(component):
return
if is_device_plane_foundation_recovery_slice(component, entries):
return
run_build(component, entries)
prepare_component_runtime(component, entries)
if is_engine_node_intelligence_transition(component, entries):
@@ -14663,6 +15421,29 @@ def module_foundry_healthcheck():
def component_healthchecks(component, entries=None, services=None):
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,
@@ -15101,6 +15882,20 @@ def run_healthchecks(component, entries=None, services=None):
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 component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
return
if is_engine_l2_closed_loop_slice(component, entries):
@@ -15881,6 +16676,10 @@ def apply_artifact(artifact):
work = Path(tmp)
manifest, entries, payload_dir = load_artifact(artifact, work)
reject_terminal_engine_l2_failed_artifact(manifest, sha)
reject_terminal_device_plane_foundation_artifact(
manifest,
sha,
)
patch_id = manifest["id"]
component = manifest["component"]
root = component_root(component)
@@ -15898,6 +16697,15 @@ def apply_artifact(artifact):
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 not root.is_dir():
if bootstrap_root:
root.mkdir(parents=True, exist_ok=True)
@@ -16176,6 +16984,12 @@ def apply_artifact(artifact):
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":
device_plane_runtime_before = (
device_plane_runtime_inventory(
DEVICE_PLANE_RUNTIME_SERVICES
)
)
if is_engine_n8n_transition(component, entries):
transition_descriptor = read_engine_n8n_transition_descriptor(
@@ -16202,6 +17016,23 @@ def apply_artifact(artifact):
include_nginx_html = component == "engine" and component_publish_dist(component, entries)
create_backup(root, backup_dir, entries, include_nginx_html)
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 component == "platform" and touches_external_data_plane_files(entries):
validate_backup_partition(
entries,