Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ main and differ from the producer only in the closed qualification-source set.
The existing v1 model still requires runner and producer to be the same commit.
V2 receipts retain both identities, each actual dependency image, the owned
resource scope, body observations and verified cleanup. The four Compose cells
retain engine target IDs, platform manifests and config digests distinctly, and
run on amd64 hosts; their arm64 server cells use the pinned QEMU helper. These
receipts do not select alias promotion or a GitHub Release.

Expand Down
4 changes: 2 additions & 2 deletions scripts/ci/check-docker-workflow-isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
# checks/CLI transport is bound to reviewed bytes. A source change requires an
# explicit model update and countercontrol review before gaining coverage.
QUALIFICATION_CLEANUP_SOURCES = {
"scripts/ci/native_qualification.py": "2b7309458989723e0635549a8a1c176a18297d4d057eda612474e10e311c5980",
"scripts/ci/native_alias_evidence.py": "38241ccbb0b20a7566a43df3fec2ef0afffa0e33bb7c442f4bdce39b92fe2673",
"scripts/ci/native_qualification.py": "65916e73b7f0c6431c612f411e27f9429a2c37dbab287d1f17b840dee0f8d185",
"scripts/ci/native_alias_evidence.py": "b39aa4c96b7b5642b4fbb800dad0d02f0a393f7638a95220dc0c439a939ad200",
"scripts/ci/native_alias_transport.py": "053af14689c7c69d6620860390801fc900a331126f32a4f68fac6eb0e6593fef",
}

