Publish curated Unraid-AppOps-Gateway source from 2e53f628d284213b90ca2f9c5e121671f5933574
This commit is contained in:
1 parent
4f9c5a1106
commit
2facb8de34
13 files changed
+3868
-1749
No files matched your search
@@ -12,21 +12,21 @@ _REPOSITORY = "jens/itworx_mcp_hub"
|
||||
_COMPOSE_PROJECT = "itworx-mcp-hub"
|
||||
_TOPOLOGY_CONTRACT = "itworx.mcp-hub-topology/17-v1"
|
||||
|
||||
# One-time fail-closed re-attestation for the existing MCP Hub runtime. The
|
||||
# script never builds, restarts, recreates, stops or deploys a container. It
|
||||
# repairs only controller state after proving the exact target, Git ancestry,
|
||||
# complete Compose topology and every currently running image identity.
|
||||
# Read-only re-attestation for an already-successful modern MCP Hub receipt.
|
||||
# Unknown, failed, legacy and mixed-topology states require the guarded deploy
|
||||
# path. Reconciliation never repairs or writes controller state.
|
||||
_RECONCILE_SCRIPT = r'''
|
||||
import fcntl
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
repository, expected_compose_project = sys.argv[1:3]
|
||||
registry_path = Path("/controller-config/projects.json")
|
||||
@@ -80,7 +80,9 @@ if (
|
||||
or project.get("slug") != expected_compose_project
|
||||
or project.get("compose_project") != expected_compose_project
|
||||
or not isinstance(expected_containers, list)
|
||||
or set(expected_containers) != expected_fixed
|
||||
or any(not isinstance(item, str) for item in expected_containers)
|
||||
or len(expected_containers) != len(set(expected_containers))
|
||||
or frozenset(expected_containers) not in {frozenset(expected_fixed), frozenset(expected_runtime)}
|
||||
):
|
||||
raise SystemExit(23)
|
||||
expected_containers = sorted(expected_runtime)
|
||||
@@ -89,8 +91,6 @@ state_path = state_root / f"{expected_compose_project}.json"
|
||||
project_lock_path = state_root / "locks" / f"{expected_compose_project}.lock"
|
||||
owner, name = repository.split("/", 1)
|
||||
bare_repo = Path("/gitea-repositories") / owner / f"{name}.git"
|
||||
canonical_repository_root = Path("/mnt/cache/appdata/gitea/git/repositories")
|
||||
canonical_bare_repo = canonical_repository_root / owner / f"{name}.git"
|
||||
if not bare_repo.is_dir() or bare_repo.is_symlink():
|
||||
raise SystemExit(24)
|
||||
|
||||
@@ -106,44 +106,6 @@ def git_run(*args):
|
||||
)
|
||||
|
||||
|
||||
def canonical_git_run(*args):
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={canonical_bare_repo}", *args],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def canonical_image_ancestry_is_proven(recorded_revision, image_revision, expected_main):
|
||||
if not canonical_bare_repo.is_dir() or canonical_bare_repo.is_symlink():
|
||||
return False
|
||||
resolved_recorded = canonical_git_run("rev-parse", f"{recorded_revision}^{{commit}}")
|
||||
resolved_image = canonical_git_run("rev-parse", f"{image_revision}^{{commit}}")
|
||||
resolved_main = canonical_git_run("rev-parse", "refs/heads/main")
|
||||
if not (
|
||||
resolved_recorded.returncode == 0
|
||||
and resolved_recorded.stdout.strip() == recorded_revision
|
||||
and resolved_image.returncode == 0
|
||||
and resolved_image.stdout.strip() == image_revision
|
||||
and resolved_main.returncode == 0
|
||||
and resolved_main.stdout.strip() == expected_main
|
||||
):
|
||||
return False
|
||||
return (
|
||||
canonical_git_run(
|
||||
"merge-base", "--is-ancestor", recorded_revision, image_revision
|
||||
).returncode
|
||||
== 0
|
||||
and canonical_git_run(
|
||||
"merge-base", "--is-ancestor", image_revision, expected_main
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
main = git_run("rev-parse", "refs/heads/main")
|
||||
main_revision = main.stdout.strip()
|
||||
if main.returncode != 0 or re.fullmatch(r"[0-9a-f]{40}", main_revision) is None:
|
||||
@@ -219,13 +181,20 @@ def main_declares_sessionless_dataplane_image(container_name, image_ref):
|
||||
def main_declares_contextforge_compat_image(container_name, image_ref):
|
||||
if container_name != "itworx-mcp-hub-contextforge-1":
|
||||
return False
|
||||
candidate_reference = (
|
||||
re.fullmatch(r"[0-9a-f]{40}", recorded_live) is not None
|
||||
and image_ref == f"itworx-mcp-hub/contextforge-schema:{recorded_live}"
|
||||
)
|
||||
# The caller already binds recorded_live to the receipt and protected-main
|
||||
# ancestry. Read a candidate's build contract from that exact source tree.
|
||||
proof_revision = recorded_live if candidate_reference else main_revision
|
||||
manifest = git_run(
|
||||
"show",
|
||||
f"{main_revision}:infrastructure/images/contextforge/compat-image.env",
|
||||
f"{proof_revision}:infrastructure/images/contextforge/compat-image.env",
|
||||
)
|
||||
builder = git_run(
|
||||
"show",
|
||||
f"{main_revision}:scripts/build_contextforge_compat_image.sh",
|
||||
f"{proof_revision}:scripts/build_contextforge_compat_image.sh",
|
||||
)
|
||||
if manifest.returncode != 0 or builder.returncode != 0:
|
||||
return False
|
||||
@@ -257,7 +226,7 @@ def main_declares_contextforge_compat_image(container_name, image_ref):
|
||||
or re.fullmatch(r"[0-9a-f]{40}", upstream_commit) is None
|
||||
or re.fullmatch(r"[0-9a-f]{12}", alembic_head) is None
|
||||
or re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", mcp_sdk) is None
|
||||
or local_image != image_ref
|
||||
or (local_image != image_ref and not candidate_reference)
|
||||
or local_image != f"itworx-mcp-hub/contextforge-schema:{alembic_head}"
|
||||
):
|
||||
return False
|
||||
@@ -269,18 +238,370 @@ def main_declares_contextforge_compat_image(container_name, image_ref):
|
||||
'alembic -c mcpgateway/alembic.ini heads',
|
||||
'docker tag "$STAGED_IMAGE" "$CONTEXTFORGE_LOCAL_IMAGE"',
|
||||
)
|
||||
if candidate_reference:
|
||||
required_builder_contract += (
|
||||
'--release-candidate',
|
||||
'CANDIDATE_IMAGE="itworx-mcp-hub/contextforge-schema:${APPOPS_SOURCE_REVISION}"',
|
||||
'docker tag "$STAGED_IMAGE" "$CANDIDATE_IMAGE"',
|
||||
)
|
||||
return all(fragment in builder.stdout for fragment in required_builder_contract)
|
||||
|
||||
|
||||
state_root.mkdir(parents=True, exist_ok=True)
|
||||
project_lock_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
def live_attested_mounts(container):
|
||||
raw_mounts = container.get("Mounts")
|
||||
if not isinstance(raw_mounts, list):
|
||||
raise SystemExit(52)
|
||||
observed = []
|
||||
for raw in raw_mounts:
|
||||
if not isinstance(raw, dict):
|
||||
raise SystemExit(52)
|
||||
kind = str(raw.get("Type") or "")
|
||||
destination = str(raw.get("Destination") or "")
|
||||
if kind not in {"bind", "volume", "tmpfs"} or not destination:
|
||||
raise SystemExit(52)
|
||||
source = str(raw.get("Source") or "") if kind in {"bind", "volume"} else ""
|
||||
name = str(raw.get("Name") or "") if kind == "volume" else ""
|
||||
mount = {
|
||||
"type": kind,
|
||||
"source": os.path.normpath(source) if source else "",
|
||||
"destination": destination,
|
||||
"rw": raw.get("RW") is True,
|
||||
"name": name,
|
||||
}
|
||||
if kind == "volume":
|
||||
if not name:
|
||||
raise SystemExit(52)
|
||||
volume = subprocess.run(
|
||||
["docker", "volume", "inspect", name],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
if volume.returncode != 0:
|
||||
raise SystemExit(52)
|
||||
try:
|
||||
volume_items = json.loads(volume.stdout)
|
||||
volume_item = volume_items[0]
|
||||
options = volume_item.get("Options") or {}
|
||||
mountpoint = os.path.normpath(str(volume_item.get("Mountpoint") or ""))
|
||||
created_at = volume_item.get("CreatedAt")
|
||||
created = datetime.fromisoformat(str(created_at).replace("Z", "+00:00"))
|
||||
except Exception:
|
||||
raise SystemExit(52)
|
||||
if (
|
||||
not isinstance(options, dict)
|
||||
or str(volume_item.get("Name") or "") != name
|
||||
or mountpoint != mount["source"]
|
||||
or str(volume_item.get("Driver") or "") != str(raw.get("Driver") or "")
|
||||
or not isinstance(created_at, str)
|
||||
or created.tzinfo is None
|
||||
or created.utcoffset() is None
|
||||
or created.astimezone(timezone.utc) > datetime.now(timezone.utc)
|
||||
):
|
||||
raise SystemExit(52)
|
||||
mount["driver"] = str(volume_item.get("Driver") or "")
|
||||
mount["options"] = {str(key): str(value) for key, value in options.items()}
|
||||
mount["created_at"] = created_at
|
||||
observed.append(mount)
|
||||
return sorted(observed, key=lambda item: str(item["destination"]))
|
||||
|
||||
|
||||
def fixed_image_reference(service_name, image_ref):
|
||||
if service_name == "dataplane-router" and image_ref == "itworx-mcp-hub/dataplane:sessionless-final":
|
||||
return main_declares_sessionless_dataplane_image(
|
||||
"itworx-mcp-hub-dataplane-router-1", image_ref
|
||||
)
|
||||
owned = {
|
||||
"connector-appops": "gitea.example.com/jens/itworx-mcp-hub/connector-appops",
|
||||
"connector-gitea": "gitea.example.com/jens/itworx-mcp-hub/connector-gitea",
|
||||
"connector-ingestion": "gitea.example.com/jens/itworx-mcp-hub/connector-ingestion",
|
||||
"connector-knowledge": "gitea.example.com/jens/itworx-mcp-hub/connector-knowledge",
|
||||
"connector-mobilityops": "gitea.example.com/jens/itworx-mcp-hub/connector-mobilityops",
|
||||
"connector-projectbrain": "gitea.example.com/jens/itworx-mcp-hub/connector-projectbrain",
|
||||
"connector-stitch": "gitea.example.com/jens/itworx-mcp-hub/connector-stitch",
|
||||
"connector-unraid": "gitea.example.com/jens/itworx-mcp-hub/connector-unraid",
|
||||
"control-api": "gitea.example.com/jens/itworx-mcp-hub/control-api",
|
||||
"dataplane-router": "gitea.example.com/jens/itworx-mcp-hub/dataplane",
|
||||
"docker-read-broker": "gitea.example.com/jens/itworx-mcp-hub/docker-read-broker",
|
||||
"web": "gitea.example.com/jens/itworx-mcp-hub/web",
|
||||
"worker": "gitea.example.com/jens/itworx-mcp-hub/worker",
|
||||
}
|
||||
if service_name in owned:
|
||||
return (
|
||||
re.fullmatch(r"sha256:[0-9a-f]{64}", image_ref) is not None
|
||||
or (
|
||||
image_ref.startswith(owned[service_name] + "@sha256:")
|
||||
and re.fullmatch(r".+@sha256:[0-9a-f]{64}", image_ref) is not None
|
||||
)
|
||||
)
|
||||
if service_name == "postgres":
|
||||
return image_ref == "postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a" or (
|
||||
image_ref == "postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04"
|
||||
and main_declares_external_image(image_ref)
|
||||
)
|
||||
if service_name == "ingestion-clamd":
|
||||
return image_ref == "clamav/clamav:1.4.6@sha256:a5f03c12a79dbe9f6d8a527b6bb1ea053fa8dd061d3738a26897f055ee2d9303"
|
||||
if service_name == "redis":
|
||||
return image_ref == "redis:8.10.0@sha256:c29e49ab2f85760a3827b53882e6dd9f5c6c3f0bb7d724e07bb31cbf275a5236" or (
|
||||
image_ref == "redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48"
|
||||
and main_declares_external_image(image_ref)
|
||||
)
|
||||
if service_name == "contextforge":
|
||||
return main_declares_contextforge_compat_image(
|
||||
"itworx-mcp-hub-contextforge-1", image_ref
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def image_digest_matches(image, image_ref):
|
||||
repository, separator, digest = image_ref.partition("@")
|
||||
if not separator or re.fullmatch(r"sha256:[0-9a-f]{64}", digest) is None:
|
||||
return False
|
||||
first, slash, last = repository.partition("/")
|
||||
if slash:
|
||||
if ":" in last:
|
||||
last = last.rsplit(":", 1)[0]
|
||||
repository = first + slash + last
|
||||
elif ":" in repository:
|
||||
repository = repository.rsplit(":", 1)[0]
|
||||
aliases = {repository}
|
||||
first_component = repository.split("/", 1)[0]
|
||||
is_registry = "." in first_component or ":" in first_component or first_component == "localhost"
|
||||
if not is_registry:
|
||||
if "/" in repository:
|
||||
aliases.add("docker.io/" + repository)
|
||||
else:
|
||||
aliases.add("docker.io/library/" + repository)
|
||||
if repository.startswith("docker.io/"):
|
||||
aliases.add(repository.removeprefix("docker.io/"))
|
||||
digests = image.get("RepoDigests", [])
|
||||
return isinstance(digests, list) and any(
|
||||
"@" in str(item)
|
||||
and str(item).split("@", 1)[0] in aliases
|
||||
and str(item).split("@", 1)[1] == digest
|
||||
for item in digests
|
||||
)
|
||||
|
||||
|
||||
def receipt_bound_image_reference(launch_image, image_id, recorded, receipt_item):
|
||||
if not isinstance(recorded, dict) or not isinstance(receipt_item, dict):
|
||||
raise SystemExit(52)
|
||||
image_ref = receipt_item.get("image_ref")
|
||||
if (
|
||||
re.fullmatch(r"sha256:[0-9a-f]{64}", image_id) is None
|
||||
or not isinstance(image_ref, str)
|
||||
or not image_ref
|
||||
or recorded.get("image_ref") != image_ref
|
||||
or recorded.get("image_id") != image_id
|
||||
or receipt_item.get("image_id") != image_id
|
||||
or launch_image not in {image_ref, image_id}
|
||||
or (image_ref.startswith("sha256:") and image_ref != image_id)
|
||||
):
|
||||
raise SystemExit(52)
|
||||
return image_ref
|
||||
|
||||
|
||||
def trusted_private_path(path, expected_mode, *, directory):
|
||||
try:
|
||||
info = path.lstat()
|
||||
except OSError:
|
||||
return False
|
||||
if (
|
||||
path.is_symlink()
|
||||
or (not stat.S_ISDIR(info.st_mode) if directory else not stat.S_ISREG(info.st_mode))
|
||||
or info.st_uid != 0
|
||||
or (stat.S_IMODE(info.st_mode) != expected_mode if directory else stat.S_IMODE(info.st_mode) & 0o077 != 0)
|
||||
):
|
||||
return False
|
||||
try:
|
||||
attributes = os.listxattr(path, follow_symlinks=False)
|
||||
except OSError as exc:
|
||||
if exc.errno in {errno.ENOTSUP, getattr(errno, "EOPNOTSUPP", errno.ENOTSUP)}:
|
||||
attributes = []
|
||||
else:
|
||||
return False
|
||||
return not any(
|
||||
item in {"system.posix_acl_access", "system.posix_acl_default"}
|
||||
for item in attributes
|
||||
)
|
||||
|
||||
|
||||
def validate_migration_intent_state(
|
||||
state_root, state_bytes, repository, recorded_live, recorded_status,
|
||||
recorded_live_status, last_attempt, attempt_status, attempt_revision,
|
||||
):
|
||||
attempt_metadata = dict(last_attempt.get("metadata") or {})
|
||||
migration_attempt_id = str(attempt_metadata.get("mcpHubMigrationAttemptId") or "")
|
||||
migration_root = state_root / "mcp-hub-topology-migrations"
|
||||
migration_path = migration_root / "active-intent.json"
|
||||
if migration_root.is_symlink():
|
||||
raise SystemExit(56)
|
||||
if migration_root.exists() and not trusted_private_path(migration_root, 0o700, directory=True):
|
||||
raise SystemExit(56)
|
||||
if migration_path.is_symlink():
|
||||
raise SystemExit(56)
|
||||
if migration_path.exists():
|
||||
if (
|
||||
not trusted_private_path(migration_path, 0o600, directory=False)
|
||||
or stat.S_IMODE(migration_path.lstat().st_mode) != 0o600
|
||||
):
|
||||
raise SystemExit(56)
|
||||
try:
|
||||
intent_bytes = migration_path.read_bytes()
|
||||
intent = json.loads(intent_bytes.decode("utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(56)
|
||||
common_intent_fields = {
|
||||
"schema", "status", "attemptId", "repository", "candidateRevision",
|
||||
"priorLiveRevision", "backupManifestSha256", "backupComposeSha256",
|
||||
"baselineStateSha256", "startedAtEpoch",
|
||||
}
|
||||
observed_intent_fields = {"observedBaselinePath", "observedBaselineSha256"}
|
||||
observed_fields_present = set(intent) & observed_intent_fields if isinstance(intent, dict) else set()
|
||||
if observed_fields_present not in (set(), observed_intent_fields):
|
||||
raise SystemExit(56)
|
||||
required_intent_fields = common_intent_fields | observed_fields_present
|
||||
if isinstance(intent, dict) and intent.get("status") == "complete":
|
||||
required_intent_fields |= {"completedAtEpoch", "completedStateSha256"}
|
||||
if (
|
||||
not isinstance(intent, dict)
|
||||
or intent.get("status") != "complete"
|
||||
or set(intent) != required_intent_fields
|
||||
or intent.get("schema") != "itworx.mcp-hub-migration-intent/v1"
|
||||
or intent.get("repository") != repository
|
||||
or re.fullmatch(r"[0-9a-f]{32}", str(intent.get("attemptId") or "")) is None
|
||||
or intent.get("attemptId") != migration_attempt_id
|
||||
or intent.get("candidateRevision") != recorded_live
|
||||
or re.fullmatch(r"[0-9a-f]{40}", str(intent.get("priorLiveRevision") or "")) is None
|
||||
or any(
|
||||
re.fullmatch(r"[0-9a-f]{64}", str(intent.get(field) or "")) is None
|
||||
for field in ("backupManifestSha256", "backupComposeSha256", "baselineStateSha256", "completedStateSha256")
|
||||
)
|
||||
or type(intent.get("startedAtEpoch")) is not int
|
||||
or type(intent.get("completedAtEpoch")) is not int
|
||||
or intent.get("completedAtEpoch") < intent.get("startedAtEpoch")
|
||||
or intent.get("completedStateSha256") != hashlib.sha256(state_bytes).hexdigest()
|
||||
or recorded_status != "success"
|
||||
or recorded_live_status != "healthy"
|
||||
or attempt_status != "success"
|
||||
or attempt_revision != recorded_live
|
||||
or not isinstance(attempt_metadata.get("mcpHubBackupReceipt"), dict)
|
||||
or set(attempt_metadata.get("mcpHubBackupReceipt", {})) != {
|
||||
"schema", "repository", "revision", "backupDirectory", "manifestSha256",
|
||||
"composeSha256", "restoreVerified", "createdAtEpoch", "receiptPath",
|
||||
}
|
||||
or attempt_metadata.get("mcpHubBackupReceipt", {}).get("schema") != "itworx.mcp-hub-backup-receipt/v1"
|
||||
or attempt_metadata.get("mcpHubBackupReceipt", {}).get("repository") != repository
|
||||
or attempt_metadata.get("mcpHubBackupReceipt", {}).get("revision") != recorded_live
|
||||
or attempt_metadata.get("mcpHubBackupReceipt", {}).get("restoreVerified") is not True
|
||||
or type(attempt_metadata.get("mcpHubBackupReceipt", {}).get("createdAtEpoch")) is not int
|
||||
or re.fullmatch(r"[0-9a-f]{64}", str(attempt_metadata.get("mcpHubBackupReceipt", {}).get("manifestSha256") or "")) is None
|
||||
or re.fullmatch(r"[0-9a-f]{64}", str(attempt_metadata.get("mcpHubBackupReceipt", {}).get("composeSha256") or "")) is None
|
||||
or attempt_metadata.get("mcpHubBackupReceipt", {}).get("manifestSha256") != intent.get("backupManifestSha256")
|
||||
or attempt_metadata.get("mcpHubBackupReceipt", {}).get("composeSha256") != intent.get("backupComposeSha256")
|
||||
):
|
||||
raise SystemExit(56)
|
||||
backup_receipt = attempt_metadata.get("mcpHubBackupReceipt", {})
|
||||
receipt_root = state_root / "mcp-hub-backup-receipts"
|
||||
backup_path = Path(str(backup_receipt.get("receiptPath") or ""))
|
||||
if (
|
||||
backup_path.parent != receipt_root
|
||||
or re.fullmatch(
|
||||
rf"[0-9]+-{recorded_live[:12]}-[0-9a-f]{{16}}\.json",
|
||||
backup_path.name,
|
||||
) is None
|
||||
or backup_path.is_symlink()
|
||||
or not trusted_private_path(receipt_root, 0o700, directory=True)
|
||||
or not trusted_private_path(backup_path, 0o600, directory=False)
|
||||
or stat.S_IMODE(backup_path.lstat().st_mode) != 0o600
|
||||
):
|
||||
raise SystemExit(56)
|
||||
try:
|
||||
stored_backup_receipt = json.loads(backup_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(56)
|
||||
if stored_backup_receipt != {
|
||||
key: value for key, value in backup_receipt.items() if key != "receiptPath"
|
||||
}:
|
||||
raise SystemExit(56)
|
||||
backup_root = state_root / "mcp-hub-backups"
|
||||
backup_directory = Path(str(backup_receipt.get("backupDirectory") or ""))
|
||||
manifest_path = backup_directory / "manifest.json"
|
||||
if (
|
||||
backup_directory.parent != backup_root
|
||||
or re.fullmatch(
|
||||
rf"[0-9]+-{recorded_live[:12]}-[0-9a-f]{{16}}",
|
||||
backup_directory.name,
|
||||
) is None
|
||||
or not trusted_private_path(backup_root, 0o700, directory=True)
|
||||
or not trusted_private_path(backup_directory, 0o700, directory=True)
|
||||
or not trusted_private_path(manifest_path, 0o600, directory=False)
|
||||
or stat.S_IMODE(manifest_path.lstat().st_mode) != 0o600
|
||||
):
|
||||
raise SystemExit(56)
|
||||
try:
|
||||
manifest_bytes = manifest_path.read_bytes()
|
||||
manifest = json.loads(manifest_bytes.decode("utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(56)
|
||||
if (
|
||||
hashlib.sha256(manifest_bytes).hexdigest() != intent.get("backupManifestSha256")
|
||||
or not isinstance(manifest, dict)
|
||||
or manifest.get("schema") != "itworx.mcp-hub-backup/v1"
|
||||
or manifest.get("repository") != repository
|
||||
or manifest.get("revision") != recorded_live
|
||||
or manifest.get("backupDirectory") != str(backup_directory)
|
||||
or manifest.get("composeSha256") != intent.get("backupComposeSha256")
|
||||
or manifest.get("configRestoreVerified") is not True
|
||||
or dict(manifest.get("restore") or {}).get("status") != "verified"
|
||||
or set(dict(manifest.get("databases") or {})) != {"itworx_mcp_hub", "contextforge"}
|
||||
):
|
||||
raise SystemExit(56)
|
||||
if observed_fields_present:
|
||||
observed_path = Path(str(intent.get("observedBaselinePath") or ""))
|
||||
if (
|
||||
observed_path.parent != migration_root
|
||||
or re.fullmatch(
|
||||
rf"observed-baseline-{recorded_live[:12]}-[0-9a-f]{{20}}\.json",
|
||||
observed_path.name,
|
||||
) is None
|
||||
or observed_path.is_symlink()
|
||||
or not trusted_private_path(observed_path, 0o600, directory=False)
|
||||
or stat.S_IMODE(observed_path.lstat().st_mode) != 0o600
|
||||
):
|
||||
raise SystemExit(56)
|
||||
try:
|
||||
observed_bytes = observed_path.read_bytes()
|
||||
observed = json.loads(observed_bytes.decode("utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(56)
|
||||
if (
|
||||
hashlib.sha256(observed_bytes).hexdigest() != intent.get("observedBaselineSha256")
|
||||
or not isinstance(observed, dict)
|
||||
or observed.get("schema") != "itworx.mcp-hub-observed-baseline/v1"
|
||||
or observed.get("claim") != "observed_mixed_runtime_facts_only"
|
||||
or observed.get("candidateRevision") != recorded_live
|
||||
or observed.get("historicalStateSha256") != intent.get("baselineStateSha256")
|
||||
or observed.get("backupManifestSha256") != intent.get("backupManifestSha256")
|
||||
or observed.get("backupComposeSha256") != intent.get("backupComposeSha256")
|
||||
or observed.get("backupRestoreVerified") is not True
|
||||
or not isinstance(observed.get("postgresVolumeBaseline"), list)
|
||||
or len(observed.get("postgresVolumeBaseline", [])) != 2
|
||||
):
|
||||
raise SystemExit(56)
|
||||
elif migration_attempt_id:
|
||||
raise SystemExit(56)
|
||||
|
||||
|
||||
try:
|
||||
project_lock = project_lock_path.open("a+", encoding="utf-8")
|
||||
global_lock = global_lock_path.open("a+", encoding="utf-8")
|
||||
project_lock_fd = os.open(project_lock_path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
||||
global_lock_fd = os.open(global_lock_path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
||||
except Exception:
|
||||
raise SystemExit(29)
|
||||
|
||||
with project_lock:
|
||||
with os.fdopen(project_lock_fd, "rb") as project_lock, os.fdopen(global_lock_fd, "rb") as global_lock:
|
||||
try:
|
||||
fcntl.flock(project_lock, fcntl.LOCK_EX)
|
||||
except Exception:
|
||||
@@ -298,10 +619,17 @@ with project_lock:
|
||||
if current_projects.get(repository) != project:
|
||||
raise SystemExit(37)
|
||||
|
||||
if state_path.is_symlink() or not state_path.is_file():
|
||||
if (
|
||||
not trusted_private_path(state_root, 0o700, directory=True)
|
||||
or not trusted_private_path(state_root / "locks", 0o700, directory=True)
|
||||
or not trusted_private_path(project_lock_path, 0o600, directory=False)
|
||||
or not trusted_private_path(global_lock_path, 0o600, directory=False)
|
||||
or not trusted_private_path(state_path, 0o600, directory=False)
|
||||
):
|
||||
raise SystemExit(42)
|
||||
try:
|
||||
state = json.loads(state_path.read_text(encoding="utf-8"))
|
||||
state_bytes = state_path.read_bytes()
|
||||
state = json.loads(state_bytes.decode("utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(42)
|
||||
if not isinstance(state, dict) or state.get("schema_version") != 3 or state.get("repository") != repository:
|
||||
@@ -313,9 +641,12 @@ with project_lock:
|
||||
last_attempt = dict(state.get("last_attempt") or {})
|
||||
attempt_revision = str(last_attempt.get("revision") or "")
|
||||
attempt_status = str(last_attempt.get("status") or "")
|
||||
last_attempt_message = str(last_attempt.get("message") or "")
|
||||
receipt = state.get("live_receipt")
|
||||
recorded_artifacts = state.get("live_artifacts")
|
||||
validate_migration_intent_state(
|
||||
state_root, state_bytes, repository, recorded_live, recorded_status,
|
||||
recorded_live_status, last_attempt, attempt_status, attempt_revision,
|
||||
)
|
||||
|
||||
registered_live_containers = (
|
||||
recorded_artifacts.get("containers")
|
||||
@@ -326,86 +657,73 @@ with project_lock:
|
||||
print("mcp-hub-topology-migration-required", flush=True)
|
||||
raise SystemExit(56)
|
||||
|
||||
if set(expected_containers) != expected_runtime:
|
||||
raise SystemExit(56)
|
||||
|
||||
receipt_payload = receipt if isinstance(receipt, dict) else {}
|
||||
receipt_tree = str(receipt_payload.get("tree") or "")
|
||||
receipt_tree_digest = str(receipt_payload.get("treeDigest") or "")
|
||||
raw_receipt_build = receipt_payload.get("build")
|
||||
receipt_build = raw_receipt_build if isinstance(raw_receipt_build, dict) else {}
|
||||
expected_build = {
|
||||
"id": recorded_live[:12],
|
||||
"repository": repository,
|
||||
"revision": recorded_live,
|
||||
"tree": receipt_tree,
|
||||
"treeDigest": receipt_tree_digest,
|
||||
"builtAt": str(receipt_build.get("builtAt") or ""),
|
||||
}
|
||||
receipt_identity_valid = (
|
||||
re.fullmatch(r"[0-9a-f]{40}", receipt_tree) is not None
|
||||
and receipt_tree_digest == hashlib.sha256(
|
||||
f"git-tree:{receipt_tree}".encode("utf-8")
|
||||
).hexdigest()
|
||||
and receipt_build == expected_build
|
||||
and re.fullmatch(
|
||||
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z",
|
||||
expected_build["builtAt"],
|
||||
) is not None
|
||||
and isinstance(recorded_artifacts, dict)
|
||||
and set(recorded_artifacts)
|
||||
== {"containers", "compose_sha256", "topology_contract"}
|
||||
and re.fullmatch(
|
||||
r"[0-9a-f]{64}", str(recorded_artifacts.get("compose_sha256") or "")
|
||||
) is not None
|
||||
)
|
||||
|
||||
already_reconciled = (
|
||||
recorded_status == "success"
|
||||
and recorded_live_status == "healthy"
|
||||
and state.get("revision") == recorded_live
|
||||
and state.get("live_legacy_provenance") is False
|
||||
and re.fullmatch(r"[0-9a-f]{40}", recorded_live) is not None
|
||||
and isinstance(recorded_artifacts, dict)
|
||||
and state.get("compose_project") == expected_compose_project
|
||||
and state.get("slug") == expected_compose_project
|
||||
and attempt_status == "success"
|
||||
and attempt_revision == recorded_live
|
||||
and isinstance(registered_live_containers, dict)
|
||||
and set(registered_live_containers) == expected_runtime
|
||||
and recorded_artifacts.get("topology_contract") == topology_contract
|
||||
and (
|
||||
state.get("live_legacy_provenance") is True
|
||||
or (
|
||||
isinstance(receipt, dict)
|
||||
and receipt.get("schema") == "gitea-deploy-receipt.v1"
|
||||
and receipt.get("repository") == repository
|
||||
and receipt.get("revision") == recorded_live
|
||||
)
|
||||
)
|
||||
and state.get("artifacts") == recorded_artifacts
|
||||
and isinstance(receipt, dict)
|
||||
and set(receipt) == {"schema", "repository", "revision", "tree", "treeDigest", "build", "containers", "live"}
|
||||
and receipt.get("schema") == "gitea-deploy-receipt.v1"
|
||||
and receipt.get("repository") == repository
|
||||
and receipt.get("revision") == recorded_live
|
||||
and receipt_identity_valid
|
||||
and isinstance(receipt.get("containers"), dict)
|
||||
and set(dict(receipt.get("containers") or {})) == expected_runtime
|
||||
and receipt.get("live") is None
|
||||
)
|
||||
if not already_reconciled and (
|
||||
recorded_status != "failed"
|
||||
or recorded_live_status not in {"healthy", "unknown"}
|
||||
or state.get("live_legacy_provenance") is not False
|
||||
or attempt_status != "failed"
|
||||
or re.fullmatch(r"[0-9a-f]{40}", recorded_live) is None
|
||||
or re.fullmatch(r"[0-9a-f]{40}", attempt_revision) is None
|
||||
or attempt_revision == recorded_live
|
||||
or not isinstance(receipt, dict)
|
||||
or receipt.get("schema") != "gitea-deploy-receipt.v1"
|
||||
or receipt.get("repository") != repository
|
||||
or receipt.get("revision") != recorded_live
|
||||
or not isinstance(recorded_artifacts, dict)
|
||||
):
|
||||
raise SystemExit(52)
|
||||
|
||||
# deploy.py deliberately downgrades the prior live status to unknown
|
||||
# when capture_compose_rollback() fails before production mutation.
|
||||
# Accept that state only when the durable attempt message is one of the
|
||||
# bounded snapshot-stage failure classes and no rollback/source-restore
|
||||
# error was recorded. The runtime is still re-proven container by
|
||||
# container below before any controller state is changed.
|
||||
snapshot_failure_prefixes = (
|
||||
"Refusing to mutate a Compose project with unrecorded containers:",
|
||||
"Deployment state claims a live revision but all production containers are missing",
|
||||
"Cannot capture a complete rollback snapshot; containers are missing:",
|
||||
"Existing production containers require a valid healthy live deployment state",
|
||||
"Container ",
|
||||
"Existing deployment state is not bound to its live revision",
|
||||
"Recorded deployment receipt ",
|
||||
"Existing deployment no longer matches its verified live receipt",
|
||||
"Expected containers are not healthy/running:",
|
||||
"Compose project container set does not match the allowlisted runtime:",
|
||||
"Containers are linked to the wrong Compose project:",
|
||||
"Running container image IDs do not match the verified artifacts:",
|
||||
"Immutable image provenance does not match the requested deployment:",
|
||||
)
|
||||
if (
|
||||
not already_reconciled
|
||||
and recorded_live_status == "unknown"
|
||||
and (
|
||||
not last_attempt_message.startswith(snapshot_failure_prefixes)
|
||||
or "; rollback error:" in last_attempt_message
|
||||
or "; source rollback error:" in last_attempt_message
|
||||
)
|
||||
):
|
||||
raise SystemExit(55)
|
||||
if not already_reconciled:
|
||||
raise SystemExit(56 if recorded_live_status in {"unknown", "healthy"} else 52)
|
||||
|
||||
resolved_live = git_run("rev-parse", f"{recorded_live}^{{commit}}")
|
||||
if resolved_live.returncode != 0 or resolved_live.stdout.strip() != recorded_live:
|
||||
raise SystemExit(25)
|
||||
if git_run("merge-base", "--is-ancestor", recorded_live, main_revision).returncode != 0:
|
||||
raise SystemExit(52)
|
||||
if not already_reconciled:
|
||||
resolved_attempt = git_run("rev-parse", f"{attempt_revision}^{{commit}}")
|
||||
if resolved_attempt.returncode != 0 or resolved_attempt.stdout.strip() != attempt_revision:
|
||||
raise SystemExit(25)
|
||||
if git_run("merge-base", "--is-ancestor", recorded_live, attempt_revision).returncode != 0:
|
||||
raise SystemExit(52)
|
||||
if git_run("merge-base", "--is-ancestor", attempt_revision, main_revision).returncode != 0:
|
||||
raise SystemExit(52)
|
||||
|
||||
listed = subprocess.run(
|
||||
[
|
||||
@@ -428,11 +746,10 @@ with project_lock:
|
||||
raise SystemExit(47)
|
||||
|
||||
old_containers = recorded_artifacts.get("containers")
|
||||
receipt_containers = dict(receipt.get("containers") or {})
|
||||
if not isinstance(old_containers, dict) or set(old_containers) != set(expected_containers):
|
||||
raise SystemExit(52)
|
||||
|
||||
container_artifacts = {}
|
||||
drift_found = False
|
||||
for name in expected_containers:
|
||||
inspected = subprocess.run(
|
||||
["docker", "inspect", name],
|
||||
@@ -450,17 +767,31 @@ with project_lock:
|
||||
raise SystemExit(44)
|
||||
runtime_state = dict(container.get("State") or {})
|
||||
health = str(dict(runtime_state.get("Health") or {}).get("Status") or "none")
|
||||
labels = dict(dict(container.get("Config") or {}).get("Labels") or {})
|
||||
config = dict(container.get("Config") or {})
|
||||
labels = dict(config.get("Labels") or {})
|
||||
name_service = name.removeprefix("itworx-mcp-hub-").removesuffix("-1")
|
||||
image_id = str(container.get("Image") or "")
|
||||
image_ref = str(config.get("Image") or "")
|
||||
if not runtime_state.get("Running") or health in {"starting", "unhealthy"}:
|
||||
launch_image = str(config.get("Image") or "")
|
||||
container_id = str(container.get("Id") or "")
|
||||
if runtime_state.get("Running") is not True or health != "healthy":
|
||||
raise SystemExit(45)
|
||||
if labels.get("com.docker.compose.project") != expected_compose_project:
|
||||
if (
|
||||
labels.get("com.docker.compose.project") != expected_compose_project
|
||||
or labels.get("com.docker.compose.service") != name_service
|
||||
):
|
||||
raise SystemExit(47)
|
||||
if re.fullmatch(r"sha256:[0-9a-f]{64}", image_id) is None or not image_ref:
|
||||
if (
|
||||
re.fullmatch(r"sha256:[0-9a-f]{64}", image_id) is None
|
||||
or not launch_image
|
||||
or not container_id
|
||||
):
|
||||
raise SystemExit(46)
|
||||
|
||||
recorded = old_containers.get(name)
|
||||
receipt_item = receipt_containers.get(name)
|
||||
image_ref = receipt_bound_image_reference(
|
||||
launch_image, image_id, recorded, receipt_item
|
||||
)
|
||||
image_result = subprocess.run(
|
||||
["docker", "image", "inspect", image_id],
|
||||
text=True,
|
||||
@@ -476,149 +807,80 @@ with project_lock:
|
||||
except Exception:
|
||||
raise SystemExit(50)
|
||||
image_labels = dict(dict(image.get("Config") or {}).get("Labels") or {})
|
||||
image_source = str(image_labels.get("org.opencontainers.image.source") or "")
|
||||
image_revision = str(image_labels.get("org.opencontainers.image.revision") or "")
|
||||
|
||||
recorded = old_containers.get(name)
|
||||
if not isinstance(recorded, dict):
|
||||
raise SystemExit(52)
|
||||
recorded_image_id = str(recorded.get("image_id") or "")
|
||||
recorded_container_id = str(recorded.get("container_id") or "")
|
||||
if re.fullmatch(r"sha256:[0-9a-f]{64}", recorded_image_id) is None:
|
||||
raise SystemExit(52)
|
||||
|
||||
image_changed = image_id != recorded_image_id
|
||||
if image_changed:
|
||||
contextforge_compat_image = main_declares_contextforge_compat_image(name, image_ref)
|
||||
sessionless_dataplane_image = (
|
||||
not already_reconciled
|
||||
and main_declares_sessionless_dataplane_image(name, image_ref)
|
||||
)
|
||||
if contextforge_compat_image or sessionless_dataplane_image:
|
||||
pass
|
||||
elif image_source == owned_image_source:
|
||||
if re.fullmatch(r"[0-9a-f]{40}", image_revision) is None:
|
||||
raise SystemExit(51)
|
||||
resolved_image = git_run("rev-parse", f"{image_revision}^{{commit}}")
|
||||
mounted_recorded_to_image = git_run("merge-base", "--is-ancestor", recorded_live, image_revision)
|
||||
mounted_image_to_main = git_run("merge-base", "--is-ancestor", image_revision, main_revision)
|
||||
mounted_ancestry_proven = (
|
||||
resolved_image.returncode == 0
|
||||
and resolved_image.stdout.strip() == image_revision
|
||||
and mounted_recorded_to_image.returncode == 0
|
||||
and mounted_image_to_main.returncode == 0
|
||||
)
|
||||
if not mounted_ancestry_proven and not canonical_image_ancestry_is_proven(
|
||||
recorded_live, image_revision, main_revision
|
||||
):
|
||||
raise SystemExit(51)
|
||||
elif not main_declares_external_image(image_ref):
|
||||
raise SystemExit(51)
|
||||
|
||||
container_id = str(container.get("Id") or "")
|
||||
if not container_id:
|
||||
raise SystemExit(46)
|
||||
if image_id != recorded_image_id or container_id != recorded_container_id:
|
||||
drift_found = True
|
||||
|
||||
container_artifacts[name] = {
|
||||
artifact = {
|
||||
"container_id": container_id,
|
||||
"image_id": image_id,
|
||||
"image_ref": image_ref,
|
||||
"container_revision": str(labels.get("org.opencontainers.image.revision") or ""),
|
||||
"image_revision": image_revision,
|
||||
"image_source": image_source,
|
||||
"image_revision": str(image_labels.get("org.opencontainers.image.revision") or ""),
|
||||
"image_source": str(image_labels.get("org.opencontainers.image.source") or ""),
|
||||
"image_tree": str(image_labels.get("org.opencontainers.image.source-tree") or ""),
|
||||
"image_version": str(image_labels.get("org.opencontainers.image.version") or ""),
|
||||
"image_created": str(image_labels.get("org.opencontainers.image.created") or ""),
|
||||
}
|
||||
|
||||
if already_reconciled:
|
||||
if drift_found:
|
||||
if str(image.get("Id") or "") != image_id:
|
||||
raise SystemExit(46)
|
||||
if (
|
||||
set(recorded) != set(artifact) | {"mounts_json"}
|
||||
or set(receipt_item) != set(artifact) | {"mounts_sha256"}
|
||||
):
|
||||
raise SystemExit(52)
|
||||
print("already-reconciled", flush=True)
|
||||
raise SystemExit(0)
|
||||
if not drift_found:
|
||||
raise SystemExit(52)
|
||||
for key, value in artifact.items():
|
||||
if recorded.get(key) != value or receipt_item.get(key) != value:
|
||||
raise SystemExit(52)
|
||||
|
||||
artifacts = {
|
||||
"containers": container_artifacts,
|
||||
"compose_sha256": "",
|
||||
"topology_contract": topology_contract,
|
||||
}
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
"schema_version": 3,
|
||||
"repository": repository,
|
||||
"slug": expected_compose_project,
|
||||
"compose_project": expected_compose_project,
|
||||
"revision": recorded_live,
|
||||
"live_revision": recorded_live,
|
||||
"live_status": "healthy",
|
||||
"live_legacy_provenance": True,
|
||||
"live_artifacts": artifacts,
|
||||
"live_receipt": {},
|
||||
"rollback_snapshot": {},
|
||||
"status": "success",
|
||||
"message": "Verified mixed MCP Hub runtime reconciled as a legacy provenance baseline",
|
||||
"updated_at_epoch": now,
|
||||
"artifacts": artifacts,
|
||||
"last_attempt": {
|
||||
"revision": recorded_live,
|
||||
"status": "success",
|
||||
"message": "Verified MCP Hub runtime state reconciliation",
|
||||
"updated_at_epoch": now,
|
||||
"metadata": {"reconciled_failed_revision": attempt_revision},
|
||||
},
|
||||
"previous_revision": recorded_live,
|
||||
"rollback_restored_revision": "",
|
||||
}
|
||||
|
||||
backup = state_path.with_name(f"{state_path.name}.mcp-hub-reconcile-backup-{now}-{os.getpid()}")
|
||||
temporary = state_path.with_name(f".{state_path.name}.{os.getpid()}.mcp-hub-reconcile.tmp")
|
||||
try:
|
||||
mode = stat.S_IMODE(state_path.stat().st_mode)
|
||||
shutil.copy2(state_path, backup)
|
||||
with temporary.open("x", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.chmod(temporary, mode)
|
||||
os.replace(temporary, state_path)
|
||||
directory_fd = os.open(str(state_root), os.O_RDONLY)
|
||||
raw_mounts = recorded.get("mounts_json")
|
||||
if not isinstance(raw_mounts, str):
|
||||
raise SystemExit(52)
|
||||
try:
|
||||
os.fsync(directory_fd)
|
||||
finally:
|
||||
os.close(directory_fd)
|
||||
except Exception:
|
||||
try:
|
||||
temporary.unlink(missing_ok=True)
|
||||
recorded_mounts = json.loads(raw_mounts)
|
||||
except Exception:
|
||||
pass
|
||||
raise SystemExit(53)
|
||||
raise SystemExit(52)
|
||||
mount_bytes = json.dumps(
|
||||
recorded_mounts, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
if (
|
||||
not isinstance(recorded_mounts, list)
|
||||
or hashlib.sha256(mount_bytes).hexdigest() != receipt_item.get("mounts_sha256")
|
||||
or live_attested_mounts(container) != recorded_mounts
|
||||
):
|
||||
raise SystemExit(52)
|
||||
|
||||
if not fixed_image_reference(name_service, image_ref):
|
||||
raise SystemExit(51)
|
||||
owned_service = name_service in {
|
||||
"connector-appops", "connector-gitea", "connector-ingestion",
|
||||
"connector-knowledge", "connector-mobilityops", "connector-projectbrain",
|
||||
"connector-stitch", "connector-unraid", "control-api", "dataplane-router",
|
||||
"docker-read-broker", "web", "worker",
|
||||
}
|
||||
if owned_service and image_ref != "itworx-mcp-hub/dataplane:sessionless-final":
|
||||
if (
|
||||
artifact["image_source"] != owned_image_source
|
||||
or artifact["image_revision"] != recorded_live
|
||||
or artifact["image_tree"] != receipt_tree
|
||||
or artifact["image_version"] != recorded_live[:12]
|
||||
or artifact["image_created"] != expected_build["builtAt"]
|
||||
):
|
||||
raise SystemExit(51)
|
||||
if "@sha256:" in image_ref and not image_digest_matches(image, image_ref):
|
||||
raise SystemExit(51)
|
||||
|
||||
try:
|
||||
verify = json.loads(state_path.read_text(encoding="utf-8"))
|
||||
if state_path.read_bytes() != state_bytes:
|
||||
raise SystemExit(52)
|
||||
current_projects = json.loads(registry_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(54)
|
||||
verified_containers = dict(dict(verify.get("live_artifacts") or {}).get("containers") or {})
|
||||
if not (
|
||||
verify.get("repository") == repository
|
||||
and verify.get("live_revision") == recorded_live
|
||||
and verify.get("live_status") == "healthy"
|
||||
and verify.get("live_legacy_provenance") is True
|
||||
and verify.get("status") == "success"
|
||||
and set(verified_containers) == set(expected_containers)
|
||||
and all(str(dict(verified_containers[name]).get("image_id") or "") == str(container_artifacts[name]["image_id"]) for name in expected_containers)
|
||||
):
|
||||
raise SystemExit(54)
|
||||
raise SystemExit(37)
|
||||
if current_projects.get(repository) != project:
|
||||
raise SystemExit(37)
|
||||
|
||||
print("reconciled", flush=True)
|
||||
print("already-reconciled", flush=True)
|
||||
'''
|
||||
|
||||
|
||||
class McpHubDeploymentReconciliation:
|
||||
"""Re-attest only the exact proven MCP Hub state drift, without touching runtime."""
|
||||
"""Read-only verify only an existing, exact modern MCP Hub success receipt."""
|
||||
|
||||
def __init__(self, portainer: PortainerConnector | None = None):
|
||||
self.portainer = portainer or PortainerConnector()
|
||||
@@ -706,11 +968,8 @@ class McpHubDeploymentReconciliation:
|
||||
47: "live MCP Hub Compose project differs from the controller target",
|
||||
50: "a live MCP Hub image cannot be inspected",
|
||||
51: "live MCP Hub image revision or external immutable image is not proven by the recorded-live to main chain",
|
||||
52: "controller state drift is not the exact known recoverable MCP Hub state",
|
||||
53: "reconciled MCP Hub deployment state could not be written durably",
|
||||
54: "reconciled MCP Hub deployment state did not verify after write",
|
||||
55: "unknown MCP Hub live status was not produced by a proven recoverable snapshot-stage failure",
|
||||
56: "MCP Hub 15-to-17 topology migration requires its guarded protected-main deployment",
|
||||
52: "controller state or live facts do not match the exact modern MCP Hub receipt",
|
||||
56: "MCP Hub legacy, unknown, mixed, or unresolved migration state requires guarded deployment or data-aware recovery",
|
||||
}
|
||||
if exit_code != 0:
|
||||
if exit_code == 56:
|
||||
|
||||
Reference in new issue
Block a user