927 lines
33 KiB
Python
927 lines
33 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
import types
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
@pytest.fixture
|
|
def controller():
|
|
if "fcntl" not in sys.modules:
|
|
shim = types.ModuleType("fcntl")
|
|
shim.LOCK_EX, shim.LOCK_UN, shim.flock = 2, 8, lambda *_args: None
|
|
sys.modules["fcntl"] = shim
|
|
spec = importlib.util.spec_from_file_location(
|
|
"hub_candidate_controller", ROOT / "ops/gitea-deploy-broker/deploy.py"
|
|
)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.fixture
|
|
def prepared(controller, tmp_path, monkeypatch):
|
|
monkeypatch.setattr(controller, "STATE_ROOT", tmp_path)
|
|
monkeypatch.setattr(controller, "_mcp_hub_require_trusted_state_root", lambda: None)
|
|
monkeypatch.setattr(controller, "_mcp_hub_require_no_posix_acl", lambda _path: None)
|
|
revision = "a" * 40
|
|
directory = tmp_path / "mcp-hub-release-candidates" / revision
|
|
directory.mkdir(parents=True, mode=0o700)
|
|
directory.parent.chmod(0o700)
|
|
now = (
|
|
datetime.now(timezone.utc)
|
|
.isoformat(timespec="milliseconds")
|
|
.replace("+00:00", "Z")
|
|
)
|
|
metadata = {
|
|
"id": revision[:12],
|
|
"repository": controller.MCP_HUB_REPOSITORY,
|
|
"revision": revision,
|
|
"tree": "b" * 40,
|
|
"treeDigest": hashlib.sha256(("git-tree:" + "b" * 40).encode()).hexdigest(),
|
|
"builtAt": now,
|
|
}
|
|
project = {
|
|
"_mcp_hub_contract": controller.MCP_HUB_TOPOLOGY_CONTRACT,
|
|
"_repository_path": str(tmp_path / "hub.git"),
|
|
"_deployment_metadata": metadata,
|
|
"_mcp_hub_source_date_epoch": "1790888863",
|
|
}
|
|
monkeypatch.setattr(
|
|
controller, "deployment_metadata", lambda *_args, **_kwargs: metadata
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "mcp_hub_source_date_epoch", lambda *_args: "1790888863"
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "mcp_hub_stage_release_source", lambda *_args: tmp_path / "release"
|
|
)
|
|
checks = []
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"_mcp_hub_check_candidate_exceptions",
|
|
lambda *_args: checks.append("gate"),
|
|
)
|
|
images = {}
|
|
model = {"services": {}}
|
|
|
|
def write(name, value):
|
|
data = (
|
|
json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n"
|
|
).encode()
|
|
path = directory / name
|
|
path.write_bytes(data)
|
|
path.chmod(0o600)
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
for index, service in enumerate(controller.MCP_HUB_SERVICES, 1):
|
|
image_id = "sha256:" + f"{index:064x}"
|
|
ref = f"fixture/{service}@sha256:" + f"{index:064x}"
|
|
if service == "contextforge":
|
|
ref = f"itworx-mcp-hub/contextforge-schema:{revision}"
|
|
model["services"][service] = {"image": ref}
|
|
vuln = {
|
|
"SchemaVersion": 2,
|
|
"Trivy": {"Version": "0.73.0"},
|
|
"CreatedAt": now,
|
|
"Metadata": {
|
|
"ImageID": image_id,
|
|
"ImageConfig": {"architecture": "amd64", "os": "linux"},
|
|
},
|
|
"Results": [
|
|
{"Class": "os-pkgs", "Packages": [{"Name": "fixture", "Version": "1"}]}
|
|
],
|
|
}
|
|
secret = {
|
|
"ImageID": image_id,
|
|
"CreatedAt": now,
|
|
"TrivyVersion": "0.73.0",
|
|
"FindingCount": 0,
|
|
"SensitiveFindingDetailsPersisted": False,
|
|
"Results": [{"FindingCount": 0}],
|
|
}
|
|
images[service] = {
|
|
"imageRef": ref,
|
|
"imageId": image_id,
|
|
"vulnerabilitySha256": write(f"vulnerability-{service}.json", vuln),
|
|
"secretSha256": write(f"secret-{service}.json", secret),
|
|
}
|
|
candidate = {
|
|
"schema": "itworx.mcp-hub-release-candidate/v1",
|
|
"repository": controller.MCP_HUB_REPOSITORY,
|
|
"revision": revision,
|
|
"build": metadata,
|
|
"sourceDateEpoch": "1790888863",
|
|
"images": images,
|
|
"composeSha256": hashlib.sha256(
|
|
json.dumps(model, sort_keys=True, separators=(",", ":")).encode()
|
|
).hexdigest(),
|
|
"databaseMetadataSha256": write(
|
|
"trivy-db-metadata.json", {"UpdatedAt": now, "DownloadedAt": now}
|
|
),
|
|
"summarySha256": write("scan-summary.json", {"unresolvedCriticalOrHigh": []}),
|
|
"digestsSha256": write(
|
|
"image-digests.json",
|
|
{
|
|
"images": {
|
|
s: {"imageId": v["imageId"], "tag": v["imageRef"]}
|
|
for s, v in images.items()
|
|
}
|
|
},
|
|
),
|
|
}
|
|
candidate_sha = write("candidate.json", candidate)
|
|
exceptions_sha = write("exceptions.json", {"exceptions": []})
|
|
write(
|
|
"approval.json",
|
|
{
|
|
"schema": "itworx.mcp-hub-cve-approval/v1",
|
|
"candidateSha256": candidate_sha,
|
|
"exceptionsSha256": exceptions_sha,
|
|
},
|
|
)
|
|
labels = {
|
|
"org.opencontainers.image.source": controller.MCP_HUB_OCI_IMAGE_SOURCE,
|
|
"org.opencontainers.image.revision": revision,
|
|
"org.opencontainers.image.source-tree": metadata["tree"],
|
|
"org.opencontainers.image.version": metadata["id"],
|
|
"org.opencontainers.image.created": metadata["builtAt"],
|
|
"org.opencontainers.image.source-date-epoch": "1790888863",
|
|
}
|
|
facts = {
|
|
v["imageRef"]: {
|
|
"Id": v["imageId"],
|
|
"Config": {"User": "10001:10001", "Labels": dict(labels)},
|
|
}
|
|
for v in images.values()
|
|
}
|
|
monkeypatch.setattr(controller, "inspect_image", lambda ref: facts.get(ref))
|
|
return types.SimpleNamespace(
|
|
project=project,
|
|
revision=revision,
|
|
directory=directory,
|
|
candidate=candidate,
|
|
model=model,
|
|
metadata=metadata,
|
|
write=write,
|
|
checks=checks,
|
|
facts=facts,
|
|
)
|
|
|
|
|
|
def test_fresh_complete_candidate_freezes_metadata_and_checks_explicit_gate(
|
|
controller, prepared
|
|
):
|
|
result = controller.mcp_hub_load_release_candidate(
|
|
prepared.project, prepared.revision
|
|
)
|
|
assert result["build"] == prepared.metadata
|
|
assert set(result["images"]) == set(controller.MCP_HUB_SERVICES)
|
|
assert prepared.checks == ["gate"]
|
|
controller.mcp_hub_require_candidate_images(
|
|
prepared.project, prepared.model, prepared.revision
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
[
|
|
"missing",
|
|
"extra",
|
|
"revision",
|
|
"build",
|
|
"epoch",
|
|
"scan_hash",
|
|
"scan_identity",
|
|
"scan_empty",
|
|
"secret",
|
|
"stale",
|
|
"db_stale",
|
|
"approval",
|
|
"summary",
|
|
"digests",
|
|
],
|
|
)
|
|
def test_candidate_drift_rejects_before_any_production_action(
|
|
controller, prepared, mutation
|
|
):
|
|
value = copy.deepcopy(prepared.candidate)
|
|
if mutation == "missing":
|
|
value["images"].pop("web")
|
|
elif mutation == "extra":
|
|
value["images"]["foreign"] = value["images"]["web"]
|
|
elif mutation == "revision":
|
|
value["revision"] = "c" * 40
|
|
elif mutation == "build":
|
|
value["build"]["tree"] = "c" * 40
|
|
elif mutation == "epoch":
|
|
value["sourceDateEpoch"] = "1"
|
|
elif mutation == "scan_hash":
|
|
value["images"]["web"]["vulnerabilitySha256"] = "c" * 64
|
|
elif mutation in {"scan_identity", "scan_empty", "stale"}:
|
|
path = prepared.directory / "vulnerability-web.json"
|
|
report = json.loads(path.read_text())
|
|
if mutation == "scan_identity":
|
|
report["Metadata"]["ImageID"] = "sha256:" + "c" * 64
|
|
elif mutation == "scan_empty":
|
|
report["Results"] = []
|
|
else:
|
|
report["CreatedAt"] = "2026-01-01T00:00:00Z"
|
|
value["images"]["web"]["vulnerabilitySha256"] = prepared.write(
|
|
path.name, report
|
|
)
|
|
elif mutation == "secret":
|
|
report = json.loads((prepared.directory / "secret-web.json").read_text())
|
|
report["FindingCount"] = 1
|
|
value["images"]["web"]["secretSha256"] = prepared.write(
|
|
"secret-web.json", report
|
|
)
|
|
elif mutation == "db_stale":
|
|
value["databaseMetadataSha256"] = prepared.write(
|
|
"trivy-db-metadata.json",
|
|
{
|
|
"UpdatedAt": "2026-01-01T00:00:00Z",
|
|
"DownloadedAt": "2026-01-01T00:00:00Z",
|
|
},
|
|
)
|
|
elif mutation == "summary":
|
|
value["summarySha256"] = prepared.write(
|
|
"scan-summary.json", {"unresolvedCriticalOrHigh": [{"package": "invented"}]}
|
|
)
|
|
elif mutation == "digests":
|
|
value["digestsSha256"] = prepared.write("image-digests.json", {"images": {}})
|
|
candidate_sha = prepared.write("candidate.json", value)
|
|
exceptions_sha = hashlib.sha256(
|
|
(prepared.directory / "exceptions.json").read_bytes()
|
|
).hexdigest()
|
|
prepared.write(
|
|
"approval.json",
|
|
{
|
|
"schema": "itworx.mcp-hub-cve-approval/v1",
|
|
"candidateSha256": "c" * 64 if mutation == "approval" else candidate_sha,
|
|
"exceptionsSha256": exceptions_sha,
|
|
},
|
|
)
|
|
with pytest.raises(controller.DeployError):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
assert prepared.checks == []
|
|
|
|
|
|
def test_pending_or_expired_human_exception_gate_cannot_be_substituted(
|
|
controller, prepared, monkeypatch
|
|
):
|
|
def denied(*_args):
|
|
raise controller.DeployError("CVE exception gate did not pass")
|
|
|
|
monkeypatch.setattr(controller, "_mcp_hub_check_candidate_exceptions", denied)
|
|
with pytest.raises(controller.DeployError, match="CVE exception gate"):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"service", ["web", "contextforge", "postgres", "connector-ingestion"]
|
|
)
|
|
def test_retagged_or_changed_candidate_image_is_rejected_before_apply(
|
|
controller, prepared, service
|
|
):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.facts[prepared.candidate["images"][service]["imageRef"]]["Id"] = (
|
|
"sha256:" + "f" * 64
|
|
)
|
|
with pytest.raises(controller.DeployError, match="image identity"):
|
|
controller.mcp_hub_require_candidate_images(
|
|
prepared.project, prepared.model, prepared.revision
|
|
)
|
|
|
|
|
|
def test_changed_rendered_model_is_rejected(controller, prepared):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.model["services"]["web"]["environment"] = {"unexpected": "change"}
|
|
with pytest.raises(controller.DeployError, match="Compose"):
|
|
controller.mcp_hub_require_candidate_images(
|
|
prepared.project, prepared.model, prepared.revision
|
|
)
|
|
|
|
|
|
def test_changed_approval_is_rejected_between_stages(controller, prepared):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.write("exceptions.json", {"exceptions": [{"approvalStatus": "pending"}]})
|
|
with pytest.raises(controller.DeployError):
|
|
controller.mcp_hub_require_candidate_images(
|
|
prepared.project, prepared.model, prepared.revision
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("service", ["web", "contextforge"])
|
|
@pytest.mark.parametrize(
|
|
"label",
|
|
[
|
|
"org.opencontainers.image.source",
|
|
"org.opencontainers.image.revision",
|
|
"org.opencontainers.image.source-tree",
|
|
"org.opencontainers.image.source-date-epoch",
|
|
"org.opencontainers.image.created",
|
|
"org.opencontainers.image.version",
|
|
],
|
|
)
|
|
def test_frozen_image_provenance_is_checked_before_database_intent(
|
|
controller, prepared, service, label
|
|
):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.facts[prepared.candidate["images"][service]["imageRef"]]["Config"][
|
|
"Labels"
|
|
][label] = "wrong"
|
|
with pytest.raises(controller.DeployError, match="provenance"):
|
|
controller.mcp_hub_require_candidate_images(
|
|
prepared.project, prepared.model, prepared.revision
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("service", ["web", "contextforge"])
|
|
@pytest.mark.parametrize("user", ["", "root", "root:10001", "0", "00:10001"])
|
|
def test_frozen_image_root_user_is_rejected_before_database_intent(
|
|
controller, prepared, service, user
|
|
):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.facts[prepared.candidate["images"][service]["imageRef"]]["Config"][
|
|
"User"
|
|
] = user
|
|
with pytest.raises(controller.DeployError, match="non-root runtime"):
|
|
controller.mcp_hub_require_candidate_images(
|
|
prepared.project, prepared.model, prepared.revision
|
|
)
|
|
|
|
|
|
def test_changed_raw_scan_is_rejected_between_stages(controller, prepared):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.write("vulnerability-contextforge.json", {"Results": []})
|
|
with pytest.raises(controller.DeployError, match="hash"):
|
|
controller.mcp_hub_require_candidate_images(
|
|
prepared.project, prepared.model, prepared.revision
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("changed", [False, True])
|
|
def test_staged_start_consumes_no_build_no_pull_and_checks_running_ids(
|
|
controller, prepared, monkeypatch, changed
|
|
):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.project["_mcp_hub_rendered_model"] = prepared.model
|
|
commands = []
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"compose_command",
|
|
lambda _p: (prepared.directory, ["docker", "compose"]),
|
|
)
|
|
monkeypatch.setattr(controller, "deployment_environment", lambda *_args: {})
|
|
monkeypatch.setattr(
|
|
controller, "run", lambda command, **_kwargs: commands.append(command)
|
|
)
|
|
monkeypatch.setattr(controller, "mcp_hub_wait_services", lambda *_args: None)
|
|
expected = prepared.candidate["images"]["postgres"]["imageId"]
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"inspect_container",
|
|
lambda _name: {"Image": "sha256:" + "f" * 64 if changed else expected},
|
|
)
|
|
if changed:
|
|
with pytest.raises(controller.DeployError, match="exact scanned image"):
|
|
controller.mcp_hub_up_services(
|
|
prepared.project,
|
|
prepared.revision,
|
|
controller.MCP_HUB_REPOSITORY,
|
|
("postgres",),
|
|
)
|
|
else:
|
|
controller.mcp_hub_up_services(
|
|
prepared.project,
|
|
prepared.revision,
|
|
controller.MCP_HUB_REPOSITORY,
|
|
("postgres",),
|
|
)
|
|
assert commands == [
|
|
[
|
|
"docker",
|
|
"compose",
|
|
"up",
|
|
"--detach",
|
|
"--no-deps",
|
|
"--no-build",
|
|
"--pull",
|
|
"never",
|
|
"postgres",
|
|
]
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("operation", ["start", "migrate"])
|
|
def test_compose_operations_pin_all_scanned_ids_without_changing_source_model(
|
|
controller, prepared, monkeypatch, operation
|
|
):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.project["_mcp_hub_rendered_model"] = prepared.model
|
|
original = copy.deepcopy(prepared.model)
|
|
observed = []
|
|
|
|
def compose(project):
|
|
path = Path(project["_mcp_hub_image_override_file"])
|
|
assert path.stat().st_mode & 0o777 == 0o600
|
|
observed.append((path, json.loads(path.read_text())))
|
|
return prepared.directory, ["docker", "compose", "-f", str(path)]
|
|
|
|
commands = []
|
|
monkeypatch.setattr(controller, "compose_command", compose)
|
|
monkeypatch.setattr(controller, "deployment_environment", lambda *_args: {})
|
|
monkeypatch.setattr(controller, "run", lambda cmd, **_kwargs: commands.append(cmd))
|
|
monkeypatch.setattr(controller, "mcp_hub_wait_services", lambda *_args: None)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"inspect_container",
|
|
lambda _name: {"Image": prepared.candidate["images"]["postgres"]["imageId"]},
|
|
)
|
|
if operation == "start":
|
|
controller.mcp_hub_up_services(
|
|
prepared.project,
|
|
prepared.revision,
|
|
controller.MCP_HUB_REPOSITORY,
|
|
("postgres",),
|
|
)
|
|
else:
|
|
controller.mcp_hub_run_database_migration(
|
|
prepared.project, prepared.revision, controller.MCP_HUB_REPOSITORY
|
|
)
|
|
assert commands[0][-3:] == [
|
|
"-e",
|
|
"SERVICE_NAME=database-migrate",
|
|
"control-api",
|
|
]
|
|
assert len(observed) == len(commands) == 1
|
|
path, payload = observed[0]
|
|
assert payload == {
|
|
"services": {
|
|
service: {"image": record["imageId"]}
|
|
for service, record in prepared.candidate["images"].items()
|
|
}
|
|
}
|
|
assert "--no-deps" in commands[0]
|
|
assert commands[0][commands[0].index("--pull") + 1] == "never"
|
|
assert not path.exists()
|
|
assert "_mcp_hub_image_override_file" not in prepared.project
|
|
assert prepared.model == original
|
|
|
|
|
|
def test_migration_rechecks_candidate_before_running(controller, prepared, monkeypatch):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
prepared.project["_mcp_hub_rendered_model"] = prepared.model
|
|
prepared.write("vulnerability-control-api.json", {"Results": []})
|
|
monkeypatch.setattr(
|
|
controller, "run", lambda *_args, **_kwargs: pytest.fail("must not run")
|
|
)
|
|
with pytest.raises(controller.DeployError, match="hash"):
|
|
controller.mcp_hub_run_database_migration(
|
|
prepared.project, prepared.revision, controller.MCP_HUB_REPOSITORY
|
|
)
|
|
|
|
|
|
def test_candidate_runtime_verifies_pinned_ids_after_migration(
|
|
controller, prepared, monkeypatch
|
|
):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
project = prepared.project
|
|
project["compose_project"] = controller.MCP_HUB_PROJECT
|
|
project["slug"] = "itworx-mcp-hub"
|
|
project["source_dir"] = str(prepared.directory)
|
|
project["compose_files"] = []
|
|
project["containers"] = list(controller.MCP_HUB_CONTAINERS)
|
|
project["_mcp_hub_rendered_model"] = prepared.model
|
|
project["_mcp_hub_candidate_image_ids"] = (
|
|
controller.mcp_hub_require_candidate_images(
|
|
project, prepared.model, prepared.revision
|
|
)
|
|
)
|
|
for service in ("control-api", "worker"):
|
|
prepared.model["services"][service]["environment"] = {
|
|
"INGESTION_CONNECTOR_ENABLED": "true"
|
|
}
|
|
containers = {}
|
|
for service, record in prepared.candidate["images"].items():
|
|
name = f"itworx-mcp-hub-{service}-1"
|
|
containers[name] = {
|
|
"Id": "c" * 64,
|
|
"Image": record["imageId"],
|
|
"Config": {
|
|
"Image": record["imageId"],
|
|
"Env": (
|
|
["INGESTION_CONNECTOR_ENABLED=true"]
|
|
if service in {"control-api", "worker"}
|
|
else []
|
|
),
|
|
"Labels": {
|
|
"com.docker.compose.project": controller.MCP_HUB_PROJECT,
|
|
"com.docker.compose.service": service,
|
|
},
|
|
},
|
|
"State": {"Running": True, "Health": {"Status": "healthy"}},
|
|
}
|
|
monkeypatch.setattr(
|
|
controller, "compose_project_container_names", lambda _project: set(containers)
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "inspect_container", lambda name: containers.get(name)
|
|
)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"container_state",
|
|
lambda _name: (True, "healthy", controller.MCP_HUB_PROJECT),
|
|
)
|
|
labels = {
|
|
"org.opencontainers.image.revision": prepared.revision,
|
|
"org.opencontainers.image.source": controller.MCP_HUB_OCI_IMAGE_SOURCE,
|
|
"org.opencontainers.image.source-tree": prepared.metadata["tree"],
|
|
"org.opencontainers.image.version": prepared.metadata["id"],
|
|
"org.opencontainers.image.created": prepared.metadata["builtAt"],
|
|
}
|
|
image_ids_by_ref = {
|
|
record["imageRef"]: record["imageId"]
|
|
for record in prepared.candidate["images"].values()
|
|
}
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"inspect_image",
|
|
lambda image_ref: {
|
|
"Id": image_ids_by_ref.get(image_ref, image_ref),
|
|
"Config": {"Labels": labels},
|
|
"RepoDigests": [],
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "mcp_hub_attested_live_mounts", lambda *_args, **_kwargs: []
|
|
)
|
|
assert set(controller.verify_mcp_hub_live_topology(project, prepared.model)) == set(
|
|
containers
|
|
)
|
|
assert set(
|
|
controller.verify_containers(
|
|
project, expected_image_ids=project["_mcp_hub_candidate_image_ids"]
|
|
)
|
|
) == set(containers)
|
|
for service in controller.MCP_HUB_APPLICATION_SERVICES:
|
|
prepared.model["services"][service]["build"] = {"context": "."}
|
|
manifest = (
|
|
"CONTEXTFORGE_UPSTREAM_COMMIT=" + "1" * 40 + "\n"
|
|
"CONTEXTFORGE_ALEMBIC_HEAD=12d4a0c7789c\n"
|
|
"CONTEXTFORGE_MCP_SDK=1.30.0\n"
|
|
"CONTEXTFORGE_LOCAL_IMAGE=itworx-mcp-hub/contextforge-schema:12d4a0c7789c\n"
|
|
)
|
|
builder = "\n".join(
|
|
[
|
|
"--release-candidate",
|
|
'CANDIDATE_IMAGE="itworx-mcp-hub/contextforge-schema:${APPOPS_SOURCE_REVISION}"',
|
|
'docker tag "$STAGED_IMAGE" "$CANDIDATE_IMAGE"',
|
|
'git -C "$SRC" fetch --depth 1 origin "$CONTEXTFORGE_UPSTREAM_COMMIT"',
|
|
'git -C "$SRC" checkout --detach FETCH_HEAD',
|
|
"alembic -c mcpgateway/alembic.ini heads",
|
|
'docker tag "$STAGED_IMAGE" "$CONTEXTFORGE_LOCAL_IMAGE"',
|
|
]
|
|
)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"_mcp_hub_git_output",
|
|
lambda _project, _show, path: manifest
|
|
if path.endswith("compat-image.env")
|
|
else builder,
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "_mcp_hub_dataplane_compat_proven", lambda *_args: True
|
|
)
|
|
monkeypatch.setattr(controller, "_mcp_hub_digest_ref_proven", lambda *_args: True)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"_mcp_hub_live_image_proven",
|
|
lambda _project, name, item, source_ref, *_args, **_kwargs: {
|
|
"container_id": item["Id"],
|
|
"image_id": item["Image"],
|
|
"image_ref": source_ref,
|
|
"container_revision": "",
|
|
"image_revision": prepared.revision,
|
|
"image_source": controller.MCP_HUB_OCI_IMAGE_SOURCE,
|
|
"image_tree": prepared.metadata["tree"],
|
|
"image_version": prepared.metadata["id"],
|
|
"image_created": prepared.metadata["builtAt"],
|
|
},
|
|
)
|
|
receipt = controller.verify_deployment(
|
|
project,
|
|
prepared.metadata,
|
|
expected_image_ids=project["_mcp_hub_candidate_image_ids"],
|
|
)
|
|
for service in controller.MCP_HUB_APPLICATION_SERVICES:
|
|
name = f"itworx-mcp-hub-{service}-1"
|
|
assert (
|
|
receipt["containers"][name]["image_ref"]
|
|
== receipt["containers"][name]["image_id"]
|
|
)
|
|
assert (
|
|
controller.validate_recorded_receipt(
|
|
project, receipt, controller.MCP_HUB_REPOSITORY, prepared.revision
|
|
)
|
|
== prepared.metadata
|
|
)
|
|
state = {}
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"_mcp_hub_read_controller_state_bytes",
|
|
lambda **_kwargs: state.get("bytes"),
|
|
)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"_mcp_hub_write_controller_state_bytes",
|
|
lambda payload, **_kwargs: state.__setitem__("bytes", payload),
|
|
)
|
|
controller.write_state(
|
|
controller.MCP_HUB_REPOSITORY,
|
|
project,
|
|
prepared.revision,
|
|
"success",
|
|
"verified",
|
|
live_receipt=receipt,
|
|
)
|
|
saved = json.loads(state["bytes"])
|
|
assert saved["live_status"] == "healthy"
|
|
for name in controller.MCP_HUB_CONTAINERS:
|
|
assert (
|
|
saved["live_artifacts"]["containers"][name]["image_ref"]
|
|
== receipt["containers"][name]["image_ref"]
|
|
)
|
|
project.pop("_mcp_hub_candidate_image_ids")
|
|
assert (
|
|
controller.verified_current_receipt(
|
|
controller.MCP_HUB_REPOSITORY,
|
|
project,
|
|
prepared.revision,
|
|
prepared.metadata["tree"],
|
|
)
|
|
== receipt
|
|
)
|
|
assert "_mcp_hub_receipt_image_ids" not in project
|
|
project["_repository_name"] = controller.MCP_HUB_REPOSITORY
|
|
rollback_commands = []
|
|
monkeypatch.setattr(
|
|
controller, "run", lambda command, **_kwargs: rollback_commands.append(command)
|
|
)
|
|
snapshot = controller.capture_compose_rollback(project)
|
|
assert len(snapshot["containers"]) == len(rollback_commands) == 17
|
|
for item in snapshot["containers"]:
|
|
assert item["image_ref"] == receipt["containers"][item["name"]]["image_ref"]
|
|
cf = prepared.candidate["images"]["contextforge"]
|
|
assert not controller._mcp_hub_contextforge_compat_proven(
|
|
project, prepared.revision, cf["imageRef"], "sha256:" + "f" * 64
|
|
)
|
|
containers["itworx-mcp-hub-web-1"]["Config"]["Image"] = "web:mutable"
|
|
with pytest.raises(controller.DeployError, match="identity or health"):
|
|
controller.verify_mcp_hub_live_topology(project, prepared.model)
|
|
|
|
|
|
@pytest.mark.parametrize("same_revision", [True, False])
|
|
def test_same_revision_receipt_prepares_exact_source_before_verification(
|
|
controller, prepared, monkeypatch, same_revision
|
|
):
|
|
project = prepared.project
|
|
project.update({"source_dir": str(prepared.directory), "slug": "itworx-mcp-hub"})
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"read_state",
|
|
lambda _project: {
|
|
"live_revision": prepared.revision if same_revision else "b" * 40
|
|
},
|
|
)
|
|
events = []
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"sync_source",
|
|
lambda *_args, **kwargs: events.append(("sync", kwargs["exact"])),
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "copy_environment", lambda *_args: events.append("environment")
|
|
)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"mcp_hub_compose_model",
|
|
lambda *_args: events.append("model") or prepared.model,
|
|
)
|
|
controller.mcp_hub_prepare_current_receipt_model(
|
|
prepared.directory, project, controller.MCP_HUB_REPOSITORY, prepared.revision
|
|
)
|
|
assert events == ([("sync", True), "environment", "model"] if same_revision else [])
|
|
assert (project.get("_mcp_hub_rendered_model") is prepared.model) is same_revision
|
|
|
|
|
|
@pytest.mark.parametrize("mutation", ["missing", "extra", "tag", "nonmapping"])
|
|
def test_candidate_compose_override_rejects_malformed_ids(
|
|
controller, prepared, mutation
|
|
):
|
|
ids = {
|
|
f"itworx-mcp-hub-{service}-1": record["imageId"]
|
|
for service, record in prepared.candidate["images"].items()
|
|
}
|
|
if mutation == "missing":
|
|
ids.pop("itworx-mcp-hub-web-1")
|
|
elif mutation == "extra":
|
|
ids["foreign"] = "sha256:" + "a" * 64
|
|
elif mutation == "tag":
|
|
ids["itworx-mcp-hub-web-1"] = "web:latest"
|
|
else:
|
|
ids = []
|
|
prepared.project["_mcp_hub_rendered_model"] = prepared.model
|
|
prepared.project["_mcp_hub_candidate_image_ids"] = ids
|
|
with pytest.raises(controller.DeployError, match="candidate image IDs"):
|
|
with controller._mcp_hub_compose_image_override(prepared.project):
|
|
pytest.fail("must reject invalid image IDs")
|
|
|
|
|
|
def test_actual_high_findings_are_bound_to_current_service_tags(controller, prepared):
|
|
report = json.loads((prepared.directory / "vulnerability-web.json").read_text())
|
|
report["Results"][0]["Vulnerabilities"] = [
|
|
{
|
|
"PkgName": "fixture",
|
|
"VulnerabilityID": "CVE-2026-12345",
|
|
"InstalledVersion": "1",
|
|
"Severity": "HIGH",
|
|
}
|
|
]
|
|
prepared.candidate["images"]["web"]["vulnerabilitySha256"] = prepared.write(
|
|
"vulnerability-web.json", report
|
|
)
|
|
summary = {
|
|
"unresolvedCriticalOrHigh": [
|
|
{
|
|
"package": "fixture",
|
|
"cves": ["CVE-2026-12345"],
|
|
"installedVersion": "1",
|
|
"severity": "HIGH",
|
|
"affectedImages": [prepared.candidate["images"]["web"]["imageRef"]],
|
|
}
|
|
]
|
|
}
|
|
prepared.candidate["summarySha256"] = prepared.write("scan-summary.json", summary)
|
|
candidate_sha = prepared.write("candidate.json", prepared.candidate)
|
|
prepared.write(
|
|
"approval.json",
|
|
{
|
|
"schema": "itworx.mcp-hub-cve-approval/v1",
|
|
"candidateSha256": candidate_sha,
|
|
"exceptionsSha256": hashlib.sha256(
|
|
(prepared.directory / "exceptions.json").read_bytes()
|
|
).hexdigest(),
|
|
},
|
|
)
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
assert prepared.checks == ["gate"]
|
|
|
|
|
|
def test_fixable_high_findings_cannot_be_approved_around(controller, prepared):
|
|
report = json.loads((prepared.directory / "vulnerability-web.json").read_text())
|
|
report["Results"][0]["Vulnerabilities"] = [
|
|
{
|
|
"PkgName": "fixture",
|
|
"VulnerabilityID": "CVE-2026-12345",
|
|
"InstalledVersion": "1",
|
|
"FixedVersion": "2",
|
|
"Severity": "HIGH",
|
|
}
|
|
]
|
|
prepared.candidate["images"]["web"]["vulnerabilitySha256"] = prepared.write(
|
|
"vulnerability-web.json", report
|
|
)
|
|
candidate_sha = prepared.write("candidate.json", prepared.candidate)
|
|
prepared.write(
|
|
"approval.json",
|
|
{
|
|
"schema": "itworx.mcp-hub-cve-approval/v1",
|
|
"candidateSha256": candidate_sha,
|
|
"exceptionsSha256": hashlib.sha256(
|
|
(prepared.directory / "exceptions.json").read_bytes()
|
|
).hexdigest(),
|
|
},
|
|
)
|
|
with pytest.raises(controller.DeployError, match="fixable critical/high"):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
assert prepared.checks == []
|
|
|
|
|
|
def test_missing_normal_upgrade_volume_baseline_denies_before_intent_or_start(
|
|
controller, prepared, monkeypatch
|
|
):
|
|
controller.mcp_hub_load_release_candidate(prepared.project, prepared.revision)
|
|
monkeypatch.setattr(controller, "mcp_hub_require_docker_capacity", lambda: None)
|
|
monkeypatch.setattr(
|
|
controller, "mcp_hub_stage_release_source", lambda *_args: prepared.directory
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "mcp_hub_compose_model", lambda *_args: prepared.model
|
|
)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"compose_command",
|
|
lambda *_args: (prepared.directory, ["docker", "compose"]),
|
|
)
|
|
monkeypatch.setattr(controller, "deployment_environment", lambda *_args: {})
|
|
monkeypatch.setattr(
|
|
controller.subprocess,
|
|
"run",
|
|
lambda *_args, **_kwargs: types.SimpleNamespace(returncode=0),
|
|
)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"_mcp_hub_begin_migration_intent",
|
|
lambda *_args: pytest.fail(
|
|
"missing volume identity must precede durable intent"
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"mcp_hub_up_services",
|
|
lambda *_args: pytest.fail(
|
|
"missing volume identity must precede Compose mutation"
|
|
),
|
|
)
|
|
with pytest.raises(controller.DeployError, match="volume baseline"):
|
|
controller.deploy_mcp_hub_staged(
|
|
prepared.project, prepared.revision, controller.MCP_HUB_REPOSITORY
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mode, candidate_epoch",
|
|
[("deploy", "1790888863"), ("deploy", "1"), ("preflight", "1790888863")],
|
|
)
|
|
def test_main_hub_path_sets_validated_epoch_before_compose_render(
|
|
controller, prepared, monkeypatch, mode, candidate_epoch
|
|
):
|
|
class ReachedValidatedRender(Exception):
|
|
pass
|
|
|
|
target = {
|
|
**prepared.project,
|
|
"branch": "main",
|
|
"slug": controller.MCP_HUB_PROJECT,
|
|
"source_dir": str(prepared.directory),
|
|
}
|
|
target["compose_project"] = controller.MCP_HUB_PROJECT
|
|
target.pop("_mcp_hub_source_date_epoch", None)
|
|
monkeypatch.setattr(
|
|
controller, "load_projects", lambda: {controller.MCP_HUB_REPOSITORY: target}
|
|
)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"effective_mcp_hub_project",
|
|
lambda _repository, project: dict(project),
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "repository_path", lambda *_args: prepared.directory
|
|
)
|
|
monkeypatch.setattr(controller, "validate_commit", lambda *_args: prepared.revision)
|
|
candidate = copy.deepcopy(prepared.candidate)
|
|
candidate["sourceDateEpoch"] = candidate_epoch
|
|
monkeypatch.setattr(
|
|
controller, "mcp_hub_load_release_candidate", lambda *_args: candidate
|
|
)
|
|
monkeypatch.setenv("SOURCE_DATE_EPOCH", "untrusted-environment-override")
|
|
|
|
def render(_repo, project, revision):
|
|
if mode == "deploy" and candidate_epoch != "1790888863":
|
|
pytest.fail(
|
|
"mismatched candidate epoch must deny before Compose validation"
|
|
)
|
|
assert project["_mcp_hub_source_date_epoch"] == "1790888863"
|
|
assert (
|
|
controller.deployment_environment(
|
|
project, revision, controller.MCP_HUB_REPOSITORY
|
|
)["SOURCE_DATE_EPOCH"]
|
|
== "1790888863"
|
|
)
|
|
raise ReachedValidatedRender()
|
|
|
|
monkeypatch.setattr(controller, "validate_project_config", render)
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
["deploy.py", mode, controller.MCP_HUB_REPOSITORY, prepared.revision],
|
|
)
|
|
if mode == "deploy" and candidate_epoch != "1790888863":
|
|
with pytest.raises(controller.DeployError, match="epoch"):
|
|
controller.main()
|
|
else:
|
|
with pytest.raises(ReachedValidatedRender):
|
|
controller.main()
|