From 04c4c20bb682bc6be0ef489a1c5da49eef7c851f Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Fri, 28 Aug 2026 13:35:24 +0200 Subject: [PATCH 1/7] chore: bump version to 0.7.110 --- README.md | 4 ++-- pyproject.toml | 2 +- src/iicp_client/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0ef2d78..8ea2182 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ What good looks like: ```bash iicp-node --help # shows query, serve, proxy, mcp-gateway, credits, ... which iicp-node # points to your Python environment -iicp-node --version # prints iicp-node 0.7.109 or newer +iicp-node --version # prints iicp-node 0.7.110 or newer ``` The query command contacts the public directory, discovers a matching live node, @@ -213,7 +213,7 @@ base URL. Full guide: ## Keep provider nodes current -The current public release line is **0.7.109**. Upgrade through your package +The current public release line is **0.7.110**. Upgrade through your package manager before troubleshooting an older installation. Routing profiles can refuse remote dispatch before a prompt leaves the client; use `sensitive` for local-only work, `eu-restricted` for EU/EEA routing, or `strict-policy` when a diff --git a/pyproject.toml b/pyproject.toml index 6036103..1aad303 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "iicp-client" -version = "0.7.109" +version = "0.7.110" description = "Use the open IICP AI mesh from Python without running a node" readme = "README.md" license = {text = "Apache-2.0"} diff --git a/src/iicp_client/__init__.py b/src/iicp_client/__init__.py index d7f366e..b913127 100644 --- a/src/iicp_client/__init__.py +++ b/src/iicp_client/__init__.py @@ -149,7 +149,7 @@ TaskResponse, ) -__version__ = "0.7.109" +__version__ = "0.7.110" __all__ = [ "IicpClient", "IicpError", diff --git a/uv.lock b/uv.lock index ffad58b..178b927 100644 --- a/uv.lock +++ b/uv.lock @@ -664,7 +664,7 @@ wheels = [ [[package]] name = "iicp-client" -version = "0.7.109" +version = "0.7.110" source = { editable = "." } dependencies = [ { name = "cryptography" }, From 9241a54ddfc1104727aec5c5bd0229ae3d844bc1 Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Fri, 28 Aug 2026 13:51:19 +0200 Subject: [PATCH 2/7] qualification: align Python support and build evidence --- CHANGELOG.md | 10 ++ pyproject.toml | 4 +- scripts/build_pre1_candidate_artifacts.py | 204 ++++++++++++++++++++++ scripts/pre1_artifact_common.py | 198 +++++++++++++++++++++ scripts/test_pre1_candidate_artifacts.py | 28 +++ uv.lock | 41 +---- 6 files changed, 444 insertions(+), 41 deletions(-) create mode 100644 scripts/build_pre1_candidate_artifacts.py create mode 100755 scripts/pre1_artifact_common.py create mode 100644 scripts/test_pre1_candidate_artifacts.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 123be50..f873085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ in the main repo). ## [Unreleased] +## [0.7.110] — 2026-08-28 + +### Changed — explicit qualification boundary + +- Bound package metadata to the fully declared CPython 3.11 through 3.14 + qualification range. This does not change runtime or wire behavior. +- Add a component-owned candidate builder that proves locked build, exact + online installation, empty-environment offline installation and CLI version + truth before emitting content-free artifact evidence. + ## [0.7.109] — 2026-08-25 ### Added — shell completion diff --git a/pyproject.toml b/pyproject.toml index 1aad303..23a6baa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.7.110" description = "Use the open IICP AI mesh from Python without running a node" readme = "README.md" license = {text = "Apache-2.0"} -requires-python = ">=3.11" +requires-python = ">=3.11,<3.15" authors = [ {name = "IICP Contributors"}, ] @@ -20,6 +20,8 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules", ] diff --git a/scripts/build_pre1_candidate_artifacts.py b/scripts/build_pre1_candidate_artifacts.py new file mode 100644 index 0000000..91521b2 --- /dev/null +++ b/scripts/build_pre1_candidate_artifacts.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Build and prove the Python pre-stable wheel/sdist artifact fragment.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import sys +import tempfile +import tomllib +from pathlib import Path + +import pre1_artifact_common as common + + +ROOT = Path(__file__).resolve().parents[1] +COMPONENT = "client-python" +TARGETS = { + "linux-x86_64", + "linux-aarch64", + "macos-x86_64", + "macos-arm64", + "windows-x86_64", +} + + +def describe() -> dict: + return { + "schema": "iicp.pre1-artifact-builder-description.v1", + "component": COMPONENT, + "artifact_identities": [["wheel", "any"], ["sdist", "any"]], + "gates": sorted(common.GATES), + "requires_clean_source": True, + "non_authorizing": True, + } + + +def venv_python(root: Path) -> Path: + return root / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") + + +def venv_cli(root: Path) -> Path: + return root / ("Scripts/iicp-node.exe" if sys.platform == "win32" else "bin/iicp-node") + + +def build(destination: Path, requested_target: str | None) -> dict: + common.safe_output(destination) + target = common.require_target(requested_target, TARGETS) + commit = common.require_clean_source(ROOT) + manifest = tomllib.loads((ROOT / "pyproject.toml").read_text()) + version = manifest["project"]["version"] + requires = manifest["project"]["requires-python"] + if requires != ">=3.11,<3.15": + raise ValueError("Python package support boundary differs from the qualification policy") + run_root = Path(tempfile.mkdtemp(prefix="iicp-pre1-python-", dir=destination.parent)) + staging = run_root / "fragment" + staging.mkdir() + try: + common.run(["uv", "sync", "--locked", "--extra", "dev"], ROOT) + common.run(["uv", "run", "--locked", "--extra", "dev", "pytest", "-q"], ROOT) + dist = run_root / "dist" + dist.mkdir() + common.run( + [ + "uv", + "run", + "--locked", + "--extra", + "dev", + "python", + "-m", + "build", + "--outdir", + str(dist), + ], + ROOT, + ) + wheels = list(dist.glob("*.whl")) + sdists = list(dist.glob("*.tar.gz")) + if len(wheels) != 1 or len(sdists) != 1: + raise ValueError("Python build did not produce exactly one wheel and one sdist") + wheel, sdist = wheels[0], sdists[0] + + requirements = run_root / "requirements.txt" + common.run( + [ + "uv", + "export", + "--locked", + "--no-dev", + "--no-emit-project", + "--format", + "requirements-txt", + "--output-file", + str(requirements), + ], + ROOT, + ) + wheelhouse = run_root / "wheelhouse" + wheelhouse.mkdir() + common.run( + [ + sys.executable, + "-m", + "pip", + "download", + "--disable-pip-version-check", + "--dest", + str(wheelhouse), + "--requirement", + str(requirements), + ], + ROOT, + ) + + online = run_root / "online" + common.run([sys.executable, "-m", "venv", str(online)], ROOT) + common.run( + [ + str(venv_python(online)), + "-m", + "pip", + "install", + "--disable-pip-version-check", + str(wheel), + ], + ROOT, + ) + online_version = common.output([str(venv_cli(online)), "--version"], ROOT) + if version not in online_version: + raise ValueError("online Python package self-report differs") + + offline = run_root / "offline" + common.run([sys.executable, "-m", "venv", str(offline)], ROOT) + common.run( + [ + str(venv_python(offline)), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-index", + "--find-links", + str(wheelhouse), + str(wheel), + ], + ROOT, + ) + offline_version = common.output([str(venv_cli(offline)), "--version"], ROOT) + if offline_version != online_version or version not in offline_version: + raise ValueError("offline Python package self-report differs") + + copied_wheel = staging / wheel.name + copied_sdist = staging / sdist.name + shutil.copyfile(wheel, copied_wheel) + shutil.copyfile(sdist, copied_sdist) + fragment = common.emit_fragment( + staging, + component=COMPONENT, + source_commit=commit, + source_version=version, + build_target=target, + artifacts=[ + common.artifact("wheel", "any", copied_wheel), + common.artifact("sdist", "any", copied_sdist), + ], + lock_inputs_sha256=common.files_sha256( + ROOT, [ROOT / "pyproject.toml", ROOT / "uv.lock"] + ), + dependency_cache_sha256=common.tree_sha256(wheelhouse), + toolchains={ + "python": common.output([sys.executable, "--version"], ROOT), + "uv": common.output(["uv", "--version"], ROOT), + }, + ) + common.publish_staging(staging, destination) + return fragment + finally: + common.clean_failed_staging(run_root) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--describe", action="store_true") + parser.add_argument("--output", type=Path) + parser.add_argument("--target") + args = parser.parse_args() + if args.describe: + print(json.dumps(describe(), indent=2, sort_keys=True)) + return 0 + if args.output is None: + parser.error("--output is required unless --describe is used") + try: + value = build(args.output.resolve(), args.target) + except (OSError, ValueError, RuntimeError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + print(json.dumps(value, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/pre1_artifact_common.py b/scripts/pre1_artifact_common.py new file mode 100755 index 0000000..b3abd42 --- /dev/null +++ b/scripts/pre1_artifact_common.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Shared local helpers for component-owned pre-stable artifact builders.""" + +from __future__ import annotations + +import hashlib +import json +import os +import platform +import re +import shutil +import subprocess +from pathlib import Path + + +GATES = { + "locked_build": "PASS", + "online_exact_install": "PASS", + "offline_locked_install": "PASS", + "package_version_self_report": "PASS", +} + + +def canonical_sha256(value: object) -> str: + body = json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode() + return "sha256:" + hashlib.sha256(body).hexdigest() + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return "sha256:" + digest.hexdigest() + + +def files_sha256(root: Path, paths: list[Path]) -> str: + records = [ + { + "path": path.relative_to(root).as_posix(), + "sha256": file_sha256(path), + "size_bytes": path.stat().st_size, + } + for path in sorted(paths) + ] + return canonical_sha256(records) + + +def tree_sha256(root: Path) -> str: + records = [] + for path in sorted(root.rglob("*")): + if path.is_symlink(): + raise ValueError(f"dependency cache contains a symlink: {path.relative_to(root)}") + if path.is_file(): + records.append( + { + "path": path.relative_to(root).as_posix(), + "sha256": file_sha256(path), + "size_bytes": path.stat().st_size, + } + ) + return canonical_sha256(records) + + +def detected_target() -> str: + system = platform.system().lower() + machine = platform.machine().lower() + arch = ( + "x86_64" + if machine in {"x86_64", "amd64"} + else "arm64" if machine in {"arm64", "aarch64"} else None + ) + if arch is None: + raise ValueError("unsupported build architecture") + target = { + "darwin": f"macos-{arch}", + "linux": f"linux-{'aarch64' if arch == 'arm64' else arch}", + "windows": f"windows-{arch}", + }.get(system) + if target is None: + raise ValueError("unsupported build operating system") + return target + + +def require_target(requested: str | None, allowed: set[str]) -> str: + observed = detected_target() + target = requested or observed + if target != observed: + raise ValueError(f"requested target {target} differs from observed target {observed}") + if target not in allowed: + raise ValueError(f"target is outside this component boundary: {target}") + return target + + +def require_clean_source(root: Path) -> str: + if subprocess.run(["git", "diff", "--quiet", "HEAD", "--"], cwd=root).returncode: + raise ValueError("artifact build requires a clean tracked worktree") + if subprocess.run( + ["git", "diff", "--cached", "--quiet", "HEAD", "--"], cwd=root + ).returncode: + raise ValueError("artifact build requires a clean index") + commit = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=root, text=True + ).strip() + if re.fullmatch(r"[0-9a-f]{40}", commit) is None: + raise ValueError("artifact build source commit is invalid") + return commit + + +def safe_output(path: Path) -> None: + if path.exists() or path.is_symlink(): + raise ValueError("artifact output already exists") + if not path.parent.is_dir() or path.parent.is_symlink(): + raise ValueError("artifact output parent is unavailable or unsafe") + cursor = path.parent + while cursor != cursor.parent: + if cursor.is_symlink(): + raise ValueError("artifact output parent traverses a symlink") + cursor = cursor.parent + + +def run(argv: list[str], cwd: Path, env: dict[str, str] | None = None) -> None: + subprocess.run(argv, cwd=cwd, env=env, check=True) + + +def output(argv: list[str], cwd: Path, env: dict[str, str] | None = None) -> str: + return subprocess.check_output( + argv, cwd=cwd, env=env, text=True, stderr=subprocess.STDOUT + ).strip() + + +def artifact(kind: str, target: str, path: Path) -> dict: + if not path.is_file() or path.is_symlink(): + raise ValueError(f"artifact is unavailable or unsafe: {path.name}") + return { + "kind": kind, + "target": target, + "name": path.name, + "sha256": file_sha256(path), + "size_bytes": path.stat().st_size, + } + + +def emit_fragment( + staging: Path, + *, + component: str, + source_commit: str, + source_version: str, + build_target: str, + artifacts: list[dict], + lock_inputs_sha256: str, + dependency_cache_sha256: str, + toolchains: dict[str, str], +) -> dict: + value = { + "schema": "iicp.pre1-artifact-fragment.v1", + "component": component, + "source_commit": source_commit, + "source_version": source_version, + "build_target": build_target, + "artifacts": sorted( + artifacts, key=lambda row: (row["kind"], row["target"], row["name"]) + ), + "gates": dict(GATES), + "inputs": { + "lock_inputs_sha256": lock_inputs_sha256, + "dependency_cache_sha256": dependency_cache_sha256, + }, + "environment": { + "os_name": platform.system().lower(), + "os_release": platform.release(), + "architecture": platform.machine().lower(), + "toolchains": toolchains, + }, + "content_free": True, + "secrets_present": False, + "non_authorizing": True, + "fragment_sha256": None, + } + value["fragment_sha256"] = canonical_sha256(value) + manifest = staging / "artifact-fragment.json" + manifest.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n") + for path in staging.iterdir(): + if path.is_file() and not path.is_symlink(): + os.chmod(path, 0o600) + return value + + +def publish_staging(staging: Path, destination: Path) -> None: + safe_output(destination) + os.replace(staging, destination) + + +def clean_failed_staging(staging: Path) -> None: + shutil.rmtree(staging, ignore_errors=True) diff --git a/scripts/test_pre1_candidate_artifacts.py b/scripts/test_pre1_candidate_artifacts.py new file mode 100644 index 0000000..e87bd24 --- /dev/null +++ b/scripts/test_pre1_candidate_artifacts.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import json +import subprocess +import sys +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts/build_pre1_candidate_artifacts.py" + + +class Pre1CandidateArtifactBuilderTest(unittest.TestCase): + def test_description_is_content_free_and_complete(self) -> None: + value = json.loads( + subprocess.check_output([sys.executable, str(SCRIPT), "--describe"], text=True) + ) + self.assertEqual(value["component"], "client-python") + self.assertEqual( + value["artifact_identities"], [["wheel", "any"], ["sdist", "any"]] + ) + self.assertTrue(value["requires_clean_source"]) + self.assertTrue(value["non_authorizing"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index 178b927..17e6b72 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.11" -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version < '3.15'", -] +requires-python = ">=3.11, <3.15" [[package]] name = "annotated-doc" @@ -248,30 +244,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, - { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, - { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, - { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, - { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, - { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, - { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, - { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, - { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, - { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, - { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, - { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, - { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] @@ -1877,17 +1849,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, - { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, From 451ca14e20a37c97e78a7c86b9a0d8e6ee7c520b Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Fri, 28 Aug 2026 13:54:04 +0200 Subject: [PATCH 3/7] test: bind Python qualification range --- tests/test_pre1_release_boundaries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pre1_release_boundaries.py b/tests/test_pre1_release_boundaries.py index b143976..8cf180f 100644 --- a/tests/test_pre1_release_boundaries.py +++ b/tests/test_pre1_release_boundaries.py @@ -13,7 +13,7 @@ def test_minimum_python_version_is_declared_and_candidate_remains_pre1() -> None: - assert PROJECT["requires-python"] == ">=3.11" + assert PROJECT["requires-python"] == ">=3.11,<3.15" assert sys.version_info >= (3, 11) assert PROJECT["version"].split(".", 1)[0] == "0" From f4f3fa398f5d0880595b59e69679243df8fe7c9f Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Fri, 28 Aug 2026 13:57:45 +0200 Subject: [PATCH 4/7] qualification: require locked clean artifact inputs --- scripts/build_pre1_candidate_artifacts.py | 29 +++++++++++++++++++++++ scripts/pre1_artifact_common.py | 7 ++++++ 2 files changed, 36 insertions(+) diff --git a/scripts/build_pre1_candidate_artifacts.py b/scripts/build_pre1_candidate_artifacts.py index 91521b2..92f2158 100644 --- a/scripts/build_pre1_candidate_artifacts.py +++ b/scripts/build_pre1_candidate_artifacts.py @@ -123,6 +123,20 @@ def build(destination: Path, requested_target: str | None) -> dict: "pip", "install", "--disable-pip-version-check", + "--require-hashes", + "--requirement", + str(requirements), + ], + ROOT, + ) + common.run( + [ + str(venv_python(online)), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-deps", str(wheel), ], ROOT, @@ -143,6 +157,21 @@ def build(destination: Path, requested_target: str | None) -> dict: "--no-index", "--find-links", str(wheelhouse), + "--require-hashes", + "--requirement", + str(requirements), + ], + ROOT, + ) + common.run( + [ + str(venv_python(offline)), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-index", + "--no-deps", str(wheel), ], ROOT, diff --git a/scripts/pre1_artifact_common.py b/scripts/pre1_artifact_common.py index b3abd42..ea1a100 100755 --- a/scripts/pre1_artifact_common.py +++ b/scripts/pre1_artifact_common.py @@ -101,6 +101,13 @@ def require_clean_source(root: Path) -> str: ["git", "diff", "--cached", "--quiet", "HEAD", "--"], cwd=root ).returncode: raise ValueError("artifact build requires a clean index") + status = subprocess.check_output( + ["git", "status", "--porcelain", "--untracked-files=normal"], + cwd=root, + text=True, + ).strip() + if status: + raise ValueError("artifact build requires a clean worktree, including untracked files") commit = subprocess.check_output( ["git", "rev-parse", "HEAD"], cwd=root, text=True ).strip() From 246b1144657bc6ff7b999f684d4383fbd1e71301 Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Fri, 28 Aug 2026 13:59:15 +0200 Subject: [PATCH 5/7] qualification: expose native artifact targets --- scripts/build_pre1_candidate_artifacts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build_pre1_candidate_artifacts.py b/scripts/build_pre1_candidate_artifacts.py index 92f2158..8fe87e1 100644 --- a/scripts/build_pre1_candidate_artifacts.py +++ b/scripts/build_pre1_candidate_artifacts.py @@ -29,6 +29,7 @@ def describe() -> dict: return { "schema": "iicp.pre1-artifact-builder-description.v1", "component": COMPONENT, + "targets": sorted(TARGETS), "artifact_identities": [["wheel", "any"], ["sdist", "any"]], "gates": sorted(common.GATES), "requires_clean_source": True, From 6ab17a583e6495651fd4d0564713c29cb50e9bea Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Fri, 28 Aug 2026 14:03:12 +0200 Subject: [PATCH 6/7] style: normalize qualification builder imports --- scripts/build_pre1_candidate_artifacts.py | 5 +---- scripts/pre1_artifact_common.py | 27 +++++------------------ scripts/test_pre1_candidate_artifacts.py | 9 ++------ 3 files changed, 9 insertions(+), 32 deletions(-) diff --git a/scripts/build_pre1_candidate_artifacts.py b/scripts/build_pre1_candidate_artifacts.py index 8fe87e1..4596c77 100644 --- a/scripts/build_pre1_candidate_artifacts.py +++ b/scripts/build_pre1_candidate_artifacts.py @@ -13,7 +13,6 @@ import pre1_artifact_common as common - ROOT = Path(__file__).resolve().parents[1] COMPONENT = "client-python" TARGETS = { @@ -195,9 +194,7 @@ def build(destination: Path, requested_target: str | None) -> dict: common.artifact("wheel", "any", copied_wheel), common.artifact("sdist", "any", copied_sdist), ], - lock_inputs_sha256=common.files_sha256( - ROOT, [ROOT / "pyproject.toml", ROOT / "uv.lock"] - ), + lock_inputs_sha256=common.files_sha256(ROOT, [ROOT / "pyproject.toml", ROOT / "uv.lock"]), dependency_cache_sha256=common.tree_sha256(wheelhouse), toolchains={ "python": common.output([sys.executable, "--version"], ROOT), diff --git a/scripts/pre1_artifact_common.py b/scripts/pre1_artifact_common.py index ea1a100..27d3890 100755 --- a/scripts/pre1_artifact_common.py +++ b/scripts/pre1_artifact_common.py @@ -12,7 +12,6 @@ import subprocess from pathlib import Path - GATES = { "locked_build": "PASS", "online_exact_install": "PASS", @@ -22,9 +21,7 @@ def canonical_sha256(value: object) -> str: - body = json.dumps( - value, sort_keys=True, separators=(",", ":"), ensure_ascii=True - ).encode() + body = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode() return "sha256:" + hashlib.sha256(body).hexdigest() @@ -67,11 +64,7 @@ def tree_sha256(root: Path) -> str: def detected_target() -> str: system = platform.system().lower() machine = platform.machine().lower() - arch = ( - "x86_64" - if machine in {"x86_64", "amd64"} - else "arm64" if machine in {"arm64", "aarch64"} else None - ) + arch = "x86_64" if machine in {"x86_64", "amd64"} else "arm64" if machine in {"arm64", "aarch64"} else None if arch is None: raise ValueError("unsupported build architecture") target = { @@ -97,9 +90,7 @@ def require_target(requested: str | None, allowed: set[str]) -> str: def require_clean_source(root: Path) -> str: if subprocess.run(["git", "diff", "--quiet", "HEAD", "--"], cwd=root).returncode: raise ValueError("artifact build requires a clean tracked worktree") - if subprocess.run( - ["git", "diff", "--cached", "--quiet", "HEAD", "--"], cwd=root - ).returncode: + if subprocess.run(["git", "diff", "--cached", "--quiet", "HEAD", "--"], cwd=root).returncode: raise ValueError("artifact build requires a clean index") status = subprocess.check_output( ["git", "status", "--porcelain", "--untracked-files=normal"], @@ -108,9 +99,7 @@ def require_clean_source(root: Path) -> str: ).strip() if status: raise ValueError("artifact build requires a clean worktree, including untracked files") - commit = subprocess.check_output( - ["git", "rev-parse", "HEAD"], cwd=root, text=True - ).strip() + commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root, text=True).strip() if re.fullmatch(r"[0-9a-f]{40}", commit) is None: raise ValueError("artifact build source commit is invalid") return commit @@ -133,9 +122,7 @@ def run(argv: list[str], cwd: Path, env: dict[str, str] | None = None) -> None: def output(argv: list[str], cwd: Path, env: dict[str, str] | None = None) -> str: - return subprocess.check_output( - argv, cwd=cwd, env=env, text=True, stderr=subprocess.STDOUT - ).strip() + return subprocess.check_output(argv, cwd=cwd, env=env, text=True, stderr=subprocess.STDOUT).strip() def artifact(kind: str, target: str, path: Path) -> dict: @@ -168,9 +155,7 @@ def emit_fragment( "source_commit": source_commit, "source_version": source_version, "build_target": build_target, - "artifacts": sorted( - artifacts, key=lambda row: (row["kind"], row["target"], row["name"]) - ), + "artifacts": sorted(artifacts, key=lambda row: (row["kind"], row["target"], row["name"])), "gates": dict(GATES), "inputs": { "lock_inputs_sha256": lock_inputs_sha256, diff --git a/scripts/test_pre1_candidate_artifacts.py b/scripts/test_pre1_candidate_artifacts.py index e87bd24..079d0b9 100644 --- a/scripts/test_pre1_candidate_artifacts.py +++ b/scripts/test_pre1_candidate_artifacts.py @@ -6,20 +6,15 @@ import unittest from pathlib import Path - ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts/build_pre1_candidate_artifacts.py" class Pre1CandidateArtifactBuilderTest(unittest.TestCase): def test_description_is_content_free_and_complete(self) -> None: - value = json.loads( - subprocess.check_output([sys.executable, str(SCRIPT), "--describe"], text=True) - ) + value = json.loads(subprocess.check_output([sys.executable, str(SCRIPT), "--describe"], text=True)) self.assertEqual(value["component"], "client-python") - self.assertEqual( - value["artifact_identities"], [["wheel", "any"], ["sdist", "any"]] - ) + self.assertEqual(value["artifact_identities"], [["wheel", "any"], ["sdist", "any"]]) self.assertTrue(value["requires_clean_source"]) self.assertTrue(value["non_authorizing"]) From 1dee2d06947bd1a426a29fac8f3efcc44f51e978 Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Fri, 28 Aug 2026 14:06:10 +0200 Subject: [PATCH 7/7] test: tolerate loaded tunnel fixture startup --- tests/test_tunnel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tunnel.py b/tests/test_tunnel.py index bdf0b15..5382e78 100644 --- a/tests/test_tunnel.py +++ b/tests/test_tunnel.py @@ -129,7 +129,7 @@ def test_rate_limit_output_opens_creation_cooldown(self, tmp_path, monkeypatch): _reset_quick_tunnel_rate_limit_for_tests(clear_persistent=True) try: with pytest.raises(RuntimeError, match="rate limit detected"): - open_quick_tunnel(9484, timeout=1.0, binary=_fake_bin(tmp_path, FAKE_RATE_LIMITED)) + open_quick_tunnel(9484, timeout=3.0, binary=_fake_bin(tmp_path, FAKE_RATE_LIMITED)) with pytest.raises(RuntimeError, match="creation paused"): open_quick_tunnel(9484, timeout=1.0, binary=_fake_bin(tmp_path, FAKE_OK)) finally: @@ -140,7 +140,7 @@ def test_rate_limit_cooldown_survives_process_restart(self, tmp_path, monkeypatc _reset_quick_tunnel_rate_limit_for_tests(clear_persistent=True) try: with pytest.raises(RuntimeError, match="rate limit detected"): - open_quick_tunnel(9484, timeout=1.0, binary=_fake_bin(tmp_path, FAKE_RATE_LIMITED)) + open_quick_tunnel(9484, timeout=3.0, binary=_fake_bin(tmp_path, FAKE_RATE_LIMITED)) # Simulate a supervised restart: process-local state is gone, but # the node state directory still remembers the cooldown.