Expand Down
19 changes: 17 additions & 2 deletions scripts/ci/native_alias_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
RECEIPT_SCHEMA = "durable-workflow.native-qualification-receipt.v1"
MANIFEST_SCHEMA_V2 = "durable-workflow.native-alias-promotion-evidence.v2"
RECEIPT_SCHEMA_V2 = "durable-workflow.native-qualification-receipt.v2"
LOCAL_IMAGE_KEYS = {"local_image_id", "local_manifest_digest", "config_digest_source"}
SUBJECT_PATH = "scripts/ci/native-qualification-subject.json"
RUNNER_REF = "refs/heads/feat/server-native32-qualification-runner"
RUNNER_PATHS = {
Expand Down Expand Up @@ -459,7 +460,7 @@ def bind_receipt(value: dict[str, Any], role: str, selected: dict[str, Any],
run: dict[str, Any], job: dict[str, Any], release: dict[str, Any],
platform: str, profile: str | None, subject: dict[str, Any],
runner: dict[str,str] | None = None, binding: dict[str,Any] | None = None) -> None:
extra = {"runner_ref", "subject_blob", "resource_scope", "dependencies"} if runner else set()
extra = {"runner_ref", "subject_blob", "resource_scope", "dependencies"} | LOCAL_IMAGE_KEYS if runner else set()
value = closed(value, RECEIPT_KEYS | extra, "receipt_shape")
require(value["schema"] == (RECEIPT_SCHEMA_V2 if runner else RECEIPT_SCHEMA) and value["kind"] == role
and value["eligible"] is True, "receipt_not_eligible")
Expand All @@ -484,6 +485,7 @@ def bind_receipt(value: dict[str, Any], role: str, selected: dict[str, Any],
require(binding is not None and binding["release"] == release, "receipt_binding_missing")
observed_scope = resource_scope(role, run["id"], run["run_attempt"], platform, profile)
require(value["resource_scope"] == observed_scope, "receipt_resource_scope")
bind_local_image(value, subject, release["index_digest"])
bind_dependencies(value["dependencies"], role, platform, binding)
cleanup = closed(value["cleanup"],
{"api_ok", "verified_absent", "container_count", "network_count", "volume_count"},
Expand All @@ -508,6 +510,18 @@ def resource_scope(role: str, run: int, attempt: int, platform: str, profile: st
f"published-compose-{run}-{attempt}-smoke-{platform.split('/')[1]}-{profile}")


def bind_local_image(value: dict[str, Any], subject: dict[str, Any], root_digest: str) -> None:
require(matching(value["local_image_id"], DIGEST), "receipt_local_image_identity")
if value["config_digest_source"] == "engine_id":
require(value["local_image_id"] == subject["image_config_digest"]
and value["local_manifest_digest"] is None, "receipt_local_image_identity")
else:
require(value["config_digest_source"] == "manifest_descriptor"
and value["local_image_id"] in {root_digest, subject["child_manifest_digest"]}
and value["local_manifest_digest"] == subject["child_manifest_digest"],
"receipt_local_image_identity")


def bind_dependencies(value: Any, role: str, platform: str, subject: dict[str,Any]) -> None:
if role == "corpus":
closed(value, set(), "receipt_dependency_shape")
Expand All @@ -519,11 +533,12 @@ def bind_dependencies(value: Any, role: str, platform: str, subject: dict[str,An
continue
keys = {"requested_image", "child_manifest_digest", "image_config_digest",
"actual_image_config_digest", "container_image_architecture", "container_image_os"}
observed = closed(value[name], keys | ({"container_id"} if name == "mysql" else set()),
observed = closed(value[name], keys | LOCAL_IMAGE_KEYS | ({"container_id"} if name == "mysql" else set()),
"receipt_dependency_identity")
expected = {**subject[name], "actual_image_config_digest": subject[name]["image_config_digest"],
"container_image_architecture": "amd64", "container_image_os": "linux"}
require(all(literal(observed[key], item) for key,item in expected.items()), "receipt_dependency_binding")
bind_local_image(observed, subject[name], subject[name]["child_manifest_digest"])
if name == "mysql":
require(type(observed["container_id"]) is str and re.fullmatch(r"[0-9a-f]{64}", observed["container_id"]),
"receipt_database_container")
Expand Down
131 changes: 109 additions & 22 deletions scripts/ci/native_qualification.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
DIGEST, FINAL_TAG, GHCR_IMAGE, JSON_LIMIT, PLATFORMS,
RECEIPT_SCHEMA, RECEIPT_SCHEMA_V2, REPOSITORY, REPOSITORY_ID, SHA, WORKFLOWS,
SUBJECT_PATH, RUNNER_REF, qualification_subject, runner_subject,
resource_scope, bind_dependencies,
resource_scope, bind_dependencies, LOCAL_IMAGE_KEYS,
Refusal, canonical, closed, literal, matching, positive,
require, strict_json)
from native_alias_transport import Commands, GitHubRegistryProvider
Expand Down Expand Up @@ -100,43 +100,125 @@ def admit(provider: GitHubRegistryProvider, role: str, image: str) -> dict[str,
return {"runner": runner, "subject": subject}


def dependency_image(commands: Commands, subject: dict[str,str], repositories: tuple[str,...]) -> dict[str,Any]:
config = inspect(commands, "image", subject["image_config_digest"])
require(config.get("Id") == subject["image_config_digest"] and config.get("Architecture") == "amd64"
and config.get("Os") == "linux" and type(config.get("RepoDigests")) is list
MANIFEST_TYPES = {"application/vnd.oci.image.manifest.v1+json",
"application/vnd.docker.distribution.manifest.v2+json"}
DEPENDENCY_REPOSITORIES = {
"mysql": ("mysql:8.0", ("mysql", "docker.io/library/mysql")),
"qemu": ("docker.io/tonistiigi/binfmt", ("tonistiigi/binfmt", "docker.io/tonistiigi/binfmt")),
}


def require_manifest(value: Any, digest: str) -> None:
require(type(value) is dict and value.get("digest") == digest
and type(value.get("mediaType")) is str and value["mediaType"] in MANIFEST_TYPES,
"actual_platform_manifest")


def image_manifest(provider: GitHubRegistryProvider, subject: dict[str, str], requested: str) -> Any:
repository = requested.split("@", 1)[0]
if repository == GHCR_IMAGE:
return provider.registry_json(repository, subject["child_manifest_digest"])
# The shared promotion transport remains Server-only. These two exact
# qualification dependencies use an independently closed read-only path.
require(repository in {item[0] for item in DEPENDENCY_REPOSITORIES.values()}
and matching(subject.get("child_manifest_digest"), DIGEST)
and requested == repository + "@" + subject["child_manifest_digest"], "dependency_registry_subject")
return strict_json(provider.commands.run(
["docker", "buildx", "imagetools", "inspect", "--raw", requested]), JSON_LIMIT)


def local_image(provider: GitHubRegistryProvider, subject: dict[str, str], requested: str,
platform: str, root_digest: str, container: dict[str, Any] | None = None
) -> tuple[dict[str, Any], dict[str, Any]]:
"""Keep the engine target ID distinct from its manifest's config digest."""
commands = provider.commands
if container is None:
config = inspect(commands, "image", requested)
local_id = config.get("Id")
else:
local_id = container.get("Image")
require(matching(local_id, DIGEST), "actual_container_image")
require(local_id in {subject["image_config_digest"], root_digest, subject["child_manifest_digest"]},
"actual_container_image")
if local_id == subject["image_config_digest"]:
config = inspect(commands, "image", local_id)
else:
require_manifest(container.get("ImageManifestDescriptor"), subject["child_manifest_digest"])
config = inspect(commands, "image", local_id, platform=platform)
require(matching(local_id, DIGEST), "actual_local_image")
if local_id == subject["image_config_digest"]:
require(config.get("Id") == local_id, "actual_local_image")
identity = {"local_image_id": local_id, "local_manifest_digest": None,
"config_digest_source": "engine_id"}
else:
require(local_id in {root_digest, subject["child_manifest_digest"]}
and config.get("Id") == subject["child_manifest_digest"], "actual_local_image")
require_manifest(config.get("Descriptor"), subject["child_manifest_digest"])
# The platform descriptor is engine-observed. Its immutable manifest
# supplies the config linkage; an engine target ID is never relabeled.
manifest = image_manifest(provider, subject, requested)
require(type(manifest) is dict and manifest.get("schemaVersion") == 2
and type(manifest.get("mediaType")) is str and manifest["mediaType"] in MANIFEST_TYPES
and type(manifest.get("config")) is dict
and manifest["config"].get("digest") == subject["image_config_digest"],
"actual_manifest_config")
identity = {"local_image_id": local_id, "local_manifest_digest": subject["child_manifest_digest"],
"config_digest_source": "manifest_descriptor"}
require(config.get("Architecture") == platform.split("/")[1] and config.get("Os") == "linux",
"actual_container_platform")
return config, identity


def dependency_image(provider: GitHubRegistryProvider, subject: dict[str,str], role: str,
container: dict[str, Any] | None = None) -> dict[str,Any]:
require(type(role) is str and role in DEPENDENCY_REPOSITORIES, "dependency_registry_subject")
repository, repositories = DEPENDENCY_REPOSITORIES[role]
require(matching(subject.get("child_manifest_digest"), DIGEST)
and subject.get("requested_image") == repository + "@" + subject["child_manifest_digest"],
"dependency_registry_subject")
config, identity = local_image(provider, subject, subject["requested_image"], "linux/amd64",
subject["child_manifest_digest"], container)
require(type(config.get("RepoDigests")) is list
and any(repository + "@" + subject["child_manifest_digest"] in config["RepoDigests"]
for repository in repositories), "actual_dependency_image")
return {**subject, "actual_image_config_digest": config["Id"],
return {**subject, **identity, "actual_image_config_digest": subject["image_config_digest"],
"container_image_architecture": config["Architecture"], "container_image_os": config["Os"]}


def observe_dependencies(commands: Commands, role: str, platform: str, scope: str, subject: dict[str,Any]) -> dict[str,Any]:
def observe_dependencies(provider: GitHubRegistryProvider, role: str, platform: str, scope: str, subject: dict[str,Any]) -> dict[str,Any]:
if role == "corpus":
return {}
commands = provider.commands
containers = lines(commands, ["container", "ls", "-aq", "--no-trunc", "--filter",
LABEL_FILTER + COMPOSE_OWNER + "=" + scope, "--filter", "label=com.docker.compose.service=mysql"], OBJECT_ID)
require(len(containers) == 1, "actual_database_inventory")
identity = containers.pop()
database = inspect(commands, "container", identity)
labels = database.get("Config", {}).get("Labels", {})
require(database.get("Id") == identity and database.get("Image") == subject["mysql"]["image_config_digest"]
require(database.get("Id") == identity
and matching(database.get("Image"), DIGEST)
and database["Image"] in {subject["mysql"]["image_config_digest"], subject["mysql"]["child_manifest_digest"]}
and database.get("Config", {}).get("Image") == subject["mysql"]["requested_image"]
and labels.get(COMPOSE_OWNER) == scope and labels.get("com.docker.compose.service") == "mysql",
"actual_database_subject")
mysql = {**dependency_image(commands, subject["mysql"], ("mysql", "docker.io/library/mysql")),
mysql = {**dependency_image(provider, subject["mysql"], "mysql", database),
"container_id": identity}
qemu = (dependency_image(commands, subject["qemu"], ("tonistiigi/binfmt", "docker.io/tonistiigi/binfmt"))
qemu = (dependency_image(provider, subject["qemu"], "qemu")
if platform == "linux/arm64" else None)
return {"mysql": mysql, "qemu": qemu}


def inspect(commands: Commands, resource: str, identity: str) -> dict[str, Any]:
def inspect(commands: Commands, resource: str, identity: str, *, platform: str | None = None) -> dict[str, Any]:
if resource not in ("container", "image", "network", "volume"):
raise Refusal("docker_resource_kind")
# Leading CLI options, whitespace, separators and paths are never identities.
if not re.fullmatch(r"(?:sha256:[0-9a-f]{64}|[A-Za-z0-9][A-Za-z0-9_.-]{0,180})", identity):
pinned_image = (resource == "image" and re.fullmatch(
r"(?:ghcr\.io/durable-workflow/server|mysql:8\.0|docker\.io/tonistiigi/binfmt)@sha256:[0-9a-f]{64}", identity))
if not pinned_image and not re.fullmatch(r"(?:sha256:[0-9a-f]{64}|[A-Za-z0-9][A-Za-z0-9_.-]{0,180})", identity):
raise Refusal("docker_resource_identity")
value = strict_json(commands.run(["docker", resource, "inspect", identity]), JSON_LIMIT)
require(platform is None or resource == "image" and platform in PLATFORMS, "docker_inspect_platform")
args = ["docker", resource, "inspect"] + (["--platform", platform] if platform else []) + [identity]
value = strict_json(commands.run(args), JSON_LIMIT)
require(type(value) is list and len(value) == 1 and type(value[0]) is dict,
"docker_inspect_shape")
return value[0]
Expand All @@ -156,12 +238,16 @@ def observe_image(provider: GitHubRegistryProvider, role: str, image: str,
index = image.split("@", 1)[1]
subject = provider.index_subjects(GHCR_IMAGE, index, source, tag)[platform]
instance = inspect(provider.commands, "container", container)
require(instance.get("Image") == subject["image_config_digest"], "actual_container_image")
config = inspect(provider.commands, "image", subject["image_config_digest"])
require(config.get("Id") == subject["image_config_digest"]
and config.get("Architecture") == platform.split("/")[1]
and config.get("Os") == "linux" and type(config.get("Config")) is dict,
"actual_container_platform")
local = {}
if admission:
config, local = local_image(provider, subject, image, platform, index, instance)
else:
require(instance.get("Image") == subject["image_config_digest"], "actual_container_image")
config = inspect(provider.commands, "image", subject["image_config_digest"])
require(config.get("Id") == subject["image_config_digest"]
and config.get("Architecture") == platform.split("/")[1]
and config.get("Os") == "linux", "actual_container_platform")
require(type(config.get("Config")) is dict, "actual_container_platform")
labels = config["Config"].get("Labels")
require(type(labels) is dict and labels.get("org.opencontainers.image.revision") == source
and labels.get("dev.durable-workflow.release.tag") == tag, "actual_container_source")
Expand All @@ -172,9 +258,10 @@ def observe_image(provider: GitHubRegistryProvider, role: str, image: str,
require(os.environ.get("DW_PUBLISHED_COMPOSE_PROJECT" if role == "compose" else "RESOURCE_SCOPE") == observed_scope,
"qualification_resource_scope")
extra = {"runner_ref": admission["runner"]["ref"], "subject_blob": admission["runner"]["subject_blob"],
"resource_scope": observed_scope, "dependencies": observe_dependencies(provider.commands, role, platform, observed_scope, admission["subject"])}
"resource_scope": observed_scope, **local,
"dependencies": observe_dependencies(provider, role, platform, observed_scope, admission["subject"])}
return {"eligible": True, "requested_image": image, "index_digest": index,
"platform": platform, **subject, "actual_image_config_digest": instance["Image"],
"platform": platform, **subject, "actual_image_config_digest": subject["image_config_digest"],
"container_image_architecture": config["Architecture"], "container_image_os": config["Os"],
"image_source_commit": source, "image_release_tag": tag, **extra}

Expand Down Expand Up @@ -330,7 +417,7 @@ def finalize(provider: GitHubRegistryProvider, role: str, image: str, platform:
"actual_image_config_digest", "container_image_architecture", "container_image_os",
"image_source_commit", "image_release_tag"}
if admission:
image_keys |= {"runner_ref", "subject_blob", "resource_scope", "dependencies"}
image_keys |= {"runner_ref", "subject_blob", "resource_scope", "dependencies"} | LOCAL_IMAGE_KEYS
require(identity.get("runner_ref") == admission["runner"]["ref"]
and identity.get("subject_blob") == admission["runner"]["subject_blob"]
and identity.get("resource_scope") == resource_scope(role,run_id,attempt,platform,profile),
Expand Down
Loading
Loading