cf-bootstrap-source-pypi 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ """Deterministic PyPI artifact resolver and semantic package surface."""
2
+
3
+ from pathlib import Path
4
+
5
+ PACKAGE_DISTRIBUTION = "cf-bootstrap-source-pypi"
6
+ PYTHON_PACKAGE = "cf_bootstrap_source_pypi"
7
+ PACKAGE_VERSION = "0.1.8"
8
+ __version__ = "0.1.8"
9
+
10
+
11
+ def package_root() -> Path:
12
+ return Path(__file__).resolve().parent
13
+
14
+
15
+ def semantics_dir() -> Path:
16
+ return package_root() / "semantics"
17
+
18
+
19
+ def semantic_files() -> tuple[Path, ...]:
20
+ return (semantics_dir() / "package.trig",)
@@ -0,0 +1,60 @@
1
+ """Wheel and publication validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import csv
7
+ import hashlib
8
+ from io import TextIOWrapper
9
+ from pathlib import Path
10
+ import re
11
+ import zipfile
12
+
13
+ from .model import ResolverError, normalize_distribution
14
+
15
+
16
+ def _safe(name: str) -> bool:
17
+ return bool(name) and not name.startswith(("/", "\\")) and not re.match(r"^[A-Za-z]:", name) and ".." not in Path(name).parts and ".." not in name.replace("\\", "/").split("/")
18
+
19
+
20
+ def validate_wheel(path: Path, expected: dict[str, object]) -> None:
21
+ try:
22
+ with zipfile.ZipFile(path) as wheel:
23
+ names = wheel.namelist()
24
+ if len(names) != len(set(names)) or any(not _safe(name) for name in names):
25
+ raise ResolverError("ARTIFACT_INVALID", "wheel contains unsafe or duplicate ZIP paths")
26
+ dist_infos = [name for name in names if name.endswith(".dist-info/METADATA")]
27
+ wheel_infos = [name for name in names if name.endswith(".dist-info/WHEEL")]
28
+ records = [name for name in names if name.endswith(".dist-info/RECORD")]
29
+ if len(dist_infos) != 1 or len(wheel_infos) != 1 or len(records) != 1:
30
+ raise ResolverError("ARTIFACT_INVALID", "wheel metadata files are incomplete")
31
+ metadata = wheel.read(dist_infos[0]).decode("utf-8")
32
+ fields = {}
33
+ for line in metadata.splitlines():
34
+ if ":" in line:
35
+ key, value = line.split(":", 1)
36
+ fields.setdefault(key.lower(), value.strip())
37
+ if normalize_distribution(fields.get("name", "")) != expected["normalized_distribution"] or fields.get("version") != expected["version"]:
38
+ raise ResolverError("ARTIFACT_INVALID", "wheel metadata does not match the report")
39
+ if any(name.endswith("direct_url.json") or name.endswith(".pth") for name in names):
40
+ raise ResolverError("ARTIFACT_INVALID", "wheel contains a forbidden direct URL or .pth file")
41
+ record_name = records[0]
42
+ checked: set[str] = set()
43
+ with TextIOWrapper(wheel.open(record_name), encoding="utf-8", newline="") as stream:
44
+ for row in csv.reader(stream):
45
+ if len(row) != 3 or row[0] in checked or row[0] not in names:
46
+ raise ResolverError("ARTIFACT_INVALID", "wheel RECORD coverage is invalid")
47
+ checked.add(row[0])
48
+ if row[0] == record_name:
49
+ if row[1] or row[2]:
50
+ raise ResolverError("ARTIFACT_INVALID", "RECORD self-entry is invalid")
51
+ else:
52
+ data = wheel.read(row[0])
53
+ if row[1] != "sha256=" + base64.urlsafe_b64encode(hashlib.sha256(data).digest()).decode().rstrip("=") or row[2] != str(len(data)):
54
+ raise ResolverError("ARTIFACT_INVALID", "RECORD hash or size is invalid")
55
+ if checked != set(names):
56
+ raise ResolverError("ARTIFACT_INVALID", "RECORD does not cover the wheel")
57
+ except zipfile.BadZipFile as error:
58
+ raise ResolverError("ARTIFACT_INVALID", "artifact is not a valid wheel") from error
59
+ if hashlib.sha256(path.read_bytes()).hexdigest() != expected["sha256"] or path.stat().st_size <= 0 or ("size_bytes" in expected and path.stat().st_size != expected["size_bytes"]):
60
+ raise ResolverError("ARTIFACT_INVALID", "wheel hash or size is invalid")
@@ -0,0 +1,37 @@
1
+ """Validation for the single explicit package index."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import sys
7
+ from urllib.parse import unquote, urlparse
8
+ from urllib.request import url2pathname
9
+
10
+ from .model import ResolverError
11
+
12
+ DEFAULT_INDEX = "https://pypi.org/simple"
13
+
14
+
15
+ def validate_index_url(value: object) -> str:
16
+ if not isinstance(value, str) or not value or any(char.isspace() for char in value):
17
+ raise ResolverError("INDEX_URL_INVALID", "index URL is invalid")
18
+ parsed = urlparse(value)
19
+ if parsed.scheme == "https":
20
+ if not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment:
21
+ raise ResolverError("INDEX_URL_INVALID", "HTTPS index URL must not contain credentials, query, or fragment")
22
+ return value.rstrip("/")
23
+ if parsed.scheme == "file":
24
+ if parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.netloc not in ("", "localhost"):
25
+ raise ResolverError("INDEX_URL_INVALID", "file index URL is not local and absolute")
26
+ decoded = unquote(parsed.path)
27
+ if "\x00" in decoded or ".." in decoded.replace("\\", "/").split("/"):
28
+ raise ResolverError("INDEX_URL_INVALID", "file index URL contains unsafe path segments")
29
+ path = Path(url2pathname(decoded))
30
+ if sys.platform == "win32" and decoded.startswith("/") and len(decoded) > 3 and decoded[2] == ":":
31
+ path = Path(url2pathname(decoded[1:]))
32
+ if not path.is_absolute() or not path.is_dir():
33
+ raise ResolverError("INDEX_URL_INVALID", "file index URL must point to an existing directory")
34
+ if any(parent.is_symlink() for parent in (path, *path.parents)):
35
+ raise ResolverError("INDEX_URL_INVALID", "file index URL must not traverse symlinks")
36
+ return value.rstrip("/")
37
+ raise ResolverError("INDEX_URL_INVALID", "only HTTPS and local file indexes are supported")
@@ -0,0 +1,18 @@
1
+ """Canonical hash-locked requirements generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+
7
+ from .model import ResolverError
8
+
9
+
10
+ def render_lock(artifacts: list[dict[str, object]]) -> bytes:
11
+ if not artifacts:
12
+ raise ResolverError("REPORT_INVALID", "cannot create an empty lock")
13
+ lines = [f"{item['normalized_distribution']}=={item['version']} --hash=sha256:{item['sha256']}" for item in sorted(artifacts, key=lambda item: str(item["normalized_distribution"]))]
14
+ return ("\n".join(lines) + "\n").encode("utf-8")
15
+
16
+
17
+ def lock_digest(lock: bytes) -> str:
18
+ return hashlib.sha256(lock).hexdigest()
@@ -0,0 +1,80 @@
1
+ """Protocol, request, and canonical serialization helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path, PurePosixPath, PureWindowsPath
7
+ import re
8
+ from typing import Any
9
+
10
+ PROTOCOL = "cf.bootstrap.artifact-resolver.v1"
11
+ REQUEST_SCHEMA = "cf.bootstrap.pypi-artifact-resolution.request.v1"
12
+ RESULT_SCHEMA = "cf.bootstrap.pypi-artifact-resolution.v1"
13
+ ARTIFACT_SET_SCHEMA = "cf.bootstrap.pypi-artifact-set.v1"
14
+ PLAN_SCHEMA = "cf.bootstrap.plan.v1"
15
+ _NAME = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?\Z")
16
+ _VERSION = re.compile(r"[0-9]+(?:\.[0-9]+){0,2}(?:[-+][A-Za-z0-9.-]+)?\Z")
17
+
18
+
19
+ class ResolverError(ValueError):
20
+ def __init__(self, code: str, message: str):
21
+ super().__init__(message)
22
+ self.code = code
23
+
24
+
25
+ def canonical_json(value: object) -> bytes:
26
+ return (json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
27
+
28
+
29
+ def normalize_distribution(name: str) -> str:
30
+ return re.sub(r"[-_.]+", "-", name).lower()
31
+
32
+
33
+ def _absolute_path(value: object, label: str) -> Path:
34
+ if not isinstance(value, str) or not value or "\x00" in value or ".." in PurePosixPath(value).parts or ".." in PureWindowsPath(value).parts:
35
+ raise ResolverError("SCHEMA_INVALID", f"{label} is unsafe")
36
+ parsed = PureWindowsPath(value) if PureWindowsPath(value).drive or "\\" in value else PurePosixPath(value)
37
+ if not parsed.is_absolute() or str(parsed) == parsed.anchor:
38
+ raise ResolverError("SCHEMA_INVALID", f"{label} must be an absolute non-root path")
39
+ return Path(value)
40
+
41
+
42
+ def _inside(child: Path, parent: Path) -> bool:
43
+ try:
44
+ child.resolve().relative_to(parent.resolve())
45
+ return True
46
+ except ValueError:
47
+ return False
48
+
49
+
50
+ def parse_request(value: object) -> tuple[dict[str, Any], Path, str]:
51
+ if not isinstance(value, dict):
52
+ raise ResolverError("SCHEMA_INVALID", "request must be an object")
53
+ fields = {"schema", "bootstrap_plan", "artifact_output_root", "index_url"}
54
+ if set(value) - fields:
55
+ raise ResolverError("UNKNOWN_REQUEST_FIELD", "request contains unknown fields")
56
+ if set(value) != fields or value.get("schema") != REQUEST_SCHEMA:
57
+ raise ResolverError("SCHEMA_INVALID", "request schema is invalid")
58
+ plan = value.get("bootstrap_plan")
59
+ plan_fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "stages"}
60
+ profiled_fields = plan_fields | {"profile"}
61
+ valid_plan = isinstance(plan, dict) and ((set(plan) == plan_fields and plan.get("schema") == PLAN_SCHEMA) or (set(plan) == profiled_fields and plan.get("schema") == "cf.bootstrap.plan.v2"))
62
+ if not valid_plan:
63
+ raise ResolverError("SCHEMA_INVALID", "bootstrap plan schema is invalid")
64
+ source = plan.get("artifact_source")
65
+ if not isinstance(source, dict) or set(source) != {"mode", "package", "version"} or source.get("mode") != "pypi":
66
+ raise ResolverError("SOURCE_INVALID", "plan source must be pypi")
67
+ if not isinstance(source["package"], str) or not _NAME.fullmatch(source["package"]):
68
+ raise ResolverError("SOURCE_INVALID", "package name is invalid")
69
+ if not isinstance(source["version"], str) or not _VERSION.fullmatch(source["version"]):
70
+ raise ResolverError("SOURCE_INVALID", "exact package version is invalid")
71
+ output = _absolute_path(value.get("artifact_output_root"), "artifact_output_root")
72
+ for key in ("installation_root", "cogniflow_home"):
73
+ if not isinstance(plan.get(key), str):
74
+ raise ResolverError("SCHEMA_INVALID", f"{key} is invalid")
75
+ if _inside(output, _absolute_path(plan[key], key)):
76
+ raise ResolverError("SCHEMA_INVALID", "artifact output is inside a protected path")
77
+ if output.exists() and output.is_symlink():
78
+ raise ResolverError("OUTPUT_CONFLICT", "artifact output must not be a symlink")
79
+ from .index_url import validate_index_url
80
+ return plan, output, validate_index_url(value["index_url"])
@@ -0,0 +1,34 @@
1
+ """Controlled external pip invocations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+
11
+ from .model import ResolverError
12
+
13
+
14
+ def _environment(temp_root: Path) -> dict[str, str]:
15
+ env = dict(os.environ)
16
+ for key in ("PYTHONPATH", "PYTHONHOME", "PIP_INDEX_URL", "PIP_EXTRA_INDEX_URL", "PIP_FIND_LINKS", "PIP_TRUSTED_HOST", "PIP_REQUIREMENT", "PIP_CONSTRAINT", "PIP_BUILD_CONSTRAINT", "PIP_TARGET", "PIP_PREFIX", "PIP_ROOT", "PIP_USER", "PIP_EDITABLE", "PIP_PRE", "PIP_NO_BINARY", "PIP_ONLY_BINARY", "PIP_NO_DEPS", "PIP_REPORT", "PIP_DRY_RUN"):
17
+ env.pop(key, None)
18
+ null_config = "NUL" if os.name == "nt" else "/dev/null"
19
+ env.update({"PIP_CONFIG_FILE": null_config, "PIP_DISABLE_PIP_VERSION_CHECK": "1", "PIP_NO_CACHE_DIR": "1", "PIP_NO_INPUT": "1", "PYTHONDONTWRITEBYTECODE": "1", "HOME": str(temp_root / "home"), "USERPROFILE": str(temp_root / "home"), "XDG_CACHE_HOME": str(temp_root / "cache"), "TMPDIR": str(temp_root), "TEMP": str(temp_root), "TMP": str(temp_root)})
20
+ return env
21
+
22
+
23
+ def run_pip(arguments: list[str], temp_root: Path, timeout: float = 120.0) -> subprocess.CompletedProcess[str]:
24
+ try:
25
+ return subprocess.run([sys.executable, "-m", "pip", "--isolated", *arguments], cwd=str(temp_root), env=_environment(temp_root), text=True, capture_output=True, timeout=timeout, check=False)
26
+ except (OSError, subprocess.TimeoutExpired) as error:
27
+ raise ResolverError("RESOLUTION_TOOL_UNAVAILABLE", "pip process was unavailable or timed out") from error
28
+
29
+
30
+ def pip_version(temp_root: Path) -> str:
31
+ result = run_pip(["--version"], temp_root)
32
+ if result.returncode or not result.stdout.startswith("pip "):
33
+ raise ResolverError("RESOLUTION_TOOL_UNAVAILABLE", "pip version could not be determined")
34
+ return result.stdout.split()[1]
@@ -0,0 +1,69 @@
1
+ """Strict validation and canonical extraction of pip report version 1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import PurePosixPath, PureWindowsPath
8
+ import re
9
+ from urllib.parse import unquote, urlparse
10
+
11
+ from .model import ResolverError, normalize_distribution
12
+
13
+ _ENVIRONMENT = ("implementation_name", "implementation_version", "os_name", "platform_machine", "platform_system", "python_full_version", "python_version", "platform_python_implementation", "sys_platform")
14
+ _HASH = re.compile(r"[0-9a-f]{64}\Z")
15
+
16
+
17
+ def parse_report(raw: bytes, package: str, version: str) -> tuple[dict[str, object], str]:
18
+ try:
19
+ report = json.loads(raw)
20
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
21
+ raise ResolverError("REPORT_INVALID", "pip report is not valid JSON") from error
22
+ if not isinstance(report, dict) or report.get("version") != "1" or not isinstance(report.get("pip_version"), str) or not report["pip_version"] or not isinstance(report.get("install"), list) or not report["install"] or not isinstance(report.get("environment"), dict) or any(not isinstance(report["environment"].get(key), str) for key in _ENVIRONMENT):
23
+ raise ResolverError("UNSUPPORTED_REPORT_VERSION" if isinstance(report, dict) and report.get("version") != "1" else "REPORT_INVALID", "pip report does not match the supported schema")
24
+ expected = normalize_distribution(package)
25
+ roots = []
26
+ artifacts: list[dict[str, object]] = []
27
+ names: set[str] = set()
28
+ filenames: set[str] = set()
29
+ for item in report["install"]:
30
+ if not isinstance(item, dict) or item.get("requested") is not False and item.get("requested") is not True:
31
+ raise ResolverError("REPORT_INVALID", "report entry is invalid")
32
+ metadata = item.get("metadata")
33
+ info = item.get("download_info")
34
+ if not isinstance(metadata, dict) or not isinstance(info, dict) or not isinstance(metadata.get("name"), str) or not isinstance(metadata.get("version"), str):
35
+ raise ResolverError("REPORT_INVALID", "report metadata is invalid")
36
+ normalized = normalize_distribution(metadata["name"])
37
+ if normalized in names:
38
+ raise ResolverError("REPORT_INVALID", "report contains duplicate distributions")
39
+ names.add(normalized)
40
+ if item.get("is_direct") is not False or item.get("is_yanked") is not False:
41
+ raise ResolverError("DIRECT_SOURCE_REJECTED" if item.get("is_direct") else "YANKED_ARTIFACT_REJECTED", "report contains a forbidden source")
42
+ if item.get("requested_extras") not in (None, [], ()):
43
+ raise ResolverError("DIRECT_SOURCE_REJECTED", "report contains requested extras")
44
+ url = info.get("url")
45
+ parsed = urlparse(url) if isinstance(url, str) else None
46
+ decoded_path = unquote(parsed.path) if parsed is not None else ""
47
+ if parsed is None or parsed.scheme not in ("https", "file") or parsed.username or parsed.password or parsed.query or parsed.fragment or not decoded_path.lower().endswith(".whl") or any(part in ("", ".", "..") for part in decoded_path.replace("\\", "/").split("/") if part == "..") or "/" in unquote(parsed.path.rsplit("/", 1)[-1]) or "\\" in unquote(parsed.path.rsplit("/", 1)[-1]):
48
+ raise ResolverError("SOURCE_DISTRIBUTION_REJECTED", "report artifact is not a wheel URL")
49
+ hashes = info.get("archive_info", {}).get("hashes") if isinstance(info.get("archive_info"), dict) else None
50
+ digest = hashes.get("sha256") if isinstance(hashes, dict) else None
51
+ if not isinstance(digest, str) or not _HASH.fullmatch(digest):
52
+ raise ResolverError("HASH_UNAVAILABLE", "report artifact has no SHA-256")
53
+ filename = unquote(parsed.path.rsplit("/", 1)[-1])
54
+ if not filename or filename in filenames or "/" in filename or "\\" in filename or PureWindowsPath(filename).drive or ".." in PurePosixPath(filename).parts:
55
+ raise ResolverError("REPORT_INVALID", "report contains an unsafe or duplicate wheel filename")
56
+ filenames.add(filename)
57
+ entry = {"distribution": metadata["name"], "normalized_distribution": normalized, "version": metadata["version"], "filename": filename, "sha256": digest, "requested": bool(item["requested"]), "url": url}
58
+ artifacts.append(entry)
59
+ if item["requested"]:
60
+ roots.append(entry)
61
+ if len(roots) != 1 or roots[0]["normalized_distribution"] != expected or roots[0]["version"] != version:
62
+ raise ResolverError("REPORT_INVALID", "report root does not match the requested exact requirement")
63
+ if any(item["requested"] for item in artifacts if item is not roots[0]):
64
+ raise ResolverError("REPORT_INVALID", "report contains multiple roots")
65
+ environment = {key: report["environment"][key] for key in _ENVIRONMENT}
66
+ selected = [{key: item[key] for key in ("distribution", "normalized_distribution", "version", "filename", "sha256", "requested")} for item in sorted(artifacts, key=lambda item: (item["normalized_distribution"], item["version"], item["filename"]))]
67
+ canonical = {"pip_report_version": "1", "pip_version": report["pip_version"], "environment": environment, "artifacts": selected}
68
+ digest = "sha256:" + hashlib.sha256(json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
69
+ return {"pip_report_version": "1", "pip_version": report["pip_version"], "environment": environment, "artifacts": artifacts}, digest
@@ -0,0 +1,147 @@
1
+ """JSON-lines entry point and transactional PyPI resolver."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ import shutil
9
+ import sys
10
+ import tempfile
11
+
12
+ from .artifacts import validate_wheel
13
+ from .lockfile import lock_digest, render_lock
14
+ from .model import ARTIFACT_SET_SCHEMA, PROTOCOL, RESULT_SCHEMA, ResolverError, canonical_json, normalize_distribution, parse_request
15
+ from .pip_runner import pip_version, run_pip
16
+ from .report import parse_report
17
+
18
+ _FIELDS = {"jsonrpc", "protocol", "id", "method", "params"}
19
+
20
+
21
+ def _response(identifier: object, **body: object) -> dict[str, object]:
22
+ return {"jsonrpc": "2.0", "protocol": PROTOCOL, "id": identifier, **body}
23
+
24
+
25
+ def _manifest(plan: dict[str, object], report: dict[str, object], digest: str, lock: bytes, artifacts: list[dict[str, object]]) -> dict[str, object]:
26
+ source = plan["artifact_source"]
27
+ environment = report["environment"]
28
+ entries = []
29
+ for item in sorted(artifacts, key=lambda value: (str(value["normalized_distribution"]), str(value["filename"]))):
30
+ entries.append({"distribution": item["distribution"], "normalized_distribution": item["normalized_distribution"], "version": item["version"], "filename": item["filename"], "sha256": item["sha256"], "size_bytes": item["size_bytes"], "requested": item["requested"]})
31
+ root = {"distribution": source["package"], "normalized_distribution": normalize_distribution(source["package"]), "version": source["version"]}
32
+ return {"schema": ARTIFACT_SET_SCHEMA, "source_mode": "pypi", "root_requirement": root, "resolver": {"pip_report_version": "1", "pip_version": report["pip_version"]}, "target_environment": environment, "resolution_digest": digest, "lock_sha256": lock_digest(lock), "artifacts": entries}
33
+
34
+
35
+ def _existing(output: Path, expected: dict[str, object]) -> dict[str, object] | None:
36
+ if not output.exists():
37
+ return None
38
+ if not output.is_dir() or output.is_symlink():
39
+ raise ResolverError("OUTPUT_CONFLICT", "artifact output is not a real directory")
40
+ try:
41
+ manifest_path = output / "artifact-set.json"
42
+ lock_path = output / "requirements.lock"
43
+ value = json.loads(manifest_path.read_bytes())
44
+ if manifest_path.read_bytes() != canonical_json(value) or lock_path.read_bytes() != expected["lock"] or value != expected["manifest"]:
45
+ raise ResolverError("OUTPUT_CONFLICT", "existing artifact output differs from the current resolution")
46
+ names = {item["filename"] for item in value["artifacts"]}
47
+ if {child.name for child in output.iterdir()} != names | {"artifact-set.json", "requirements.lock"}:
48
+ raise ResolverError("OUTPUT_CONFLICT", "existing wheelhouse contains unexpected files")
49
+ for item in value["artifacts"]:
50
+ validate_wheel(output / item["filename"], item)
51
+ return value
52
+ except ResolverError:
53
+ raise
54
+ except (OSError, ValueError, KeyError) as error:
55
+ raise ResolverError("OUTPUT_CONFLICT", "existing artifact output is invalid") from error
56
+
57
+
58
+ def resolve(params: object) -> dict[str, object]:
59
+ plan, output, index_url = parse_request(params)
60
+ output.parent.mkdir(parents=True, exist_ok=True)
61
+ with tempfile.TemporaryDirectory(prefix="cf-bootstrap-source-pypi-", dir=str(output.parent)) as temporary:
62
+ temp = Path(temporary)
63
+ report_path = temp / "report.json"
64
+ root = plan["artifact_source"]
65
+ install_args = ["install", "--dry-run", "--ignore-installed", "--quiet", "--report", str(report_path), "--only-binary=:all:", "--index-url", index_url, "--no-cache-dir", "--disable-pip-version-check", "--no-input", f"{root['package']}=={root['version']}"]
66
+ resolution = run_pip(install_args, temp)
67
+ if resolution.returncode or not report_path.is_file():
68
+ raise ResolverError("RESOLUTION_FAILED", "pip could not resolve the requested wheel closure")
69
+ report, resolution_digest = parse_report(report_path.read_bytes(), root["package"], root["version"])
70
+ lock = render_lock(report["artifacts"])
71
+ lock_path = temp / "requirements.lock"
72
+ lock_path.write_bytes(lock)
73
+ wheelhouse = temp / "wheelhouse"
74
+ wheelhouse.mkdir()
75
+ download_args = ["download", "--no-deps", "--only-binary=:all:", "--require-hashes", "--index-url", index_url, "--no-cache-dir", "--disable-pip-version-check", "--no-input", "--dest", str(wheelhouse), "--requirement", str(lock_path)]
76
+ downloaded = run_pip(download_args, temp)
77
+ if downloaded.returncode:
78
+ raise ResolverError("DOWNLOAD_FAILED", "pip could not download the hash-locked closure")
79
+ files = list(wheelhouse.iterdir())
80
+ by_name = {item.name: item for item in files if item.is_file()}
81
+ if len(files) != len(report["artifacts"]) or set(by_name) != {item["filename"] for item in report["artifacts"]}:
82
+ raise ResolverError("ARTIFACT_SET_MISMATCH", "downloaded wheel set differs from the report")
83
+ checked = []
84
+ for item in report["artifacts"]:
85
+ path = by_name[item["filename"]]
86
+ checked_item = dict(item)
87
+ checked_item["size_bytes"] = path.stat().st_size
88
+ validate_wheel(path, checked_item)
89
+ checked.append(checked_item)
90
+ execution_pip_version = pip_version(temp)
91
+ if execution_pip_version != report["pip_version"]:
92
+ raise ResolverError("REPORT_INVALID", "pip version changed during resolution")
93
+ manifest = _manifest(plan, report, resolution_digest, lock, checked)
94
+ expected = {"manifest": manifest, "lock": lock}
95
+ existing = _existing(output, expected)
96
+ if existing is not None:
97
+ return {"schema": RESULT_SCHEMA, "wheelhouse_path": str(output), "artifact_set": existing, "lock_filename": "requirements.lock"}
98
+ staging = Path(tempfile.mkdtemp(prefix="cf-bootstrap-source-pypi-publish-", dir=str(output.parent)))
99
+ try:
100
+ for path in by_name.values():
101
+ shutil.copy2(path, staging / path.name)
102
+ (staging / "requirements.lock").write_bytes(lock)
103
+ (staging / "artifact-set.json").write_bytes(canonical_json(manifest))
104
+ staging.rename(output)
105
+ except FileExistsError as error:
106
+ shutil.rmtree(staging, ignore_errors=True)
107
+ raise ResolverError("OUTPUT_CONFLICT", "artifact output appeared during publication") from error
108
+ except OSError as error:
109
+ shutil.rmtree(staging, ignore_errors=True)
110
+ raise ResolverError("INTERNAL_ERROR", "artifact publication failed") from error
111
+ return {"schema": RESULT_SCHEMA, "wheelhouse_path": str(output), "artifact_set": manifest, "lock_filename": "requirements.lock"}
112
+
113
+
114
+ def handle(payload: object) -> dict[str, object]:
115
+ identifier = payload.get("id") if isinstance(payload, dict) else None
116
+ try:
117
+ if not isinstance(payload, dict):
118
+ raise ResolverError("INVALID_ENVELOPE", "request envelope must be an object")
119
+ if set(payload) - _FIELDS or set(payload) != _FIELDS:
120
+ raise ResolverError("UNKNOWN_ENVELOPE_FIELD" if set(payload) - _FIELDS else "INVALID_ENVELOPE", "request envelope fields are invalid")
121
+ if payload.get("jsonrpc") != "2.0" or not isinstance(payload.get("id"), (str, int)) or isinstance(payload.get("id"), bool):
122
+ raise ResolverError("INVALID_ENVELOPE", "request identity is invalid")
123
+ if payload.get("protocol") != PROTOCOL:
124
+ raise ResolverError("PROTOCOL_VERSION_UNSUPPORTED", "unsupported resolver protocol")
125
+ if payload.get("method") != "artifacts/resolve":
126
+ raise ResolverError("UNKNOWN_OPERATION", "unknown resolver operation")
127
+ return _response(identifier, result=resolve(payload["params"]))
128
+ except ResolverError as error:
129
+ return _response(identifier, error={"code": error.code, "message": str(error)})
130
+ except Exception:
131
+ return _response(identifier, error={"code": "INTERNAL_ERROR", "message": "resolver failed unexpectedly"})
132
+
133
+
134
+ def main() -> int:
135
+ for line in sys.stdin:
136
+ try:
137
+ payload = json.loads(line)
138
+ response = handle(payload)
139
+ except json.JSONDecodeError:
140
+ response = _response(None, error={"code": "MALFORMED_JSON", "message": "request must be valid JSON"})
141
+ sys.stdout.write(canonical_json(response).decode())
142
+ sys.stdout.flush()
143
+ return 0
144
+
145
+
146
+ if __name__ == "__main__":
147
+ raise SystemExit(main())
@@ -0,0 +1,13 @@
1
+ @prefix cfpkg: <https://cogniflow.odea-project.org/cf#> .
2
+ @prefix contribution: <urn:cf:contribution:> .
3
+ @prefix pkg: <urn:cf:pkg:> .
4
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
5
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
6
+
7
+ pkg:cf_bootstrap_source_pypi {
8
+ pkg:cf_bootstrap_source_pypi a cfpkg:CfPackage ;
9
+ cfpkg:hasPackageManifest [ a cfpkg:PackageManifest ; cfpkg:hasDistributionName [ a cfpkg:DistributionName ; rdf:value "cf-bootstrap-source-pypi" ] ; cfpkg:hasPythonPackageName [ a cfpkg:PythonPackageName ; rdf:value "cf_bootstrap_source_pypi" ] ; cfpkg:hasPackageVersion [ a cfpkg:PackageVersion ; rdf:value "0.1.8" ] ; cfpkg:hasImplementationLanguage [ a cfpkg:ImplementationLanguage ; rdf:value "Python" ] ] ;
10
+ cfpkg:hasPackageRole [ a cfpkg:PackageRole ; rdf:value cfpkg:SpecificationPackageRole ] ;
11
+ cfpkg:hasPackageContribution contribution:adopted_package ;
12
+ skos:prefLabel "cf-bootstrap-source-pypi" ; skos:definition "An adopted Cogniflow package conforming to the basic package template." ; skos:scopeNote "Package semantics were restored through the package conformance repair service." ; skos:example "The installed package publishes its package.trig semantic source." .
13
+ }
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: cf-bootstrap-source-pypi
3
+ Version: 0.1.8
4
+ Summary: Deterministic wheel-only PyPI artifact resolver.
5
+ License: GPL-3
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+
9
+ # cf-bootstrap-source-pypi
10
+
11
+ `cf-bootstrap-source-pypi` is the artifact-source adapter for resolving a pinned Cogniflow distribution from a Python package index. It validates a complete wheel-only dependency closure and publishes a deterministic artifact set for a separate instance provider to install.
12
+
13
+ The console script communicates over `cf.bootstrap.artifact-resolver.v1` JSON lines. Inputs identify the package, exact version, index URL, and artifact output root. Outputs bind every accepted wheel to its filename, package metadata, size, and SHA-256 digest and record the resolver report.
14
+
15
+ The package does not plan installations, choose installation profiles, import resolved packages, create target environments, or perform service handoff. Profile-driven PyPI installation is currently unavailable and the public launcher rejects that mode before orchestration; Local Repository Mode uses `cf-bootstrap-source-local` instead.
@@ -0,0 +1,13 @@
1
+ cf_bootstrap_source_pypi/__init__.py,sha256=xo3-10QDvs0f5L_MD-Irwa1TI0otKrip6X-Tlu_r6Hs,497
2
+ cf_bootstrap_source_pypi/artifacts.py,sha256=QYCvi9hhB0GskF7KYDuWAbS-kVaJSxUT31hQavri0fo,3556
3
+ cf_bootstrap_source_pypi/index_url.py,sha256=nhm01U5pS5Jj_0PL3wtuEF2PKvhdegG_iJtJ7VdmfIY,1991
4
+ cf_bootstrap_source_pypi/lockfile.py,sha256=wpVNBg9XJs9Pxt4DDBAL5XZpDbVVKddjUN4EC-2uDDk,642
5
+ cf_bootstrap_source_pypi/model.py,sha256=20mu8qrIwyVUHmbBHFEIDGtfkEqLxQO630qzNFRBhbs,4083
6
+ cf_bootstrap_source_pypi/pip_runner.py,sha256=C1axpZw2RyNfczv5PY_EI6X-Oqn7coSlakzqhm93QQM,1893
7
+ cf_bootstrap_source_pypi/report.py,sha256=OrgKyspve2I7godfiba2ALIHM29JCJGxqmXXp-mkHqk,5555
8
+ cf_bootstrap_source_pypi/resolver.py,sha256=biVnAxc7uxuNyYJKgmHDhzmgS4OxlqZNti5zp8Pb12Y,8671
9
+ cf_bootstrap_source_pypi/semantics/package.trig,sha256=QZk5sQzHbyHoHnumK1ZH3R7nlig8BsZeK3nEPHiHjFU,1243
10
+ cf_bootstrap_source_pypi-0.1.8.dist-info/METADATA,sha256=22HbAT5UnsQtxpTEOT532uknfpwXmsMLG9508yXQZKc,1142
11
+ cf_bootstrap_source_pypi-0.1.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
12
+ cf_bootstrap_source_pypi-0.1.8.dist-info/entry_points.txt,sha256=q6fJPuDBkdmuLqynqP35UOl_YKXZ6NXyXexXAiUGkcA,271
13
+ cf_bootstrap_source_pypi-0.1.8.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,8 @@
1
+ [console_scripts]
2
+ cf-bootstrap-source-pypi-resolver = cf_bootstrap_source_pypi.resolver:main
3
+
4
+ [cogniflow.artifact_resolvers.v1]
5
+ pypi = cf_bootstrap_source_pypi.resolver:main
6
+
7
+ [cogniflow.semantic_sources]
8
+ cf-bootstrap-source-pypi = cf_bootstrap_source_pypi:semantic_files