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,671 @@
|
|
|
1
|
+
"""External JSON-lines instance apply provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import configparser
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from email.parser import BytesParser
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import secrets
|
|
13
|
+
import shutil
|
|
14
|
+
import stat
|
|
15
|
+
import tempfile
|
|
16
|
+
import time
|
|
17
|
+
import zipfile
|
|
18
|
+
|
|
19
|
+
from .install import create_environment, install, prepare_launchers, remove_build_tools
|
|
20
|
+
from .layout import atomic_write, read_marker, validate_root
|
|
21
|
+
from .model import InstanceError, PROTOCOL, RESULT_SCHEMA, RESULT_SCHEMA_V2, canonical_json, digest, marker, marker_v2, parse_request
|
|
22
|
+
from .verify import inventory
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _lock(stream: object, acquire: bool) -> None:
|
|
26
|
+
if os.name == "nt":
|
|
27
|
+
import msvcrt
|
|
28
|
+
stream.seek(0)
|
|
29
|
+
msvcrt.locking(stream.fileno(), msvcrt.LK_NBLCK if acquire else msvcrt.LK_UNLCK, 1)
|
|
30
|
+
else:
|
|
31
|
+
import fcntl
|
|
32
|
+
fcntl.flock(stream.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB if acquire else fcntl.LOCK_UN)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _open_lock(path: Path) -> object:
|
|
36
|
+
"""Open the persistent lock without following a path-level redirect."""
|
|
37
|
+
flags = os.O_RDWR | os.O_CREAT
|
|
38
|
+
nofollow = getattr(os, "O_NOFOLLOW", 0)
|
|
39
|
+
try:
|
|
40
|
+
before = os.lstat(path)
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
before = None
|
|
43
|
+
except OSError as error:
|
|
44
|
+
raise InstanceError("LOCK_INVALID", "lifecycle lock cannot be inspected") from error
|
|
45
|
+
if before is not None:
|
|
46
|
+
if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode) or _is_reparse(before):
|
|
47
|
+
raise InstanceError("LOCK_INVALID", "lifecycle lock is not a regular non-link file")
|
|
48
|
+
if os.name != "nt" and before.st_mode & 0o077:
|
|
49
|
+
raise InstanceError("LOCK_INVALID", "lifecycle lock permissions are too broad")
|
|
50
|
+
try:
|
|
51
|
+
descriptor = os.open(os.fspath(path), flags | nofollow, 0o600)
|
|
52
|
+
except OSError as error:
|
|
53
|
+
raise InstanceError("LOCK_INVALID", "lifecycle lock cannot be opened safely") from error
|
|
54
|
+
try:
|
|
55
|
+
after = os.fstat(descriptor)
|
|
56
|
+
current = os.lstat(path)
|
|
57
|
+
if not stat.S_ISREG(after.st_mode) or stat.S_ISLNK(current.st_mode) or not stat.S_ISREG(current.st_mode) or _is_reparse(current) or (after.st_dev, after.st_ino) != (current.st_dev, current.st_ino):
|
|
58
|
+
raise InstanceError("LOCK_INVALID", "lifecycle lock changed during safe open")
|
|
59
|
+
if os.name != "nt" and after.st_mode & 0o077:
|
|
60
|
+
raise InstanceError("LOCK_INVALID", "lifecycle lock permissions are too broad")
|
|
61
|
+
return os.fdopen(descriptor, "a+b")
|
|
62
|
+
except BaseException:
|
|
63
|
+
os.close(descriptor)
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_reparse(value: os.stat_result) -> bool:
|
|
68
|
+
return os.name == "nt" and bool(getattr(value, "st_file_attributes", 0) & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _same_path(left: Path | str, right: Path | str) -> bool:
|
|
72
|
+
left_path = os.path.normcase(os.fspath(Path(left).resolve(strict=False)))
|
|
73
|
+
right_path = os.path.normcase(os.fspath(Path(right).resolve(strict=False)))
|
|
74
|
+
return left_path == right_path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
_SAFE_LAUNCHER_NAME = re.compile(r"[A-Za-z0-9._-]+\Z")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _safe_launcher_name(value: object, label: str) -> str:
|
|
81
|
+
if not isinstance(value, str) or not value or value != value.strip() or not _SAFE_LAUNCHER_NAME.fullmatch(value) or Path(value).name != value or value in {".", ".."}:
|
|
82
|
+
raise InstanceError("ARTIFACT_INVALID", f"{label} is unsafe")
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _regular(path: Path, label: str, directory: bool = False) -> None:
|
|
87
|
+
current = path
|
|
88
|
+
while current != Path(current.anchor):
|
|
89
|
+
try:
|
|
90
|
+
value = os.lstat(current)
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
current = current.parent
|
|
93
|
+
continue
|
|
94
|
+
if stat.S_ISLNK(value.st_mode) or (os.name == "nt" and bool(getattr(value, "st_file_attributes", 0) & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400))):
|
|
95
|
+
raise InstanceError("ARTIFACT_INVALID", f"{label} contains a link or reparse point")
|
|
96
|
+
current = current.parent
|
|
97
|
+
if path.is_symlink() or not (path.is_dir() if directory else path.is_file()):
|
|
98
|
+
raise InstanceError("ARTIFACT_INVALID", f"{label} is missing or unsafe")
|
|
99
|
+
if os.name == "nt" and bool(getattr(path.lstat(), "st_file_attributes", 0) & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)):
|
|
100
|
+
raise InstanceError("ARTIFACT_INVALID", f"{label} is a reparse point")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _launcher_script_name(target_name: str) -> str:
|
|
104
|
+
return _safe_launcher_name(target_name, "launcher target") + (".exe" if os.name == "nt" else "")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _launcher_retained_name(target_name: str) -> str:
|
|
108
|
+
suffix = ".exe" if os.name == "nt" else ""
|
|
109
|
+
return f"launcher-{_safe_launcher_name(target_name, 'launcher target')}{suffix}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _launcher_targets(wheelhouse: Path, artifacts: list[dict[str, object]]) -> list[str]:
|
|
113
|
+
targets: list[str] = []
|
|
114
|
+
seen: set[str] = set()
|
|
115
|
+
for item in artifacts:
|
|
116
|
+
wheel = wheelhouse / str(item["filename"])
|
|
117
|
+
try:
|
|
118
|
+
_regular(wheel, "recovery wheel")
|
|
119
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
120
|
+
entry_points = [entry for entry in archive.namelist() if entry.endswith(".dist-info/entry_points.txt")]
|
|
121
|
+
if len(entry_points) > 1:
|
|
122
|
+
raise ValueError("launcher metadata is incomplete")
|
|
123
|
+
if not entry_points:
|
|
124
|
+
continue
|
|
125
|
+
parser = configparser.ConfigParser(interpolation=None)
|
|
126
|
+
parser.optionxform = str
|
|
127
|
+
parser.read_string(archive.read(entry_points[0]).decode("utf-8"))
|
|
128
|
+
except (OSError, UnicodeDecodeError, ValueError, zipfile.BadZipFile, KeyError, configparser.Error) as error:
|
|
129
|
+
raise InstanceError("ARTIFACT_INVALID", f"recovery launcher metadata is invalid: {wheel.name}") from error
|
|
130
|
+
if not parser.has_section("console_scripts"):
|
|
131
|
+
continue
|
|
132
|
+
for target_name, _target_value in parser.items("console_scripts", raw=True):
|
|
133
|
+
target_name = _safe_launcher_name(target_name, "recovery launcher target")
|
|
134
|
+
key = target_name.casefold()
|
|
135
|
+
if key in seen:
|
|
136
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher names are duplicated or invalid")
|
|
137
|
+
seen.add(key)
|
|
138
|
+
targets.append(target_name)
|
|
139
|
+
return sorted(targets)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _launcher_collision_keys(names: set[str]) -> set[str]:
|
|
143
|
+
return {name.casefold() for name in names}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _launcher_images(scripts: Path, target_names: list[str], wheel_names: set[str]) -> list[dict[str, object]]:
|
|
147
|
+
records: list[dict[str, object]] = []
|
|
148
|
+
retained_names: set[str] = set()
|
|
149
|
+
wheel_collision_names = _launcher_collision_keys(wheel_names)
|
|
150
|
+
for target_name in target_names:
|
|
151
|
+
retained_name = _launcher_retained_name(target_name)
|
|
152
|
+
retained_key = retained_name.casefold()
|
|
153
|
+
if retained_key in retained_names or retained_key in wheel_collision_names or retained_name == "recovery-manifest.json":
|
|
154
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher filenames collide")
|
|
155
|
+
source = scripts / _launcher_script_name(target_name)
|
|
156
|
+
if source.is_symlink() or not source.is_file():
|
|
157
|
+
raise InstanceError("ARTIFACT_INVALID", f"recovery launcher is missing: {source.name}")
|
|
158
|
+
stat_result = source.lstat()
|
|
159
|
+
if _is_reparse(stat_result):
|
|
160
|
+
raise InstanceError("ARTIFACT_INVALID", f"recovery launcher is unsafe: {source.name}")
|
|
161
|
+
data = source.read_bytes()
|
|
162
|
+
records.append({"target_launcher": target_name, "retained_filename": retained_name, "size": stat_result.st_size, "sha256": hashlib.sha256(data).hexdigest(), "mode": stat.S_IMODE(stat_result.st_mode)})
|
|
163
|
+
retained_names.add(retained_key)
|
|
164
|
+
return sorted(records, key=lambda item: (str(item["target_launcher"]), str(item["retained_filename"])))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _validate_launcher_images(bundle: Path, launcher_images: object, target_names: list[str], wheel_names: set[str]) -> list[dict[str, object]]:
|
|
168
|
+
if not isinstance(launcher_images, list):
|
|
169
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher metadata is invalid")
|
|
170
|
+
expected_targets = {name.casefold() for name in target_names}
|
|
171
|
+
wheel_collision_names = _launcher_collision_keys(wheel_names)
|
|
172
|
+
if len(launcher_images) != len(expected_targets):
|
|
173
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher metadata is incomplete")
|
|
174
|
+
records: list[dict[str, object]] = []
|
|
175
|
+
seen_targets: set[str] = set()
|
|
176
|
+
seen_retained: set[str] = set()
|
|
177
|
+
for raw in launcher_images:
|
|
178
|
+
if not isinstance(raw, dict) or set(raw) != {"target_launcher", "retained_filename", "size", "sha256", "mode"}:
|
|
179
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher metadata is invalid")
|
|
180
|
+
target = _safe_launcher_name(raw["target_launcher"], "launcher target")
|
|
181
|
+
retained = _safe_launcher_name(raw["retained_filename"], "retained launcher filename")
|
|
182
|
+
retained_key = retained.casefold()
|
|
183
|
+
if retained != _launcher_retained_name(target) or retained_key in wheel_collision_names or retained == "recovery-manifest.json":
|
|
184
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher filenames collide")
|
|
185
|
+
target_key = target.casefold()
|
|
186
|
+
if target_key in seen_targets or retained_key in seen_retained:
|
|
187
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher names are duplicated or invalid")
|
|
188
|
+
if target_key not in expected_targets:
|
|
189
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher names are outside the accepted set")
|
|
190
|
+
path = bundle / retained
|
|
191
|
+
_regular(path, "retained launcher image")
|
|
192
|
+
stat_result = path.lstat()
|
|
193
|
+
data = path.read_bytes()
|
|
194
|
+
if type(raw["size"]) is not int or type(raw["mode"]) is not int or raw["size"] != len(data) or raw["sha256"] != hashlib.sha256(data).hexdigest() or stat.S_IMODE(stat_result.st_mode) != raw["mode"]:
|
|
195
|
+
raise InstanceError("ARTIFACT_INVALID", "retained launcher image is invalid")
|
|
196
|
+
seen_targets.add(target_key)
|
|
197
|
+
seen_retained.add(retained_key)
|
|
198
|
+
records.append({"target_launcher": target, "retained_filename": retained, "size": raw["size"], "sha256": raw["sha256"], "mode": raw["mode"], "bundle": str(path)})
|
|
199
|
+
if seen_targets != expected_targets:
|
|
200
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery launcher metadata is incomplete")
|
|
201
|
+
return sorted(records, key=lambda item: (str(item["target_launcher"]), str(item["retained_filename"])))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
_OPERATION_SCHEMA = "cf.bootstrap.instance-operation.v1"
|
|
205
|
+
_OWNER_NAME = ".cogniflow-staging-owner.json"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _operation_path(root: Path) -> Path:
|
|
209
|
+
return root.parent / f".{root.name}.operation.json"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _operation_value(root: Path, staging: Path, plan_digest: str, artifact_digest: str) -> dict[str, str]:
|
|
213
|
+
return {"schema": _OPERATION_SCHEMA, "installation_root": str(root.resolve(strict=False)), "staging_root": str(staging.resolve(strict=False)), "operation_token": secrets.token_hex(32), "plan_digest": plan_digest, "artifact_set_digest": artifact_digest}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _safe_read(path: Path) -> tuple[dict[str, object], bytes]:
|
|
217
|
+
try:
|
|
218
|
+
before = os.lstat(path)
|
|
219
|
+
if not stat.S_ISREG(before.st_mode) or _is_reparse(before) or (os.name != "nt" and before.st_mode & 0o077):
|
|
220
|
+
raise InstanceError("CLEANUP_FAILED", "ownership record is not a regular file")
|
|
221
|
+
descriptor = os.open(os.fspath(path), os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
222
|
+
try:
|
|
223
|
+
after = os.fstat(descriptor)
|
|
224
|
+
if (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino) or not stat.S_ISREG(after.st_mode) or _is_reparse(after) or (os.name != "nt" and after.st_mode & 0o077):
|
|
225
|
+
raise InstanceError("CLEANUP_FAILED", "ownership record changed during read")
|
|
226
|
+
raw = os.read(descriptor, 1024 * 1024)
|
|
227
|
+
finally:
|
|
228
|
+
os.close(descriptor)
|
|
229
|
+
value = json.loads(raw)
|
|
230
|
+
except InstanceError:
|
|
231
|
+
raise
|
|
232
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
233
|
+
raise InstanceError("CLEANUP_FAILED", f"ownership record is unreadable: {type(error).__name__}: {error}") from error
|
|
234
|
+
if not isinstance(value, dict) or raw != canonical_json(value):
|
|
235
|
+
raise InstanceError("CLEANUP_FAILED", "ownership record is not canonical")
|
|
236
|
+
return value, raw
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _safe_create(path: Path, data: bytes) -> None:
|
|
240
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
|
241
|
+
try:
|
|
242
|
+
descriptor = os.open(os.fspath(path), flags, 0o600)
|
|
243
|
+
try:
|
|
244
|
+
os.write(descriptor, data)
|
|
245
|
+
os.fsync(descriptor)
|
|
246
|
+
finally:
|
|
247
|
+
os.close(descriptor)
|
|
248
|
+
except FileExistsError as error:
|
|
249
|
+
raise InstanceError("CLEANUP_FAILED", "ownership record already exists") from error
|
|
250
|
+
except OSError as error:
|
|
251
|
+
raise InstanceError("CLEANUP_FAILED", "ownership record could not be created") from error
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _safe_unlink(path: Path) -> None:
|
|
255
|
+
if not os.path.lexists(path):
|
|
256
|
+
return
|
|
257
|
+
_safe_read(path)
|
|
258
|
+
try:
|
|
259
|
+
path.unlink()
|
|
260
|
+
except OSError as error:
|
|
261
|
+
raise InstanceError("CLEANUP_FAILED", "ownership record could not be cleared") from error
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _validate_operation(value: dict[str, object], root: Path, staging: Path | None, plan_digest: str, artifact_digest: str) -> Path:
|
|
265
|
+
expected_root = root.resolve(strict=False)
|
|
266
|
+
if set(value) != {"schema", "installation_root", "staging_root", "operation_token", "plan_digest", "artifact_set_digest"} or value.get("schema") != _OPERATION_SCHEMA or not _same_path(str(value["installation_root"]), expected_root) or value["plan_digest"] != plan_digest or value["artifact_set_digest"] != artifact_digest:
|
|
267
|
+
raise InstanceError("CLEANUP_FAILED", "ownership record is not bound to this operation")
|
|
268
|
+
token = value["operation_token"]
|
|
269
|
+
if not isinstance(token, str) or len(token) != 64 or any(char not in "0123456789abcdef" for char in token):
|
|
270
|
+
raise InstanceError("CLEANUP_FAILED", "ownership token is invalid")
|
|
271
|
+
recorded = Path(str(value["staging_root"]))
|
|
272
|
+
if not recorded.is_absolute() or not _same_path(recorded.parent, expected_root.parent) or not recorded.name.startswith(f".{root.name}.staging-") or (staging is not None and not _same_path(recorded, staging)):
|
|
273
|
+
raise InstanceError("CLEANUP_FAILED", "staging path is not bound to the final root")
|
|
274
|
+
return recorded
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _validate_owner(staging: Path, value: dict[str, object], plan_digest: str, artifact_digest: str) -> None:
|
|
278
|
+
if staging.is_symlink() or not staging.is_dir() or _is_reparse(os.lstat(staging)):
|
|
279
|
+
raise InstanceError("CLEANUP_FAILED", "owned staging root is not a real directory")
|
|
280
|
+
owner, _ = _safe_read(staging / _OWNER_NAME)
|
|
281
|
+
if owner != value or owner["plan_digest"] != plan_digest or owner["artifact_set_digest"] != artifact_digest:
|
|
282
|
+
raise InstanceError("CLEANUP_FAILED", "staging owner marker does not match operation record")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _delete_owned(staging: Path, value: dict[str, object], plan_digest: str, artifact_digest: str) -> None:
|
|
286
|
+
_validate_owner(staging, value, plan_digest, artifact_digest)
|
|
287
|
+
_cleanup_staging(staging)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _recover_staging(root: Path, record_path: Path, plan_digest: str, artifact_digest: str) -> None:
|
|
291
|
+
lookalikes = list(root.parent.glob(f".{root.name}.staging-*"))
|
|
292
|
+
if not os.path.lexists(record_path):
|
|
293
|
+
if lookalikes:
|
|
294
|
+
raise InstanceError("CLEANUP_FAILED", "unowned staging lookalike exists")
|
|
295
|
+
return
|
|
296
|
+
record, _ = _safe_read(record_path)
|
|
297
|
+
staging = _validate_operation(record, root, None, plan_digest, artifact_digest)
|
|
298
|
+
marker_path = root / ".cogniflow-instance.json"
|
|
299
|
+
if root.exists():
|
|
300
|
+
if not marker_path.is_file():
|
|
301
|
+
raise InstanceError("CLEANUP_FAILED", "owned operation conflicts with an unbound root")
|
|
302
|
+
marker, _ = read_marker(marker_path)
|
|
303
|
+
if not _same_path(str(marker["installation_root"]), root) or marker["plan_digest"] != plan_digest or marker["artifact_set_digest"] != artifact_digest or os.path.lexists(staging):
|
|
304
|
+
raise InstanceError("CLEANUP_FAILED", "stale ownership record conflicts with final root")
|
|
305
|
+
_safe_unlink(record_path)
|
|
306
|
+
return
|
|
307
|
+
_delete_owned(staging, record, plan_digest, artifact_digest)
|
|
308
|
+
_safe_unlink(record_path)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _cleanup_staging(path: Path) -> None:
|
|
312
|
+
if not path.exists():
|
|
313
|
+
return
|
|
314
|
+
if os.environ.get("CF_BOOTSTRAP_TEST_CLEANUP_FAILURE") == "1":
|
|
315
|
+
raise InstanceError("CLEANUP_FAILED", "injected staging cleanup failure")
|
|
316
|
+
try:
|
|
317
|
+
shutil.rmtree(path)
|
|
318
|
+
except OSError as error:
|
|
319
|
+
raise InstanceError("CLEANUP_FAILED", "staging root could not be removed") from error
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _test_abort(boundary: str) -> None:
|
|
323
|
+
if os.environ.get("CF_BOOTSTRAP_TEST_PAUSE_AT") == boundary:
|
|
324
|
+
ready = Path(os.environ["CF_BOOTSTRAP_TEST_PAUSE_FILE"])
|
|
325
|
+
ready.write_text("ready\n", encoding="utf-8")
|
|
326
|
+
release = ready.with_suffix(".release")
|
|
327
|
+
while not release.exists():
|
|
328
|
+
time.sleep(0.01)
|
|
329
|
+
if os.environ.get("CF_BOOTSTRAP_TEST_ABORT_AT") == boundary:
|
|
330
|
+
os._exit(97)
|
|
331
|
+
if os.environ.get("CF_BOOTSTRAP_TEST_FAIL_AT") == boundary:
|
|
332
|
+
raise InstanceError("TEST_FAILURE", "injected transaction failure")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _response(identifier: object, **body: object) -> dict[str, object]:
|
|
336
|
+
return {"jsonrpc": "2.0", "protocol": PROTOCOL, "id": identifier, **body}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _validate_wheelhouse_metadata(wheelhouse: Path, artifact_set: dict[str, object], mode: str) -> None:
|
|
340
|
+
manifest = wheelhouse / "artifact-set.json"
|
|
341
|
+
if manifest.is_symlink() or not manifest.is_file():
|
|
342
|
+
raise InstanceError("ARTIFACT_INVALID", "wheelhouse artifact-set.json is missing or unsafe")
|
|
343
|
+
try:
|
|
344
|
+
raw = manifest.read_bytes()
|
|
345
|
+
parsed = json.loads(raw)
|
|
346
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
347
|
+
raise InstanceError("ARTIFACT_INVALID", "wheelhouse artifact-set.json is invalid") from error
|
|
348
|
+
if raw != canonical_json(parsed) or parsed != artifact_set:
|
|
349
|
+
raise InstanceError("ARTIFACT_INVALID", "wheelhouse artifact-set.json is not bound to the request")
|
|
350
|
+
expected_lines = [f"{item['normalized_distribution']}=={item['version']} --hash=sha256:{item['sha256']}" for item in artifact_set["artifacts"]]
|
|
351
|
+
if mode == "pypi":
|
|
352
|
+
lock = wheelhouse / "requirements.lock"
|
|
353
|
+
if lock.is_symlink() or not lock.is_file():
|
|
354
|
+
raise InstanceError("ARTIFACT_INVALID", "wheelhouse requirements.lock is missing or unsafe")
|
|
355
|
+
lock_bytes = lock.read_bytes()
|
|
356
|
+
if hashlib.sha256(lock_bytes).hexdigest() != artifact_set["lock_sha256"]:
|
|
357
|
+
raise InstanceError("ARTIFACT_INVALID", "requirements.lock hash does not match artifact set")
|
|
358
|
+
expected_lock = ("\n".join(expected_lines) + "\n").encode("utf-8")
|
|
359
|
+
if lock_bytes != expected_lock:
|
|
360
|
+
raise InstanceError("ARTIFACT_INVALID", "requirements.lock does not exactly match artifact set")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def apply(params: object, *, recovery_authority: tuple[dict[str, object], dict[str, object], Path] | None = None) -> dict[str, object]:
|
|
364
|
+
plan, resolution, root = parse_request(params)
|
|
365
|
+
if plan.get("schema") == "cf.bootstrap.plan.v2":
|
|
366
|
+
return _apply_v2(plan, resolution, root)
|
|
367
|
+
artifact_set = resolution["artifact_set"]
|
|
368
|
+
wheelhouse = Path(str(resolution["wheelhouse_path"])).absolute()
|
|
369
|
+
source = plan["artifact_source"]
|
|
370
|
+
checkout = Path(source["checkout_path"]).absolute() if isinstance(source, dict) and source.get("mode") == "local-repository" else None
|
|
371
|
+
validate_root(root, checkout, wheelhouse, Path(plan["cogniflow_home"]))
|
|
372
|
+
if not wheelhouse.is_dir() or wheelhouse.is_symlink():
|
|
373
|
+
raise InstanceError("ARTIFACT_INVALID", "wheelhouse is not a real directory")
|
|
374
|
+
root.parent.mkdir(parents=True, exist_ok=True)
|
|
375
|
+
lock_path = root.parent / f".{root.name}.lifecycle.lock"
|
|
376
|
+
record_path = _operation_path(root)
|
|
377
|
+
stream = None
|
|
378
|
+
acquired = False
|
|
379
|
+
staging: Path | None = None
|
|
380
|
+
owner_removed = False
|
|
381
|
+
try:
|
|
382
|
+
try:
|
|
383
|
+
stream = _open_lock(lock_path)
|
|
384
|
+
stream.seek(0, os.SEEK_END)
|
|
385
|
+
if stream.tell() == 0:
|
|
386
|
+
stream.write(b"0")
|
|
387
|
+
stream.flush()
|
|
388
|
+
_lock(stream, True)
|
|
389
|
+
acquired = True
|
|
390
|
+
except (OSError, BlockingIOError) as error:
|
|
391
|
+
raise InstanceError("INSTANCE_LOCKED", "instance lifecycle lock is held") from error
|
|
392
|
+
_recover_staging(root, record_path, digest(plan), digest(artifact_set))
|
|
393
|
+
marker_path = root / ".cogniflow-instance.json"
|
|
394
|
+
if root.exists() and not marker_path.is_file():
|
|
395
|
+
raise InstanceError("ROOT_CONFLICT", "existing installation root contains foreign files")
|
|
396
|
+
_validate_wheelhouse_metadata(wheelhouse, artifact_set, plan["artifact_source"]["mode"])
|
|
397
|
+
if root.exists() and marker_path.is_file():
|
|
398
|
+
current, _ = read_marker(marker_path)
|
|
399
|
+
if current["plan_digest"] != digest(plan):
|
|
400
|
+
raise InstanceError("PLAN_CONFLICT", "existing instance belongs to another plan")
|
|
401
|
+
if current["artifact_set_digest"] != digest(artifact_set):
|
|
402
|
+
raise InstanceError("ARTIFACT_SET_CONFLICT", "existing instance belongs to another artifact set")
|
|
403
|
+
environment = Path(str(current["environment_root"]))
|
|
404
|
+
executable = Path(str(current["python_executable"]))
|
|
405
|
+
if not _same_path(str(current["installation_root"]), root) or not _same_path(environment, root / "environment") or environment.is_symlink() or not _same_path(executable, environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")) or not executable.is_file():
|
|
406
|
+
raise InstanceError("VENV_INVALID", "existing instance is not root-bound")
|
|
407
|
+
installed = inventory(executable, environment, artifact_set, checkout)
|
|
408
|
+
if current["installed_distributions"] != installed:
|
|
409
|
+
raise InstanceError("VERIFY_FAILED", "existing marker inventory differs from target")
|
|
410
|
+
return {"schema": RESULT_SCHEMA, "status": "unchanged", "marker": str(marker_path), "installation_root": str(root), "environment_root": str(environment), "python_executable": str(executable), "installed_distributions": installed}
|
|
411
|
+
expected_wheels = {item["filename"] for item in artifact_set["artifacts"]}
|
|
412
|
+
allowed = expected_wheels | {"artifact-set.json"}
|
|
413
|
+
if plan["artifact_source"]["mode"] == "pypi":
|
|
414
|
+
allowed.add("requirements.lock")
|
|
415
|
+
children = list(wheelhouse.iterdir())
|
|
416
|
+
if any(child.is_symlink() or child.is_dir() for child in children) or {child.name for child in children} != allowed:
|
|
417
|
+
raise InstanceError("ARTIFACT_INVALID", "wheelhouse contains unexpected entries")
|
|
418
|
+
staging = Path(tempfile.mkdtemp(prefix=f".{root.name}.staging-", dir=root.parent))
|
|
419
|
+
try:
|
|
420
|
+
operation = _operation_value(root, staging, digest(plan), digest(artifact_set))
|
|
421
|
+
_safe_create(record_path, canonical_json(operation))
|
|
422
|
+
_safe_create(staging / _OWNER_NAME, canonical_json(operation))
|
|
423
|
+
_test_abort("staging")
|
|
424
|
+
environment = staging / "environment"
|
|
425
|
+
temp_dir = staging / "temp"
|
|
426
|
+
temp_dir.mkdir()
|
|
427
|
+
executable = create_environment(environment, temp_dir)
|
|
428
|
+
_test_abort("venv")
|
|
429
|
+
install(executable, wheelhouse, artifact_set, temp_dir)
|
|
430
|
+
_test_abort("install")
|
|
431
|
+
final_executable = root / "environment" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
|
432
|
+
prepare_launchers(executable, final_executable)
|
|
433
|
+
_test_abort("launchers")
|
|
434
|
+
remove_build_tools(executable)
|
|
435
|
+
installed = inventory(executable, environment, artifact_set, checkout)
|
|
436
|
+
_test_abort("verify")
|
|
437
|
+
_cleanup_staging(temp_dir)
|
|
438
|
+
(staging / "state").mkdir()
|
|
439
|
+
(staging / "logs").mkdir()
|
|
440
|
+
_test_abort("state")
|
|
441
|
+
if recovery_authority is None:
|
|
442
|
+
marker_value = marker(plan, artifact_set, root, root / "environment", final_executable, installed)
|
|
443
|
+
else:
|
|
444
|
+
accepted_plan, accepted_artifact_set, accepted_wheelhouse = recovery_authority
|
|
445
|
+
recovery_path, recovery_digest = _retain_recovery_bundle(staging, root, accepted_wheelhouse, accepted_artifact_set, accepted_plan, executable, final_executable)
|
|
446
|
+
marker_value = marker_v2(accepted_plan, accepted_artifact_set, root, root / "environment", final_executable, installed, recovery_path, recovery_digest)
|
|
447
|
+
atomic_write(staging / ".cogniflow-instance.json", canonical_json(marker_value))
|
|
448
|
+
_test_abort("marker")
|
|
449
|
+
validate_root(root, checkout, wheelhouse, Path(plan["cogniflow_home"]))
|
|
450
|
+
if root.exists():
|
|
451
|
+
raise InstanceError("ROOT_CONFLICT", "installation root appeared during publication")
|
|
452
|
+
_test_abort("before-rename")
|
|
453
|
+
_safe_unlink(staging / _OWNER_NAME)
|
|
454
|
+
owner_removed = True
|
|
455
|
+
staging.replace(root)
|
|
456
|
+
_test_abort("after-rename")
|
|
457
|
+
_safe_unlink(record_path)
|
|
458
|
+
finally:
|
|
459
|
+
if staging is not None and staging.exists():
|
|
460
|
+
if owner_removed:
|
|
461
|
+
_cleanup_staging(staging)
|
|
462
|
+
_safe_unlink(record_path)
|
|
463
|
+
else:
|
|
464
|
+
_recover_staging(root, record_path, digest(plan), digest(artifact_set))
|
|
465
|
+
return {"schema": RESULT_SCHEMA, "status": "ready", "marker": str(marker_path), "installation_root": str(root), "environment_root": str(root / "environment"), "python_executable": str(final_executable), "installed_distributions": installed}
|
|
466
|
+
except InstanceError:
|
|
467
|
+
raise
|
|
468
|
+
except OSError as error:
|
|
469
|
+
raise InstanceError("PUBLISH_FAILED", "instance publication failed") from error
|
|
470
|
+
finally:
|
|
471
|
+
if acquired and stream is not None:
|
|
472
|
+
_lock(stream, False)
|
|
473
|
+
if stream is not None:
|
|
474
|
+
stream.close()
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _minimum_recovery_artifacts(wheelhouse: Path, artifacts: list[object]) -> list[dict[str, object]]:
|
|
478
|
+
records = {str(item.get("normalized_distribution", "")).replace("_", "-"): item for item in artifacts if isinstance(item, dict)}
|
|
479
|
+
roots = {"cf-runtime", "cf-service-client", "cf-install-services", "cf-service-mcp-server", "cf-bootstrap-source-local"}
|
|
480
|
+
if not roots <= records.keys():
|
|
481
|
+
raise InstanceError("ARTIFACT_INVALID", "minimum service-plane roots are incomplete")
|
|
482
|
+
dependencies: dict[str, set[str]] = {}
|
|
483
|
+
for name, item in records.items():
|
|
484
|
+
wheel = wheelhouse / str(item["filename"])
|
|
485
|
+
try:
|
|
486
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
487
|
+
metadata_names = [entry for entry in archive.namelist() if entry.endswith(".dist-info/METADATA")]
|
|
488
|
+
if len(metadata_names) != 1:
|
|
489
|
+
raise ValueError("wheel metadata is incomplete")
|
|
490
|
+
metadata = BytesParser().parsebytes(archive.read(metadata_names[0]))
|
|
491
|
+
except (OSError, ValueError, zipfile.BadZipFile, KeyError) as error:
|
|
492
|
+
raise InstanceError("ARTIFACT_INVALID", f"recovery wheel metadata is invalid: {wheel.name}") from error
|
|
493
|
+
required = set()
|
|
494
|
+
for requirement in metadata.get_all("Requires-Dist", []):
|
|
495
|
+
dependency = re.split(r"[ (<>=!;\\[]", requirement, maxsplit=1)[0].lower().replace("_", "-").replace(".", "-")
|
|
496
|
+
if dependency in records:
|
|
497
|
+
required.add(dependency)
|
|
498
|
+
dependencies[name] = required
|
|
499
|
+
closure = set(roots)
|
|
500
|
+
pending = list(roots)
|
|
501
|
+
while pending:
|
|
502
|
+
for dependency in dependencies[pending.pop()]:
|
|
503
|
+
if dependency not in closure:
|
|
504
|
+
closure.add(dependency)
|
|
505
|
+
pending.append(dependency)
|
|
506
|
+
return sorted((records[name] for name in closure), key=lambda item: (str(item["normalized_distribution"]), str(item["filename"])))
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _retain_recovery_bundle(content_root: Path, authority_root: Path, wheelhouse: Path, artifact_set: dict[str, object], plan: dict[str, object], staging_executable: Path, final_executable: Path) -> tuple[str, str]:
|
|
510
|
+
"""Retain the bootstrap-host-selected minimum closure inside the instance."""
|
|
511
|
+
artifacts = artifact_set.get("artifacts")
|
|
512
|
+
if not isinstance(artifacts, list):
|
|
513
|
+
raise InstanceError("ARTIFACT_INVALID", "recovery artifact set is invalid")
|
|
514
|
+
selected = _minimum_recovery_artifacts(wheelhouse, artifacts)
|
|
515
|
+
launcher_targets = _launcher_targets(wheelhouse, selected)
|
|
516
|
+
scripts = staging_executable.parent
|
|
517
|
+
wheel_names = {str(item["filename"]) for item in selected}
|
|
518
|
+
launcher_images = _launcher_images(scripts, launcher_targets, wheel_names)
|
|
519
|
+
bundle = content_root / "state" / "minimum-service-plane"
|
|
520
|
+
bundle.mkdir(parents=True, exist_ok=True)
|
|
521
|
+
for item in selected:
|
|
522
|
+
source = wheelhouse / str(item["filename"])
|
|
523
|
+
if source.is_symlink() or not source.is_file() or source.stat().st_size != item["size"] or hashlib.sha256(source.read_bytes()).hexdigest() != item["sha256"]:
|
|
524
|
+
raise InstanceError("ARTIFACT_INVALID", f"recovery wheel validation failed: {source.name}")
|
|
525
|
+
destination = bundle / source.name
|
|
526
|
+
shutil.copy2(source, destination)
|
|
527
|
+
if destination.is_symlink() or destination.stat().st_size != item["size"] or hashlib.sha256(destination.read_bytes()).hexdigest() != item["sha256"]:
|
|
528
|
+
raise InstanceError("PUBLISH_FAILED", f"retained recovery wheel verification failed: {source.name}")
|
|
529
|
+
_test_abort("recovery-copy")
|
|
530
|
+
for item in launcher_images:
|
|
531
|
+
source = scripts / _launcher_script_name(str(item["target_launcher"]))
|
|
532
|
+
destination = bundle / str(item["retained_filename"])
|
|
533
|
+
shutil.copy2(source, destination)
|
|
534
|
+
if destination.is_symlink() or destination.stat().st_size != item["size"] or hashlib.sha256(destination.read_bytes()).hexdigest() != item["sha256"] or stat.S_IMODE(destination.lstat().st_mode) != item["mode"]:
|
|
535
|
+
raise InstanceError("PUBLISH_FAILED", f"retained launcher verification failed: {source.name}")
|
|
536
|
+
_test_abort("recovery-copy")
|
|
537
|
+
closure = sorted(str(item["normalized_distribution"]).replace("_", "-") for item in selected)
|
|
538
|
+
manifest = {"schema": "cf.bootstrap.minimum-service-recovery-manifest.v2", "instance_id": plan["instance_id"], "plan_digest": digest(plan), "artifact_set_digest": digest(artifact_set), "target_python": str(final_executable), "bootstrap_plan": plan, "recovery_distributions": closure, "artifacts": sorted(selected, key=lambda item: (item["normalized_distribution"], item["filename"])), "launcher_images": launcher_images}
|
|
539
|
+
manifest_path = bundle / "recovery-manifest.json"
|
|
540
|
+
atomic_write(manifest_path, canonical_json(manifest))
|
|
541
|
+
_test_abort("recovery-manifest")
|
|
542
|
+
published_path = authority_root / "state" / "minimum-service-plane" / "recovery-manifest.json"
|
|
543
|
+
return str(published_path), "sha256:" + hashlib.sha256(manifest_path.read_bytes()).hexdigest()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _validate_retained_authority(root: Path, marker_value: dict[str, object], plan: dict[str, object], artifact_set: dict[str, object], wheelhouse: Path) -> None:
|
|
547
|
+
recovery_path = Path(str(marker_value.get("recovery_manifest_path", "")))
|
|
548
|
+
expected_path = root / "state" / "minimum-service-plane" / "recovery-manifest.json"
|
|
549
|
+
if recovery_path != expected_path or recovery_path.is_symlink() or not recovery_path.is_file():
|
|
550
|
+
raise InstanceError("VERIFY_FAILED", "existing recovery authority is missing or unsafe")
|
|
551
|
+
raw = recovery_path.read_bytes()
|
|
552
|
+
if marker_value.get("recovery_manifest_digest") != "sha256:" + hashlib.sha256(raw).hexdigest():
|
|
553
|
+
raise InstanceError("VERIFY_FAILED", "existing recovery authority is tampered")
|
|
554
|
+
try:
|
|
555
|
+
retained = json.loads(raw)
|
|
556
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
557
|
+
raise InstanceError("VERIFY_FAILED", "existing recovery authority is invalid") from error
|
|
558
|
+
selected = _minimum_recovery_artifacts(wheelhouse, artifact_set["artifacts"])
|
|
559
|
+
launcher_targets = _launcher_targets(wheelhouse, selected)
|
|
560
|
+
closure = sorted(str(item["normalized_distribution"]).replace("_", "-") for item in selected)
|
|
561
|
+
wheel_names = {str(item["filename"]) for item in selected}
|
|
562
|
+
launcher_images = _validate_launcher_images(recovery_path.parent, retained.get("launcher_images"), launcher_targets, wheel_names)
|
|
563
|
+
launcher_images = [{key: item[key] for key in ("target_launcher", "retained_filename", "size", "sha256", "mode")} for item in launcher_images]
|
|
564
|
+
expected = {"schema": "cf.bootstrap.minimum-service-recovery-manifest.v2", "instance_id": plan["instance_id"], "plan_digest": digest(plan), "artifact_set_digest": digest(artifact_set), "target_python": str(root / "environment" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")), "bootstrap_plan": plan, "recovery_distributions": closure, "artifacts": sorted(selected, key=lambda item: (item["normalized_distribution"], item["filename"])), "launcher_images": launcher_images}
|
|
565
|
+
if raw != canonical_json(expected) or retained != expected:
|
|
566
|
+
raise InstanceError("VERIFY_FAILED", "existing recovery authority does not match the accepted closure")
|
|
567
|
+
expected_names = wheel_names | {str(item["retained_filename"]) for item in launcher_images} | {"recovery-manifest.json"}
|
|
568
|
+
children = list(recovery_path.parent.iterdir())
|
|
569
|
+
if {child.name for child in children} != expected_names or any(child.is_symlink() or not child.is_file() for child in children):
|
|
570
|
+
raise InstanceError("VERIFY_FAILED", "existing recovery bundle contains missing or foreign state")
|
|
571
|
+
for item in selected:
|
|
572
|
+
wheel = recovery_path.parent / str(item["filename"])
|
|
573
|
+
if wheel.stat().st_size != item["size"] or hashlib.sha256(wheel.read_bytes()).hexdigest() != item["sha256"]:
|
|
574
|
+
raise InstanceError("VERIFY_FAILED", f"retained recovery artifact is tampered: {wheel.name}")
|
|
575
|
+
handoff_path = root / ".cogniflow-profile-handoff.json"
|
|
576
|
+
if handoff_path.exists():
|
|
577
|
+
if handoff_path.is_symlink() or not handoff_path.is_file():
|
|
578
|
+
raise InstanceError("VERIFY_FAILED", "accepted handoff record is unsafe")
|
|
579
|
+
try:
|
|
580
|
+
handoff = json.loads(handoff_path.read_bytes())
|
|
581
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
582
|
+
raise InstanceError("VERIFY_FAILED", "accepted handoff record is invalid") from error
|
|
583
|
+
if not isinstance(handoff, dict) or handoff.get("schema") != "cf.bootstrap.accepted-handoff.v2" or handoff.get("record_digest") != digest({key: value for key, value in handoff.items() if key != "record_digest"}) or handoff.get("instance_id") != plan["instance_id"] or handoff.get("plan_digest") != digest(plan) or handoff.get("recovery_manifest_path") != str(recovery_path) or handoff.get("recovery_manifest_digest") != marker_value["recovery_manifest_digest"]:
|
|
584
|
+
raise InstanceError("VERIFY_FAILED", "accepted handoff record is not bound to retained authority")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _apply_v2(plan: dict[str, object], resolution: dict[str, object], root: Path) -> dict[str, object]:
|
|
588
|
+
"""Run the hardened v1 installer with a lossless v2 validation boundary."""
|
|
589
|
+
artifact_set = resolution["artifact_set"]
|
|
590
|
+
wheelhouse = Path(str(resolution["wheelhouse_path"])).absolute()
|
|
591
|
+
source = plan["artifact_source"]
|
|
592
|
+
checkout = Path(str(source["checkout_path"])).absolute() if isinstance(source, dict) and source.get("mode") == "local-repository" else None
|
|
593
|
+
validate_root(root, checkout, wheelhouse, Path(str(plan["cogniflow_home"])))
|
|
594
|
+
if not wheelhouse.is_dir() or wheelhouse.is_symlink():
|
|
595
|
+
raise InstanceError("ARTIFACT_INVALID", "v2 wheelhouse is not a real directory")
|
|
596
|
+
marker_path = root / ".cogniflow-instance.json"
|
|
597
|
+
if root.exists() and marker_path.is_file():
|
|
598
|
+
current, _ = read_marker(marker_path)
|
|
599
|
+
if current.get("schema") != "cf.bootstrap.instance-marker.v2":
|
|
600
|
+
raise InstanceError("PLAN_CONFLICT", "existing instance is not a v2 target")
|
|
601
|
+
if current.get("plan_digest") != digest(plan):
|
|
602
|
+
raise InstanceError("PLAN_CONFLICT", "existing instance belongs to another plan")
|
|
603
|
+
if current.get("artifact_set_digest") != digest(artifact_set):
|
|
604
|
+
raise InstanceError("ARTIFACT_SET_CONFLICT", "existing instance belongs to another artifact set")
|
|
605
|
+
environment = Path(str(current["environment_root"]))
|
|
606
|
+
executable = Path(str(current["python_executable"]))
|
|
607
|
+
if environment.resolve() != root.resolve() / "environment" or executable.resolve() != environment.resolve() / ("Scripts/python.exe" if os.name == "nt" else "bin/python"):
|
|
608
|
+
raise InstanceError("VENV_INVALID", "existing v2 target is not root-bound")
|
|
609
|
+
installed = inventory(executable, environment, artifact_set, checkout)
|
|
610
|
+
if current.get("installed_distributions") != installed:
|
|
611
|
+
raise InstanceError("VERIFY_FAILED", "existing v2 marker inventory differs from target")
|
|
612
|
+
_validate_retained_authority(root, current, plan, artifact_set, wheelhouse)
|
|
613
|
+
return {"schema": RESULT_SCHEMA_V2, "status": "unchanged", "marker": str(marker_path), "installation_root": str(root), "environment_root": str(environment), "python_executable": str(executable), "installed_distributions": installed, "profile": plan["profile"], "selection_digest": artifact_set["selection_digest"]}
|
|
614
|
+
temporary = Path(tempfile.mkdtemp(prefix="cf-bootstrap-v2-install-", dir=str(wheelhouse.parent)))
|
|
615
|
+
try:
|
|
616
|
+
legacy_artifacts = []
|
|
617
|
+
for item in artifact_set["artifacts"]:
|
|
618
|
+
legacy = {key: item[key] for key in ("distribution", "normalized_distribution", "version", "filename", "sha256")}
|
|
619
|
+
legacy["size_bytes"] = item["size"]
|
|
620
|
+
if item["source_kind"] == "stonecastle-local":
|
|
621
|
+
legacy["source_project"] = item["source_project"]
|
|
622
|
+
if item.get("artifact_kind") == "native":
|
|
623
|
+
legacy.update({key: item[key] for key in ("artifact_kind", "wheel_tags", "executable")})
|
|
624
|
+
else:
|
|
625
|
+
legacy["source_kind"] = "external-wheelhouse"
|
|
626
|
+
legacy_artifacts.append(legacy)
|
|
627
|
+
shutil.copy2(wheelhouse / item["filename"], temporary / item["filename"])
|
|
628
|
+
legacy_set = {"schema": "cf.bootstrap.artifact-set.v1", "source_mode": "local-repository", "source_digest": artifact_set["repository_source_digest"], "index_digest": artifact_set["index_digest"], "artifacts": legacy_artifacts}
|
|
629
|
+
(temporary / "artifact-set.json").write_bytes(canonical_json(legacy_set))
|
|
630
|
+
legacy_plan = {key: value for key, value in plan.items() if key != "profile"}
|
|
631
|
+
legacy_plan["schema"] = "cf.bootstrap.plan.v1"
|
|
632
|
+
legacy_resolution = {"schema": "cf.bootstrap.local-artifact-resolution.v1", "wheelhouse_path": str(temporary), "artifact_set": legacy_set}
|
|
633
|
+
result = apply({"schema": "cf.bootstrap.instance-apply.request.v1", "bootstrap_plan": legacy_plan, "artifact_resolution": legacy_resolution}, recovery_authority=(plan, artifact_set, wheelhouse))
|
|
634
|
+
marker_path = root / ".cogniflow-instance.json"
|
|
635
|
+
installed = result.get("installed_distributions", [])
|
|
636
|
+
executable = Path(str(result["python_executable"]))
|
|
637
|
+
environment = Path(str(result["environment_root"]))
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
return {"schema": RESULT_SCHEMA_V2, "status": result["status"], "marker": str(marker_path), "installation_root": str(root), "environment_root": str(environment), "python_executable": str(executable), "installed_distributions": installed, "profile": plan["profile"], "selection_digest": artifact_set["selection_digest"]}
|
|
642
|
+
finally:
|
|
643
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def handle(payload: object) -> dict[str, object]:
|
|
647
|
+
identifier = payload.get("id") if isinstance(payload, dict) else None
|
|
648
|
+
try:
|
|
649
|
+
if not isinstance(payload, dict) or set(payload) != {"jsonrpc", "protocol", "id", "method", "params"} or payload.get("jsonrpc") != "2.0" or payload.get("protocol") != PROTOCOL or payload.get("method") != "instance/apply":
|
|
650
|
+
raise InstanceError("INVALID_ENVELOPE", "instance provider envelope is invalid")
|
|
651
|
+
return _response(identifier, result=apply(payload["params"]))
|
|
652
|
+
except InstanceError as error:
|
|
653
|
+
return _response(identifier, error={"code": error.code, "message": str(error), "details": error.details})
|
|
654
|
+
except Exception as error:
|
|
655
|
+
return _response(identifier, error={"code": "INTERNAL_ERROR", "message": "instance provider failed unexpectedly", "details": {"exception": type(error).__name__, "diagnostic": str(error)[:500]}})
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def main() -> int:
|
|
659
|
+
import sys
|
|
660
|
+
for line in sys.stdin:
|
|
661
|
+
try:
|
|
662
|
+
response = handle(json.loads(line))
|
|
663
|
+
except json.JSONDecodeError:
|
|
664
|
+
response = _response(None, error={"code": "MALFORMED_JSON", "message": "request must be valid JSON", "details": {}})
|
|
665
|
+
sys.stdout.write(canonical_json(response).decode())
|
|
666
|
+
sys.stdout.flush()
|
|
667
|
+
return 0
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
if __name__ == "__main__":
|
|
671
|
+
raise SystemExit(main())
|