cf-bootstrap-instance 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.
- cf_bootstrap_instance/__init__.py +11 -0
- cf_bootstrap_instance/handoff.py +135 -0
- cf_bootstrap_instance/install.py +119 -0
- cf_bootstrap_instance/layout.py +156 -0
- cf_bootstrap_instance/model.py +322 -0
- cf_bootstrap_instance/process.py +28 -0
- cf_bootstrap_instance/provider.py +671 -0
- cf_bootstrap_instance/recovery.py +380 -0
- cf_bootstrap_instance/semantics/package.trig +36 -0
- cf_bootstrap_instance/semantics/service.trig +93 -0
- cf_bootstrap_instance/service_executor.py +178 -0
- cf_bootstrap_instance/transition_authority.py +136 -0
- cf_bootstrap_instance/uninstall_authority.py +119 -0
- cf_bootstrap_instance/verify.py +358 -0
- cf_bootstrap_instance-0.1.8.dist-info/METADATA +6 -0
- cf_bootstrap_instance-0.1.8.dist-info/RECORD +18 -0
- cf_bootstrap_instance-0.1.8.dist-info/WHEEL +4 -0
- cf_bootstrap_instance-0.1.8.dist-info/entry_points.txt +10 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Strict instance apply protocol and canonical marker models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
REQUEST_SCHEMA = "cf.bootstrap.instance-apply.request.v1"
|
|
14
|
+
RESULT_SCHEMA = "cf.bootstrap.instance-apply.result.v1"
|
|
15
|
+
REQUEST_SCHEMA_V2 = "cf.bootstrap.instance-apply.request.v2"
|
|
16
|
+
RESULT_SCHEMA_V2 = "cf.bootstrap.instance-apply.result.v2"
|
|
17
|
+
PROTOCOL = "cf.bootstrap.instance-provider.v1"
|
|
18
|
+
MARKER_SCHEMA = "cf.bootstrap.instance-marker.v1"
|
|
19
|
+
MARKER_SCHEMA_V2 = "cf.bootstrap.instance-marker.v2"
|
|
20
|
+
PLAN_SCHEMA = "cf.bootstrap.plan.v1"
|
|
21
|
+
PLAN_SCHEMA_V2 = "cf.bootstrap.plan.v2"
|
|
22
|
+
STAGES = ("validate-inputs", "select-artifact-source", "prepare-installation", "install-artifacts", "verify-installation", "handoff-to-service")
|
|
23
|
+
_IDENTIFIER = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?\Z")
|
|
24
|
+
_VERSION = re.compile(r"[0-9][A-Za-z0-9.!+_-]*\Z")
|
|
25
|
+
_HEX = re.compile(r"[0-9a-f]{64}\Z")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InstanceError(ValueError):
|
|
29
|
+
def __init__(self, code: str, message: str, details: dict[str, object] | None = None) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.code = code
|
|
32
|
+
self.details = details or {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def canonical_json(value: object) -> bytes:
|
|
36
|
+
return (json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def digest(value: object) -> str:
|
|
40
|
+
return "sha256:" + hashlib.sha256(canonical_json(value)).hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _absolute(value: object, label: str) -> Path:
|
|
44
|
+
if not isinstance(value, str) or not value or value != value.strip() or any(ord(char) < 32 for char in value) or ".." in Path(value).parts:
|
|
45
|
+
raise InstanceError("SCHEMA_INVALID", f"{label} is unsafe")
|
|
46
|
+
path = Path(value)
|
|
47
|
+
if not path.is_absolute() or path == Path(path.anchor):
|
|
48
|
+
raise InstanceError("SCHEMA_INVALID", f"{label} must be an absolute non-root path")
|
|
49
|
+
return path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _text(value: object, label: str, pattern: re.Pattern[str] | None = None) -> str:
|
|
53
|
+
if not isinstance(value, str) or not value or value != value.strip() or any(ord(char) < 32 or char.isspace() for char in value):
|
|
54
|
+
raise InstanceError("SCHEMA_INVALID", f"{label} must be a clean string")
|
|
55
|
+
if pattern is not None and not pattern.fullmatch(value):
|
|
56
|
+
raise InstanceError("SCHEMA_INVALID", f"{label} is invalid")
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def normalize_distribution(value: str) -> str:
|
|
61
|
+
return re.sub(r"[-_.]+", "-", value).lower()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _native_wheel_tag() -> tuple[str, str]:
|
|
65
|
+
machine = platform.machine().lower()
|
|
66
|
+
if platform.system().lower() == "windows" and machine in {"amd64", "x86_64"}:
|
|
67
|
+
return "py3-none-win_amd64", ".exe"
|
|
68
|
+
if platform.system().lower() == "linux" and machine in {"amd64", "x86_64"}:
|
|
69
|
+
return "py3-none-manylinux_2_39_x86_64", ""
|
|
70
|
+
raise InstanceError("ARTIFACT_INVALID", "native artifact platform is unsupported")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _under(left: Path, right: Path) -> bool:
|
|
74
|
+
try:
|
|
75
|
+
left.resolve(strict=False).relative_to(right.resolve(strict=False))
|
|
76
|
+
return True
|
|
77
|
+
except ValueError:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _validate_plan(plan: object) -> dict[str, Any]:
|
|
82
|
+
if isinstance(plan, dict) and plan.get("schema") == PLAN_SCHEMA_V2:
|
|
83
|
+
fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "profile", "stages"}
|
|
84
|
+
if set(plan) != fields or not isinstance(plan.get("profile"), str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9+.-]*:[^\s]+", plan["profile"]):
|
|
85
|
+
raise InstanceError("SCHEMA_INVALID", "v2 bootstrap plan fields are invalid")
|
|
86
|
+
checked = {key: value for key, value in plan.items() if key != "profile"} | {"schema": PLAN_SCHEMA}
|
|
87
|
+
_validate_plan(checked)
|
|
88
|
+
return plan
|
|
89
|
+
fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "stages"}
|
|
90
|
+
if not isinstance(plan, dict) or set(plan) != fields or plan.get("schema") != PLAN_SCHEMA:
|
|
91
|
+
raise InstanceError("SCHEMA_INVALID", "bootstrap plan fields are invalid")
|
|
92
|
+
instance_id = _text(plan.get("instance_id"), "instance_id", _IDENTIFIER)
|
|
93
|
+
if len(instance_id) > 64:
|
|
94
|
+
raise InstanceError("SCHEMA_INVALID", "instance_id is too long")
|
|
95
|
+
root = _absolute(plan.get("installation_root"), "installation_root")
|
|
96
|
+
home = _absolute(plan.get("cogniflow_home"), "cogniflow_home")
|
|
97
|
+
if _under(root, home) or _under(home, root):
|
|
98
|
+
raise InstanceError("ROOT_INVALID", "installation root and Cogniflow home must be disjoint")
|
|
99
|
+
source = plan.get("artifact_source")
|
|
100
|
+
if not isinstance(source, dict) or set(source) not in ({"mode", "checkout_path"}, {"mode", "package", "version"}):
|
|
101
|
+
raise InstanceError("SCHEMA_INVALID", "artifact source fields are invalid")
|
|
102
|
+
if source.get("mode") == "local-repository":
|
|
103
|
+
if set(source) != {"mode", "checkout_path"}:
|
|
104
|
+
raise InstanceError("SCHEMA_INVALID", "local source fields are invalid")
|
|
105
|
+
_absolute(source.get("checkout_path"), "checkout_path")
|
|
106
|
+
elif source.get("mode") == "pypi":
|
|
107
|
+
if set(source) != {"mode", "package", "version"}:
|
|
108
|
+
raise InstanceError("SCHEMA_INVALID", "PyPI source fields are invalid")
|
|
109
|
+
_text(source.get("package"), "package", _IDENTIFIER)
|
|
110
|
+
_text(source.get("version"), "version", _VERSION)
|
|
111
|
+
else:
|
|
112
|
+
raise InstanceError("SCHEMA_INVALID", "unsupported artifact source mode")
|
|
113
|
+
stages = plan.get("stages")
|
|
114
|
+
if not isinstance(stages, list) or len(stages) != len(STAGES):
|
|
115
|
+
raise InstanceError("SCHEMA_INVALID", "lifecycle stages are incomplete")
|
|
116
|
+
names: list[str] = []
|
|
117
|
+
for index, stage in enumerate(stages):
|
|
118
|
+
if not isinstance(stage, dict) or set(stage) != {"name", "ordinal", "handoff"} or stage.get("name") != STAGES[index] or stage.get("ordinal") != index or stage.get("handoff") is not (index == 5):
|
|
119
|
+
raise InstanceError("SCHEMA_INVALID", "lifecycle stages are not canonical")
|
|
120
|
+
names.append(stage["name"])
|
|
121
|
+
if len(set(names)) != len(names):
|
|
122
|
+
raise InstanceError("SCHEMA_INVALID", "lifecycle stages contain duplicates")
|
|
123
|
+
return plan
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def parse_request(value: object) -> tuple[dict[str, Any], dict[str, Any], Path]:
|
|
127
|
+
if isinstance(value, dict) and value.get("schema") == REQUEST_SCHEMA_V2:
|
|
128
|
+
return _parse_request_v2(value)
|
|
129
|
+
if not isinstance(value, dict) or set(value) != {"schema", "bootstrap_plan", "artifact_resolution"}:
|
|
130
|
+
raise InstanceError("SCHEMA_INVALID", "instance request fields are invalid")
|
|
131
|
+
if value["schema"] != REQUEST_SCHEMA:
|
|
132
|
+
raise InstanceError("SCHEMA_INVALID", "unsupported instance request schema")
|
|
133
|
+
plan = _validate_plan(value["bootstrap_plan"])
|
|
134
|
+
root = _absolute(plan.get("installation_root"), "installation_root")
|
|
135
|
+
home = _absolute(plan.get("cogniflow_home"), "cogniflow_home")
|
|
136
|
+
resolution = value["artifact_resolution"]
|
|
137
|
+
if not isinstance(resolution, dict) or set(resolution) not in ({"schema", "wheelhouse_path", "artifact_set"}, {"schema", "wheelhouse_path", "artifact_set", "lock_filename"}):
|
|
138
|
+
raise InstanceError("SCHEMA_INVALID", "artifact resolution is invalid")
|
|
139
|
+
wheelhouse = _absolute(resolution.get("wheelhouse_path"), "wheelhouse_path")
|
|
140
|
+
source_mode = plan["artifact_source"]["mode"]
|
|
141
|
+
if source_mode == "local-repository" and (_under(wheelhouse, Path(plan["artifact_source"]["checkout_path"])) or _under(Path(plan["artifact_source"]["checkout_path"]), wheelhouse)):
|
|
142
|
+
raise InstanceError("ROOT_INVALID", "wheelhouse and checkout must be disjoint")
|
|
143
|
+
if _under(wheelhouse, home) or _under(home, wheelhouse):
|
|
144
|
+
raise InstanceError("ROOT_INVALID", "wheelhouse and Cogniflow home must be disjoint")
|
|
145
|
+
resolution_schema = "cf.bootstrap.pypi-artifact-resolution.v1" if source_mode == "pypi" else "cf.bootstrap.local-artifact-resolution.v1"
|
|
146
|
+
artifact_schema = "cf.bootstrap.pypi-artifact-set.v1" if source_mode == "pypi" else "cf.bootstrap.artifact-set.v1"
|
|
147
|
+
required_resolution = {"schema", "wheelhouse_path", "artifact_set", "lock_filename"} if source_mode == "pypi" else {"schema", "wheelhouse_path", "artifact_set"}
|
|
148
|
+
if set(resolution) != required_resolution or resolution.get("schema") != resolution_schema:
|
|
149
|
+
raise InstanceError("SCHEMA_INVALID", "artifact resolution fields do not match source mode")
|
|
150
|
+
if source_mode == "pypi" and resolution.get("lock_filename") != "requirements.lock":
|
|
151
|
+
raise InstanceError("SCHEMA_INVALID", "PyPI lock filename is invalid")
|
|
152
|
+
artifact_set = resolution.get("artifact_set")
|
|
153
|
+
if not isinstance(artifact_set, dict) or artifact_set.get("schema") != artifact_schema or artifact_set.get("source_mode") != source_mode or not isinstance(artifact_set.get("artifacts"), list):
|
|
154
|
+
raise InstanceError("SCHEMA_INVALID", "artifact set is invalid")
|
|
155
|
+
expected_fields = {"schema", "source_mode", "root_requirement", "resolver", "target_environment", "resolution_digest", "lock_sha256", "artifacts"} if source_mode == "pypi" else {"schema", "source_mode", "source_digest", "index_digest", "artifacts"}
|
|
156
|
+
if set(artifact_set) != expected_fields:
|
|
157
|
+
raise InstanceError("SCHEMA_INVALID", "artifact set fields are invalid")
|
|
158
|
+
if source_mode == "pypi":
|
|
159
|
+
root_requirement = artifact_set["root_requirement"]
|
|
160
|
+
if not isinstance(root_requirement, dict) or set(root_requirement) != {"distribution", "normalized_distribution", "version"} or root_requirement["distribution"] != plan["artifact_source"]["package"] or root_requirement["version"] != plan["artifact_source"]["version"] or normalize_distribution(root_requirement["distribution"]) != root_requirement["normalized_distribution"]:
|
|
161
|
+
raise InstanceError("ARTIFACT_INVALID", "PyPI root requirement is not bound to the plan")
|
|
162
|
+
if not isinstance(artifact_set["resolution_digest"], str) or not artifact_set["resolution_digest"].startswith("sha256:") or not _HEX.fullmatch(artifact_set["resolution_digest"][7:]) or not isinstance(artifact_set["lock_sha256"], str) or not _HEX.fullmatch(artifact_set["lock_sha256"]):
|
|
163
|
+
raise InstanceError("ARTIFACT_INVALID", "PyPI digests are invalid")
|
|
164
|
+
environment = artifact_set["target_environment"]
|
|
165
|
+
environment_keys = {"implementation_name", "implementation_version", "os_name", "platform_machine", "platform_system", "python_full_version", "python_version", "platform_python_implementation", "sys_platform"}
|
|
166
|
+
if not isinstance(environment, dict) or set(environment) != environment_keys or any(not isinstance(item, str) or not item for item in environment.values()):
|
|
167
|
+
raise InstanceError("ARTIFACT_INVALID", "PyPI target environment is invalid")
|
|
168
|
+
resolver = artifact_set["resolver"]
|
|
169
|
+
if not isinstance(resolver, dict) or set(resolver) != {"pip_report_version", "pip_version"} or any(not isinstance(item, str) or not item for item in resolver.values()):
|
|
170
|
+
raise InstanceError("ARTIFACT_INVALID", "PyPI resolver metadata is invalid")
|
|
171
|
+
else:
|
|
172
|
+
for key in ("source_digest", "index_digest"):
|
|
173
|
+
if not isinstance(artifact_set[key], str) or not artifact_set[key].startswith("sha256:") or not _HEX.fullmatch(artifact_set[key][7:]):
|
|
174
|
+
raise InstanceError("ARTIFACT_INVALID", "Local source digest is invalid")
|
|
175
|
+
_validate_artifacts(artifact_set["artifacts"], source_mode)
|
|
176
|
+
return plan, resolution, root
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _parse_request_v2(value: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any], Path]:
|
|
180
|
+
if set(value) != {"schema", "bootstrap_plan", "artifact_resolution"}:
|
|
181
|
+
raise InstanceError("SCHEMA_INVALID", "v2 instance request fields are invalid")
|
|
182
|
+
plan = _validate_plan(value["bootstrap_plan"])
|
|
183
|
+
root = _absolute(plan["installation_root"], "installation_root")
|
|
184
|
+
resolution = value["artifact_resolution"]
|
|
185
|
+
if not isinstance(resolution, dict) or set(resolution) != {"schema", "wheelhouse_path", "artifact_set"} or resolution.get("schema") != "cf.bootstrap.local-artifact-resolution.v2":
|
|
186
|
+
raise InstanceError("SCHEMA_INVALID", "v2 artifact resolution is invalid")
|
|
187
|
+
wheelhouse = _absolute(resolution["wheelhouse_path"], "wheelhouse_path")
|
|
188
|
+
checkout = _absolute(plan["artifact_source"].get("checkout_path"), "checkout_path") if plan["artifact_source"].get("mode") == "local-repository" else None
|
|
189
|
+
home = _absolute(plan["cogniflow_home"], "cogniflow_home")
|
|
190
|
+
if checkout is not None and (_under(wheelhouse, checkout) or _under(checkout, wheelhouse)):
|
|
191
|
+
raise InstanceError("ROOT_INVALID", "v2 wheelhouse and checkout must be disjoint")
|
|
192
|
+
if _under(wheelhouse, root) or _under(root, wheelhouse) or _under(wheelhouse, home) or _under(home, wheelhouse):
|
|
193
|
+
raise InstanceError("ROOT_INVALID", "v2 wheelhouse is not disjoint from protected roots")
|
|
194
|
+
if wheelhouse.exists() and (wheelhouse.is_symlink() or not wheelhouse.is_dir()):
|
|
195
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 wheelhouse is not a regular directory")
|
|
196
|
+
artifact_set = resolution["artifact_set"]
|
|
197
|
+
fields = {"schema", "source_mode", "plan_digest", "profile", "profile_digest", "catalog_digest", "requested_capabilities", "post_install_verification_capability", "selected_roots", "selected_first_party", "selected_external", "selection_digest", "repository_source_digest", "selected_source_digest", "index_digest", "resolver", "target_environment", "artifacts"}
|
|
198
|
+
if not isinstance(artifact_set, dict) or set(artifact_set) != fields or artifact_set.get("schema") != "cf.bootstrap.artifact-set.v2" or artifact_set.get("source_mode") != "local-repository" or artifact_set.get("profile") != plan["profile"]:
|
|
199
|
+
raise InstanceError("SCHEMA_INVALID", "v2 artifact set is invalid")
|
|
200
|
+
for key in ("plan_digest", "profile_digest", "catalog_digest", "selection_digest", "repository_source_digest", "selected_source_digest", "index_digest"):
|
|
201
|
+
if not isinstance(artifact_set[key], str) or not _HEX.fullmatch(artifact_set[key].removeprefix("sha256:")):
|
|
202
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 artifact set digest is invalid")
|
|
203
|
+
if artifact_set["plan_digest"] != digest(plan):
|
|
204
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 plan digest does not match the supplied plan")
|
|
205
|
+
selection = {
|
|
206
|
+
"profile": artifact_set["profile"],
|
|
207
|
+
"profile_digest": artifact_set["profile_digest"],
|
|
208
|
+
"catalog_digest": artifact_set["catalog_digest"],
|
|
209
|
+
"requested_capabilities": artifact_set["requested_capabilities"],
|
|
210
|
+
"post_install_verification_capability": artifact_set["post_install_verification_capability"],
|
|
211
|
+
"provider_roots": artifact_set["selected_roots"],
|
|
212
|
+
"first_party_closure": artifact_set["selected_first_party"],
|
|
213
|
+
"external_requirements": artifact_set["selected_external"],
|
|
214
|
+
"target_environment": artifact_set["target_environment"],
|
|
215
|
+
}
|
|
216
|
+
if artifact_set["selection_digest"] != digest(selection):
|
|
217
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 selection digest does not match the selection object")
|
|
218
|
+
_validate_v2_artifacts(artifact_set["artifacts"])
|
|
219
|
+
_validate_v2_selection(artifact_set)
|
|
220
|
+
return plan, resolution, root
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _external_name(requirement: str) -> str:
|
|
224
|
+
return re.split(r"[<>=!;\s\[]", requirement, maxsplit=1)[0].replace("_", "-").lower()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _validate_v2_selection(artifact_set: dict[str, Any]) -> None:
|
|
228
|
+
local = {item["normalized_distribution"] for item in artifact_set["artifacts"] if item["source_kind"] == "stonecastle-local" and item.get("artifact_kind") != "native"}
|
|
229
|
+
external = {item["normalized_distribution"] for item in artifact_set["artifacts"] if item["source_kind"] == "external-wheelhouse"}
|
|
230
|
+
expected_external = {_external_name(value) for value in artifact_set["selected_external"]}
|
|
231
|
+
roots = set(artifact_set["selected_roots"])
|
|
232
|
+
if local != set(artifact_set["selected_first_party"]):
|
|
233
|
+
raise InstanceError("ARTIFACT_INVALID", "local artifacts do not match selected first-party closure")
|
|
234
|
+
if external != expected_external or not roots <= local:
|
|
235
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact lists do not match v2 selection")
|
|
236
|
+
if any(item.startswith("cf-") for item in external):
|
|
237
|
+
raise InstanceError("EXTERNAL_SOURCE_FORBIDDEN", "external wheelhouse contains a cf-* artifact")
|
|
238
|
+
environment = artifact_set["target_environment"]
|
|
239
|
+
expected = {"implementation_name": "cpython", "python_full_version": platform.python_version(), "python_version": platform.python_version(), "sys_platform": sys.platform, "platform_system": platform.system(), "platform_machine": platform.machine().lower(), "platform_python_implementation": platform.python_implementation()}
|
|
240
|
+
if not isinstance(environment, dict) or any(environment.get(key) != value for key, value in expected.items()):
|
|
241
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact target environment does not match active interpreter")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _validate_v2_artifacts(entries: object) -> None:
|
|
245
|
+
if not isinstance(entries, list) or not entries:
|
|
246
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 artifact set is empty")
|
|
247
|
+
local_keys = {"distribution", "normalized_distribution", "version", "filename", "sha256", "size", "source_kind", "source_project"}
|
|
248
|
+
external_keys = local_keys - {"source_project"}
|
|
249
|
+
names: set[str] = set()
|
|
250
|
+
for item in entries:
|
|
251
|
+
native_keys = local_keys | {"artifact_kind", "wheel_tags", "executable"}
|
|
252
|
+
if not isinstance(item, dict) or set(item) not in (local_keys, external_keys, native_keys) or item["source_kind"] not in {"stonecastle-local", "external-wheelhouse"} or (item["source_kind"] == "stonecastle-local" and set(item) not in (local_keys, native_keys)) or (item["source_kind"] == "external-wheelhouse" and set(item) != external_keys) or not isinstance(item["sha256"], str) or not _HEX.fullmatch(item["sha256"]) or type(item["size"]) is not int or item["size"] < 0:
|
|
253
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 artifact entry is invalid")
|
|
254
|
+
if item["normalized_distribution"] in names:
|
|
255
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 artifact distributions are duplicated")
|
|
256
|
+
names.add(item["normalized_distribution"])
|
|
257
|
+
if entries != sorted(entries, key=lambda item: (item["normalized_distribution"], item["filename"])):
|
|
258
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 artifact entries are not sorted")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _validate_artifacts(entries: object, mode: str) -> None:
|
|
262
|
+
if not isinstance(entries, list) or not entries:
|
|
263
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact set is empty")
|
|
264
|
+
names: set[str] = set()
|
|
265
|
+
normalized_names: set[str] = set()
|
|
266
|
+
filenames: set[str] = set()
|
|
267
|
+
base_expected = {"distribution", "normalized_distribution", "version", "filename", "sha256", "size_bytes", "requested"} if mode == "pypi" else {"distribution", "normalized_distribution", "version", "filename", "sha256", "size_bytes", "source_project"}
|
|
268
|
+
for item in entries:
|
|
269
|
+
expected = base_expected
|
|
270
|
+
if mode == "local-repository" and isinstance(item, dict) and item.get("source_kind") == "external-wheelhouse":
|
|
271
|
+
expected = {"distribution", "normalized_distribution", "version", "filename", "sha256", "size_bytes", "source_kind"}
|
|
272
|
+
if mode == "local-repository" and isinstance(item, dict) and item.get("artifact_kind") == "native":
|
|
273
|
+
expected = expected | {"artifact_kind", "wheel_tags", "executable"}
|
|
274
|
+
if not isinstance(item, dict) or set(item) != expected:
|
|
275
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact entry fields are invalid")
|
|
276
|
+
distribution = _text(item["distribution"], "distribution", _IDENTIFIER)
|
|
277
|
+
normalized = _text(item["normalized_distribution"], "normalized_distribution", re.compile(r"[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\Z"))
|
|
278
|
+
if normalize_distribution(distribution) != normalized or distribution in names or normalized in normalized_names:
|
|
279
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact distributions are duplicated or not normalized")
|
|
280
|
+
_text(item["version"], "version", _VERSION)
|
|
281
|
+
filename = _text(item["filename"], "filename")
|
|
282
|
+
if not filename.endswith(".whl") or Path(filename).name != filename or filename in filenames:
|
|
283
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact filename is unsafe or duplicated")
|
|
284
|
+
if not isinstance(item["sha256"], str) or not _HEX.fullmatch(item["sha256"]):
|
|
285
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact hash is invalid")
|
|
286
|
+
if type(item["size_bytes"]) is not int or item["size_bytes"] < 1:
|
|
287
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact size is invalid")
|
|
288
|
+
if mode == "pypi" and type(item["requested"]) is not bool:
|
|
289
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact requested flag is invalid")
|
|
290
|
+
if mode == "local-repository" and item.get("source_kind") != "external-wheelhouse":
|
|
291
|
+
source_project = item["source_project"]
|
|
292
|
+
if not isinstance(source_project, str) or not source_project.startswith("stonecastle/") or ".." in Path(source_project).parts:
|
|
293
|
+
raise InstanceError("ARTIFACT_INVALID", "local artifact provenance is invalid")
|
|
294
|
+
if item.get("artifact_kind") == "native":
|
|
295
|
+
tag, suffix = _native_wheel_tag()
|
|
296
|
+
authorities = {
|
|
297
|
+
"cf-service-mcp-server": ("stonecastle/cf_service_mcp_server", "cf-service-mcp-server"),
|
|
298
|
+
"cogniflow-tui": ("stonecastle/cf_install_tui_ratatui", "cogniflow-tui"),
|
|
299
|
+
}
|
|
300
|
+
authority = authorities.get(item["normalized_distribution"])
|
|
301
|
+
wheel_name = item["normalized_distribution"].replace("-", "_")
|
|
302
|
+
expected_filename = f"{wheel_name}-{item['version']}-{tag}.whl"
|
|
303
|
+
expected_executable = f"{wheel_name}-{item['version']}.data/scripts/{authority[1] if authority else ''}{suffix}"
|
|
304
|
+
if authority is None or source_project != authority[0] or item["distribution"] != item["normalized_distribution"] or item["wheel_tags"] != [tag] or item["filename"] != expected_filename or item["executable"] != expected_executable:
|
|
305
|
+
raise InstanceError("ARTIFACT_INVALID", "native artifact metadata is not canonical")
|
|
306
|
+
if not isinstance(item["wheel_tags"], list) or not all(isinstance(value, str) for value in item["wheel_tags"]) or not isinstance(item["executable"], str) or "\\" in item["executable"] or not item["executable"].startswith(f"{wheel_name}-"):
|
|
307
|
+
raise InstanceError("ARTIFACT_INVALID", "native artifact fields are unsafe")
|
|
308
|
+
names.add(distribution); normalized_names.add(normalized); filenames.add(filename)
|
|
309
|
+
if entries != sorted(entries, key=lambda item: (item["normalized_distribution"], item["filename"])):
|
|
310
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact entries are not canonically sorted")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def marker(plan: dict[str, Any], artifact_set: dict[str, Any], root: Path, environment: Path, executable: Path, installed: list[dict[str, str]]) -> dict[str, object]:
|
|
314
|
+
return {"schema": MARKER_SCHEMA, "instance_id": plan["instance_id"], "installation_root": str(root), "environment_root": str(environment), "python_executable": str(executable), "plan_digest": digest(plan), "artifact_set_digest": digest(artifact_set), "installed_distributions": installed, "state": "ready"}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def marker_v2(plan: dict[str, Any], artifact_set: dict[str, Any], root: Path, environment: Path, executable: Path, installed: list[dict[str, str]], recovery_manifest_path: str | None = None, recovery_manifest_digest: str | None = None) -> dict[str, object]:
|
|
318
|
+
value = {"schema": MARKER_SCHEMA_V2, "instance_id": plan["instance_id"], "installation_root": str(root), "environment_root": str(environment), "python_executable": str(executable), "profile": plan["profile"], "plan_digest": digest(plan), "profile_digest": artifact_set["profile_digest"], "catalog_digest": artifact_set["catalog_digest"], "selection_digest": artifact_set["selection_digest"], "artifact_set_digest": digest(artifact_set), "installed_distributions": installed}
|
|
319
|
+
if recovery_manifest_path is not None and recovery_manifest_digest is not None:
|
|
320
|
+
value["recovery_manifest_path"] = recovery_manifest_path
|
|
321
|
+
value["recovery_manifest_digest"] = recovery_manifest_digest
|
|
322
|
+
return value
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Bounded child-process execution with a controlled environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .model import InstanceError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(command: list[str], *, timeout: float, cwd: Path | None = None, temp_root: Path | None = None) -> subprocess.CompletedProcess[str]:
|
|
14
|
+
removed = {"PYTHONPATH", "PYTHONHOME", "PYTHONUSERBASE", "PIP_CONFIG_FILE", "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", "PIP_CACHE_DIR"}
|
|
15
|
+
environment = {key: value for key, value in os.environ.items() if key not in removed}
|
|
16
|
+
environment.update({"PYTHONNOUSERSITE": "1", "PYTHONDONTWRITEBYTECODE": "1", "PIP_NO_INPUT": "1", "PIP_DISABLE_PIP_VERSION_CHECK": "1", "PIP_NO_CACHE_DIR": "1", "PIP_CONFIG_FILE": "NUL" if os.name == "nt" else "/dev/null", "PATH": str(Path(sys.executable).parent)})
|
|
17
|
+
if temp_root is not None:
|
|
18
|
+
temp_root.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
environment.update({"HOME": str(temp_root), "USERPROFILE": str(temp_root), "TMPDIR": str(temp_root), "TEMP": str(temp_root), "TMP": str(temp_root), "XDG_CACHE_HOME": str(temp_root / "cache")})
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(command, cwd=cwd, env=environment, capture_output=True, text=True, timeout=timeout, shell=False, check=False)
|
|
22
|
+
except subprocess.TimeoutExpired as error:
|
|
23
|
+
raise InstanceError("PROCESS_TIMEOUT", "child process exceeded its timeout") from error
|
|
24
|
+
except OSError as error:
|
|
25
|
+
raise InstanceError("PROCESS_START_FAILED", "child process could not be started") from error
|
|
26
|
+
if result.returncode != 0:
|
|
27
|
+
raise InstanceError("PROCESS_FAILED", (result.stderr or result.stdout or "child process failed").strip()[:1000])
|
|
28
|
+
return result
|