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,178 @@
|
|
|
1
|
+
"""The bounded first installer-service operation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import importlib
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import stat
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
|
|
14
|
+
from .layout import read_marker
|
|
15
|
+
|
|
16
|
+
OPERATION = "urn:cf:service:accept_bootstrap_handoff"
|
|
17
|
+
CAPABILITY = "urn:cf:service:BootstrapInstallationHandoff"
|
|
18
|
+
PROFILED_OPERATION = "urn:cf:service:accept_profiled_bootstrap_handoff"
|
|
19
|
+
PROFILED_CAPABILITY = "urn:cf:service:ProfiledBootstrapInstallationHandoff"
|
|
20
|
+
PYPI_PROFILED_OPERATION = "urn:cf:service:accept_pypi_profiled_bootstrap_handoff"
|
|
21
|
+
PYPI_PROFILED_CAPABILITY = "urn:cf:service:PublicPyPIProfiledBootstrapInstallationHandoff"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _helpers():
|
|
25
|
+
envelopes = importlib.import_module("cf_" + "service_client.envelopes")
|
|
26
|
+
executor = importlib.import_module("cf_" + "service_client.executor")
|
|
27
|
+
return envelopes.error_result, envelopes.json_output, envelopes.ok_result, executor.parse_executor_request
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _canonical(value: object) -> bytes:
|
|
31
|
+
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _digest(value: object) -> str:
|
|
35
|
+
return "sha256:" + hashlib.sha256(_canonical(value)).hexdigest()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _link_free(path: Path) -> bool:
|
|
39
|
+
current = path
|
|
40
|
+
components: list[Path] = []
|
|
41
|
+
while current != Path(current.anchor):
|
|
42
|
+
components.append(current)
|
|
43
|
+
current = current.parent
|
|
44
|
+
for component in reversed(components):
|
|
45
|
+
if component.is_symlink():
|
|
46
|
+
return False
|
|
47
|
+
if os.name == "nt" and component.exists():
|
|
48
|
+
try:
|
|
49
|
+
if os.lstat(component).st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT:
|
|
50
|
+
return False
|
|
51
|
+
except OSError:
|
|
52
|
+
return False
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _accepted_handoff(root: Path, inputs: Mapping[str, object], marker: Mapping[str, object]) -> dict[str, str]:
|
|
57
|
+
bundle = Path(str(inputs["post_handoff_artifact_root"]))
|
|
58
|
+
if not bundle.is_absolute() or not _link_free(bundle) or bundle.is_symlink() or not bundle.is_dir():
|
|
59
|
+
raise ValueError("post-handoff artifact root is missing or unsafe")
|
|
60
|
+
bundle = bundle.resolve()
|
|
61
|
+
environment = Path(sys.prefix).resolve()
|
|
62
|
+
source = inputs.get("artifact_source")
|
|
63
|
+
checkout = inputs.get("checkout_path")
|
|
64
|
+
protected = [root.resolve(), environment, Path(str(inputs["cogniflow_home"])).resolve()]
|
|
65
|
+
if checkout is not None:
|
|
66
|
+
protected.append(Path(str(checkout)).resolve())
|
|
67
|
+
if any(bundle == item or bundle.is_relative_to(item) or item.is_relative_to(bundle) for item in protected):
|
|
68
|
+
raise ValueError("post-handoff artifact root overlaps a protected root")
|
|
69
|
+
payload = {
|
|
70
|
+
"schema": "cf.bootstrap.accepted-handoff.v3" if source is not None else "cf.bootstrap.accepted-handoff.v2",
|
|
71
|
+
"instance_id": str(inputs["instance_id"]),
|
|
72
|
+
"installation_root": str(root),
|
|
73
|
+
"target_python": str(Path(sys.executable).resolve()),
|
|
74
|
+
"plan_digest": str(inputs["plan_digest"]),
|
|
75
|
+
"bootstrap_host_artifact_set_digest": str(inputs["artifact_set_digest"]),
|
|
76
|
+
"profile": str(inputs["profile"]),
|
|
77
|
+
**({"profile_digest": str(inputs["profile_digest"])} if source is None else {"artifact_source": dict(source)}),
|
|
78
|
+
"bootstrap_selection_digest": str(inputs["selection_digest"]),
|
|
79
|
+
"cogniflow_home": str(Path(str(inputs["cogniflow_home"])).resolve()),
|
|
80
|
+
**({"checkout_path": str(Path(str(checkout)).resolve())} if checkout is not None else {}),
|
|
81
|
+
"post_handoff_artifact_root": str(bundle),
|
|
82
|
+
"post_handoff_artifact_set_digest": str(inputs["post_handoff_artifact_set_digest"]),
|
|
83
|
+
"post_handoff_selection_digest": str(inputs["post_handoff_selection_digest"]),
|
|
84
|
+
"post_handoff_profile_digest": str(inputs.get("post_handoff_profile_digest", inputs["artifact_set_digest"])),
|
|
85
|
+
"post_handoff_catalog_digest": str(inputs.get("post_handoff_catalog_digest", inputs["artifact_set_digest"])),
|
|
86
|
+
"post_handoff_repository_source_digest": str(inputs.get("post_handoff_repository_source_digest", _digest(source))),
|
|
87
|
+
"post_handoff_index_digest": str(inputs.get("post_handoff_index_digest", _digest("https://pypi.org/simple"))),
|
|
88
|
+
**({"recovery_manifest_path": str(marker["recovery_manifest_path"]), "recovery_manifest_digest": str(marker["recovery_manifest_digest"])} if "recovery_manifest_path" in marker else {}),
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
payload["record_digest"] = _digest(payload)
|
|
92
|
+
path = root / ".cogniflow-profile-handoff.json"
|
|
93
|
+
raw = _canonical(payload)
|
|
94
|
+
if path.exists():
|
|
95
|
+
if path.is_symlink() or not path.is_file() or path.read_bytes() != raw:
|
|
96
|
+
raise ValueError("accepted handoff record differs from the existing record")
|
|
97
|
+
else:
|
|
98
|
+
from .layout import atomic_write
|
|
99
|
+
atomic_write(path, raw)
|
|
100
|
+
return payload
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def execute(request: Mapping[str, object]) -> dict[str, object]:
|
|
104
|
+
error_result, json_output, ok_result, parse_executor_request = _helpers()
|
|
105
|
+
try:
|
|
106
|
+
parsed = parse_executor_request(request)
|
|
107
|
+
operation = request["operation"]
|
|
108
|
+
if not isinstance(operation, Mapping):
|
|
109
|
+
return error_result("UNSUPPORTED_OPERATION", "operation is not the bootstrap handoff operation")
|
|
110
|
+
local_profiled = operation.get("iri") == PROFILED_OPERATION and operation.get("toolName") == "accept_profiled_bootstrap_handoff" and operation.get("capability") == PROFILED_CAPABILITY
|
|
111
|
+
pypi_profiled = operation.get("iri") == PYPI_PROFILED_OPERATION and operation.get("toolName") == "accept_pypi_profiled_bootstrap_handoff" and operation.get("capability") == PYPI_PROFILED_CAPABILITY
|
|
112
|
+
profiled = local_profiled or pypi_profiled
|
|
113
|
+
legacy = operation.get("iri") == OPERATION and operation.get("toolName") == "accept_bootstrap_handoff" and operation.get("capability") == CAPABILITY
|
|
114
|
+
if not (profiled or legacy):
|
|
115
|
+
return error_result("UNSUPPORTED_OPERATION", "operation is not a supported bootstrap handoff operation")
|
|
116
|
+
if parsed.parameters:
|
|
117
|
+
return error_result("INVALID_ARGUMENT", "parameters are not accepted")
|
|
118
|
+
expected = {"instance_id", "installation_root", "plan_digest", "artifact_set_digest"}
|
|
119
|
+
pypi = pypi_profiled
|
|
120
|
+
if profiled:
|
|
121
|
+
expected |= ({"profile", "selection_digest", "cogniflow_home", "artifact_source", "post_handoff_artifact_root", "post_handoff_artifact_set_digest", "post_handoff_selection_digest", "post_handoff_binding_digest"} if pypi else {"profile", "profile_digest", "selection_digest", "cogniflow_home", "checkout_path", "post_handoff_artifact_root", "post_handoff_artifact_set_digest", "post_handoff_selection_digest", "post_handoff_binding_digest", "post_handoff_profile_digest", "post_handoff_catalog_digest", "post_handoff_repository_source_digest", "post_handoff_index_digest"})
|
|
122
|
+
source = parsed.inputs.get("artifact_source")
|
|
123
|
+
if set(parsed.inputs) != expected or any(not isinstance(value, str) or not value for key, value in parsed.inputs.items() if key != "artifact_source") or (pypi and (not isinstance(source, dict) or set(source) != {"mode", "package", "version"} or source.get("mode") != "pypi" or any(not isinstance(source.get(key), str) or not source[key] for key in ("package", "version")))):
|
|
124
|
+
return error_result("INVALID_ARGUMENT", "handoff inputs are strict")
|
|
125
|
+
environment = Path(sys.prefix).resolve()
|
|
126
|
+
python = Path(sys.executable).resolve()
|
|
127
|
+
raw_root = Path(parsed.inputs["installation_root"])
|
|
128
|
+
if not raw_root.is_absolute() or not _link_free(raw_root):
|
|
129
|
+
return error_result("TARGET_MISMATCH", "installation root is unsafe")
|
|
130
|
+
root = raw_root.resolve()
|
|
131
|
+
protected_keys = ("cogniflow_home",) if pypi else ("cogniflow_home", "checkout_path")
|
|
132
|
+
protected_inputs = {key: Path(parsed.inputs[key]) for key in protected_keys} if profiled else {}
|
|
133
|
+
if profiled and any(not path.is_absolute() or not _link_free(path) or path == Path(path.anchor) for path in protected_inputs.values()):
|
|
134
|
+
return error_result("TARGET_MISMATCH", "profiled handoff root is unsafe")
|
|
135
|
+
marker_path = root / ".cogniflow-instance.json"
|
|
136
|
+
marker, _ = read_marker(marker_path)
|
|
137
|
+
if profiled and marker.get("schema") != ("cf.bootstrap.instance-marker.v1" if pypi else "cf.bootstrap.instance-marker.v2"):
|
|
138
|
+
return error_result("MARKER_MISMATCH", "profiled handoff requires a v2 instance marker")
|
|
139
|
+
else:
|
|
140
|
+
marker, _ = read_marker(marker_path)
|
|
141
|
+
expected_paths = {"installation_root": root, "environment_root": environment, "python_executable": python}
|
|
142
|
+
if marker["instance_id"] != parsed.inputs["instance_id"] or marker["plan_digest"] != parsed.inputs["plan_digest"] or marker["artifact_set_digest"] != parsed.inputs["artifact_set_digest"]:
|
|
143
|
+
return error_result("MARKER_MISMATCH", "instance marker digests or identity do not match")
|
|
144
|
+
if profiled and not pypi and any(marker[key] != parsed.inputs[key] for key in ("profile", "profile_digest", "selection_digest")):
|
|
145
|
+
return error_result("MARKER_MISMATCH", "profile or selection digests do not match")
|
|
146
|
+
if profiled:
|
|
147
|
+
bundle_raw = Path(parsed.inputs["post_handoff_artifact_root"])
|
|
148
|
+
if not bundle_raw.is_absolute() or not _link_free(bundle_raw):
|
|
149
|
+
return error_result("ARTIFACT_MISMATCH", "post-handoff artifact root is unsafe")
|
|
150
|
+
bundle = bundle_raw.resolve()
|
|
151
|
+
protected = (root, environment, *(path.resolve() for path in protected_inputs.values()))
|
|
152
|
+
if bundle.is_symlink() or any(bundle == item or bundle.is_relative_to(item) or item.is_relative_to(bundle) for item in protected):
|
|
153
|
+
return error_result("ARTIFACT_MISMATCH", "post-handoff artifact root is invalid")
|
|
154
|
+
binding_keys = (("cogniflow_home", "artifact_source", "post_handoff_artifact_root", "post_handoff_artifact_set_digest", "post_handoff_selection_digest") if pypi else ("cogniflow_home", "checkout_path", "post_handoff_artifact_root", "post_handoff_artifact_set_digest", "post_handoff_selection_digest", "post_handoff_profile_digest", "post_handoff_catalog_digest", "post_handoff_repository_source_digest", "post_handoff_index_digest"))
|
|
155
|
+
binding = {key: parsed.inputs[key] for key in binding_keys}
|
|
156
|
+
import hashlib, json
|
|
157
|
+
expected_binding = "sha256:" + hashlib.sha256((json.dumps(binding, sort_keys=True, separators=(",", ":")) + "\n").encode()).hexdigest()
|
|
158
|
+
if parsed.inputs["post_handoff_binding_digest"] != expected_binding:
|
|
159
|
+
return error_result("ARTIFACT_MISMATCH", "post-handoff artifact binding is invalid")
|
|
160
|
+
for key, value in expected_paths.items():
|
|
161
|
+
if Path(str(marker[key])).resolve() != value:
|
|
162
|
+
return error_result("TARGET_MISMATCH", "executor is not running in the marked target environment")
|
|
163
|
+
outputs = [
|
|
164
|
+
json_output("status", "accepted"), json_output("instance_id", marker["instance_id"]),
|
|
165
|
+
json_output("installation_root", str(root)), json_output("environment_root", str(environment)),
|
|
166
|
+
json_output("python_executable", str(python)), json_output("plan_digest", marker["plan_digest"]),
|
|
167
|
+
json_output("artifact_set_digest", marker["artifact_set_digest"]),
|
|
168
|
+
]
|
|
169
|
+
if profiled:
|
|
170
|
+
record = _accepted_handoff(root, parsed.inputs, marker)
|
|
171
|
+
if pypi:
|
|
172
|
+
outputs.extend((json_output("profile", parsed.inputs["profile"]), json_output("selection_digest", parsed.inputs["selection_digest"])))
|
|
173
|
+
else:
|
|
174
|
+
outputs.extend(json_output(key, marker[key]) for key in ("profile", "profile_digest", "selection_digest"))
|
|
175
|
+
outputs.append(json_output("accepted_handoff_record", record))
|
|
176
|
+
return ok_result(outputs)
|
|
177
|
+
except (ValueError, OSError) as error:
|
|
178
|
+
return error_result("INVALID_REQUEST", str(error))
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Crash-safe authority validation for accepted profile transitions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
from .layout import atomic_write
|
|
11
|
+
|
|
12
|
+
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
13
|
+
_PROFILE = re.compile(r"urn:cf:profile:[A-Za-z0-9.-]+\Z")
|
|
14
|
+
_STATE = ".cogniflow-profile-transition.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _digest(value: object) -> str:
|
|
18
|
+
return "sha256:" + hashlib.sha256((json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()).hexdigest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _canonical(value: object) -> bytes:
|
|
22
|
+
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _recovery_path(root: Path) -> Path:
|
|
26
|
+
return root / "state" / "minimum-service-plane" / "recovery-manifest.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _paths(root: Path) -> dict[str, Path]:
|
|
30
|
+
return {"marker": root / ".cogniflow-instance.json", "handoff": root / ".cogniflow-profile-handoff.json", "manifest": root / ".cogniflow-profile.json", "recovery": _recovery_path(root)}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_pair(current: dict[str, Any], target: dict[str, Any]) -> None:
|
|
34
|
+
required = {"profile", "plan_digest", "bootstrap_selection_digest", "bootstrap_artifact_set_digest", "post_handoff_artifact_set_digest", "post_handoff_selection_digest", "post_handoff_profile_digest", "post_handoff_catalog_digest", "post_handoff_repository_source_digest", "post_handoff_index_digest", "recovery_manifest_digest"}
|
|
35
|
+
if set(current) != required or set(target) != required or current == target:
|
|
36
|
+
raise ValueError("authority transition bindings are incomplete")
|
|
37
|
+
for item in (current, target):
|
|
38
|
+
if not _PROFILE.fullmatch(str(item["profile"])):
|
|
39
|
+
raise ValueError("authority profile is invalid")
|
|
40
|
+
if any(not isinstance(item[key], str) or not _DIGEST.fullmatch(item[key]) for key in required - {"profile"}):
|
|
41
|
+
raise ValueError("authority transition bindings are invalid")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read(path: Path) -> dict[str, Any]:
|
|
45
|
+
if path.is_symlink() or not path.is_file():
|
|
46
|
+
raise ValueError("authority file is missing or unsafe")
|
|
47
|
+
raw = path.read_bytes()
|
|
48
|
+
value = json.loads(raw)
|
|
49
|
+
if not isinstance(value, dict) or raw != _canonical(value):
|
|
50
|
+
raise ValueError("authority file is not canonical")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _snapshot(root: Path) -> dict[str, str]:
|
|
55
|
+
return {name: _canonical(_read(path)).decode() for name, path in _paths(root).items()}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _state(root: Path) -> dict[str, Any]:
|
|
59
|
+
path = root / _STATE
|
|
60
|
+
if path.is_symlink() or not path.is_file():
|
|
61
|
+
raise ValueError("profile transition state is missing")
|
|
62
|
+
raw = path.read_bytes()
|
|
63
|
+
value = json.loads(raw)
|
|
64
|
+
if not isinstance(value, dict) or raw != _canonical(value) or value.get("schema") != "cf.bootstrap.profile-transition-authority.v1" or value.get("status") not in {"prepared", "committed"}:
|
|
65
|
+
raise ValueError("profile transition state is invalid")
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def classify(root: Path) -> str:
|
|
70
|
+
state = _state(root)
|
|
71
|
+
actual = _snapshot(root)
|
|
72
|
+
if actual == state["current"]["files"]:
|
|
73
|
+
return "complete-a"
|
|
74
|
+
if actual == state["target"]["files"]:
|
|
75
|
+
return "complete-b"
|
|
76
|
+
raise ValueError("profile transition authority is mixed or tampered")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def recover(root: Path) -> str:
|
|
80
|
+
state = _state(root)
|
|
81
|
+
actual = _snapshot(root)
|
|
82
|
+
current_files = state.get("current", {}).get("files")
|
|
83
|
+
target_files = state.get("target", {}).get("files")
|
|
84
|
+
if not isinstance(current_files, dict) or not isinstance(target_files, dict) or set(current_files) != set(_paths(root)) or set(target_files) != set(_paths(root)):
|
|
85
|
+
raise ValueError("profile transition state snapshots are invalid")
|
|
86
|
+
if actual == current_files:
|
|
87
|
+
classification = "complete-a"
|
|
88
|
+
elif actual == target_files:
|
|
89
|
+
classification = "complete-b"
|
|
90
|
+
elif all(actual[name] in {current_files[name], target_files[name]} for name in actual):
|
|
91
|
+
classification = "prepared-mixed"
|
|
92
|
+
else:
|
|
93
|
+
raise ValueError("profile transition authority is mixed or tampered")
|
|
94
|
+
if state["status"] == "committed":
|
|
95
|
+
return "complete-b" if classification == "complete-b" else classify(root)
|
|
96
|
+
for name, path in _paths(root).items():
|
|
97
|
+
if actual[name] != current_files[name]:
|
|
98
|
+
atomic_write(path, current_files[name].encode())
|
|
99
|
+
(root / _STATE).unlink()
|
|
100
|
+
return "complete-a"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def rebind(root: Path, current: dict[str, Any], target: dict[str, Any], *, marker: dict[str, Any], handoff: dict[str, Any], manifest: dict[str, Any], recovery: dict[str, Any], target_handoff: Mapping[str, Any] | None = None, target_manifest: Mapping[str, Any] | None = None, target_recovery: Mapping[str, Any] | None = None, target_marker: Mapping[str, Any] | None = None) -> None:
|
|
104
|
+
"""Publish complete B authority as one restart-classifiable commit."""
|
|
105
|
+
legacy = {"profile", "plan_digest", "artifact_set_digest", "recovery_manifest_digest"}
|
|
106
|
+
def complete(value: dict[str, Any]) -> dict[str, Any]:
|
|
107
|
+
if set(value) != legacy:
|
|
108
|
+
return value
|
|
109
|
+
return {"profile": value["profile"], "plan_digest": value["plan_digest"], "bootstrap_selection_digest": value["artifact_set_digest"], "bootstrap_artifact_set_digest": value["artifact_set_digest"], "post_handoff_artifact_set_digest": value["artifact_set_digest"], "post_handoff_selection_digest": value["artifact_set_digest"], "post_handoff_profile_digest": value["artifact_set_digest"], "post_handoff_catalog_digest": value["artifact_set_digest"], "post_handoff_repository_source_digest": value["artifact_set_digest"], "post_handoff_index_digest": value["artifact_set_digest"], "recovery_manifest_digest": value["recovery_manifest_digest"]}
|
|
110
|
+
current = complete(current)
|
|
111
|
+
target = complete(target)
|
|
112
|
+
validate_pair(current, target)
|
|
113
|
+
if any(item.get("profile") != current["profile"] for item in (marker, handoff, manifest)):
|
|
114
|
+
raise ValueError("current authority does not match A")
|
|
115
|
+
paths = _paths(root)
|
|
116
|
+
if not paths["recovery"].is_file() or paths["recovery"].read_bytes() != _canonical(recovery):
|
|
117
|
+
raise ValueError("retained recovery authority does not match A")
|
|
118
|
+
if current["recovery_manifest_digest"] != "sha256:" + hashlib.sha256(paths["recovery"].read_bytes()).hexdigest():
|
|
119
|
+
raise ValueError("current recovery authority digest does not match A")
|
|
120
|
+
marker_value = {**marker, **dict(target_marker or {}), "profile": target["profile"], "plan_digest": target["plan_digest"], "artifact_set_digest": target["bootstrap_artifact_set_digest"], "recovery_manifest_digest": target["recovery_manifest_digest"]}
|
|
121
|
+
handoff_value = {**handoff, **dict(target_handoff or {}), "profile": target["profile"], "plan_digest": target["plan_digest"], "bootstrap_host_artifact_set_digest": target["bootstrap_artifact_set_digest"], "recovery_manifest_digest": target["recovery_manifest_digest"]}
|
|
122
|
+
handoff_value.pop("record_digest", None)
|
|
123
|
+
handoff_value["record_digest"] = _digest(handoff_value)
|
|
124
|
+
manifest_value = {**manifest, **dict(target_manifest or {}), "profile": target["profile"], "plan_digest": target["plan_digest"], "handoff_record_digest": handoff_value["record_digest"], "bootstrap_host_artifact_set_digest": target["bootstrap_artifact_set_digest"]}
|
|
125
|
+
recovery_value = {**recovery, **dict(target_recovery or {}), "plan_digest": target["plan_digest"], "artifact_set_digest": target["bootstrap_artifact_set_digest"]}
|
|
126
|
+
target_recovery_digest = "sha256:" + hashlib.sha256(_canonical(recovery_value)).hexdigest()
|
|
127
|
+
if target["recovery_manifest_digest"] != target_recovery_digest:
|
|
128
|
+
raise ValueError("target recovery authority digest does not match B")
|
|
129
|
+
current_files = _snapshot(root)
|
|
130
|
+
target_values = {"marker": marker_value, "handoff": handoff_value, "manifest": manifest_value, "recovery": recovery_value}
|
|
131
|
+
target_files = {name: _canonical(value).decode() for name, value in target_values.items()}
|
|
132
|
+
state = {"schema": "cf.bootstrap.profile-transition-authority.v1", "status": "prepared", "current": {**current, "files": current_files}, "target": {**target, "files": target_files}}
|
|
133
|
+
atomic_write(root / _STATE, _canonical(state))
|
|
134
|
+
for name, value in target_values.items():
|
|
135
|
+
atomic_write(paths[name], _canonical(value))
|
|
136
|
+
atomic_write(root / _STATE, _canonical({**state, "status": "committed"}))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Strict accepted-instance authority classification for uninstall."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import stat
|
|
10
|
+
|
|
11
|
+
from .layout import read_marker
|
|
12
|
+
from .transition_authority import classify as classify_transition
|
|
13
|
+
|
|
14
|
+
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _canonical(value: object) -> bytes:
|
|
18
|
+
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _regular_canonical(path: Path) -> dict[str, object]:
|
|
22
|
+
try:
|
|
23
|
+
info = os.lstat(path)
|
|
24
|
+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode) or (os.name == "nt" and info.st_file_attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)):
|
|
25
|
+
raise ValueError
|
|
26
|
+
raw = path.read_bytes()
|
|
27
|
+
value = json.loads(raw)
|
|
28
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
29
|
+
raise ValueError(f"accepted authority file is missing or unsafe: {path.name}") from error
|
|
30
|
+
if not isinstance(value, dict) or raw != _canonical(value):
|
|
31
|
+
raise ValueError(f"accepted authority file is not canonical: {path.name}")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _link_free(path: Path) -> bool:
|
|
36
|
+
current = path
|
|
37
|
+
while current != Path(current.anchor):
|
|
38
|
+
try:
|
|
39
|
+
info = os.lstat(current)
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
current = current.parent
|
|
42
|
+
continue
|
|
43
|
+
if stat.S_ISLNK(info.st_mode) or (os.name == "nt" and info.st_file_attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)):
|
|
44
|
+
return False
|
|
45
|
+
current = current.parent
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def classify_uninstall(target_python: Path, cogniflow_home: Path) -> dict[str, object]:
|
|
50
|
+
"""Authenticate accepted profiled authority and return only derived identity."""
|
|
51
|
+
target = Path(target_python).absolute()
|
|
52
|
+
home = Path(cogniflow_home).absolute()
|
|
53
|
+
if not target.is_absolute() or not home.is_absolute() or target == Path(target.anchor) or home == Path(home.anchor):
|
|
54
|
+
raise ValueError("uninstall paths must be absolute non-root paths")
|
|
55
|
+
root = (target.parent.parent.parent if os.name == "nt" else target.parent.parent).absolute()
|
|
56
|
+
expected = root / "environment" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
|
57
|
+
if target != expected or not target.is_file() or not _link_free(root) or not _link_free(home):
|
|
58
|
+
raise ValueError("target interpreter is not a safe accepted instance path")
|
|
59
|
+
if root == home or root in home.parents or home in root.parents:
|
|
60
|
+
raise ValueError("installation root and Cogniflow home must be disjoint")
|
|
61
|
+
|
|
62
|
+
transition = root / ".cogniflow-profile-transition.json"
|
|
63
|
+
if transition.exists() and classify_transition(root) not in {"complete-a", "complete-b"}:
|
|
64
|
+
raise ValueError("profile transition authority is not complete")
|
|
65
|
+
|
|
66
|
+
marker, marker_raw = read_marker(root / ".cogniflow-instance.json")
|
|
67
|
+
handoff = _regular_canonical(root / ".cogniflow-profile-handoff.json")
|
|
68
|
+
manifest = _regular_canonical(root / ".cogniflow-profile.json")
|
|
69
|
+
recovery_path = root / "state" / "minimum-service-plane" / "recovery-manifest.json"
|
|
70
|
+
recovery = _regular_canonical(recovery_path)
|
|
71
|
+
if marker.get("schema") != "cf.bootstrap.instance-marker.v2":
|
|
72
|
+
raise ValueError("uninstall requires an accepted profiled instance")
|
|
73
|
+
bindings = (
|
|
74
|
+
marker.get("installation_root") == str(root),
|
|
75
|
+
marker.get("environment_root") == str(root / "environment"),
|
|
76
|
+
marker.get("python_executable") == str(target),
|
|
77
|
+
handoff.get("installation_root") == str(root),
|
|
78
|
+
handoff.get("target_python") == str(target),
|
|
79
|
+
handoff.get("cogniflow_home") == str(home),
|
|
80
|
+
handoff.get("instance_id") == marker.get("instance_id"),
|
|
81
|
+
handoff.get("profile") == marker.get("profile") == manifest.get("profile"),
|
|
82
|
+
handoff.get("plan_digest") == marker.get("plan_digest") == manifest.get("plan_digest"),
|
|
83
|
+
handoff.get("bootstrap_host_artifact_set_digest") == marker.get("artifact_set_digest") == manifest.get("bootstrap_host_artifact_set_digest"),
|
|
84
|
+
handoff.get("record_digest") == manifest.get("handoff_record_digest"),
|
|
85
|
+
marker.get("recovery_manifest_path") == str(recovery_path),
|
|
86
|
+
marker.get("recovery_manifest_digest") == handoff.get("recovery_manifest_digest"),
|
|
87
|
+
recovery.get("plan_digest") == marker.get("plan_digest"),
|
|
88
|
+
recovery.get("artifact_set_digest") == marker.get("artifact_set_digest"),
|
|
89
|
+
manifest.get("host") == marker.get("installed_distributions"),
|
|
90
|
+
isinstance(manifest.get("installed"), list),
|
|
91
|
+
isinstance(manifest.get("semantic"), dict),
|
|
92
|
+
)
|
|
93
|
+
if not all(bindings):
|
|
94
|
+
raise ValueError("accepted uninstall authority is mixed or foreign")
|
|
95
|
+
accepted_digests = {
|
|
96
|
+
"plan": marker.get("plan_digest"),
|
|
97
|
+
"bootstrap_artifact_set": marker.get("artifact_set_digest"),
|
|
98
|
+
"profile": marker.get("profile_digest"),
|
|
99
|
+
"catalog": marker.get("catalog_digest"),
|
|
100
|
+
"selection": marker.get("selection_digest"),
|
|
101
|
+
"handoff": handoff.get("record_digest"),
|
|
102
|
+
"post_handoff_artifact_set": handoff.get("post_handoff_artifact_set_digest"),
|
|
103
|
+
"recovery": marker.get("recovery_manifest_digest"),
|
|
104
|
+
"profile_manifest": "sha256:" + hashlib.sha256(_canonical(manifest)).hexdigest(),
|
|
105
|
+
"marker": "sha256:" + hashlib.sha256(marker_raw).hexdigest(),
|
|
106
|
+
}
|
|
107
|
+
if any(not isinstance(value, str) or not _DIGEST.fullmatch(value) for value in accepted_digests.values()):
|
|
108
|
+
raise ValueError("accepted uninstall authority digest is invalid")
|
|
109
|
+
return {
|
|
110
|
+
"schema": "cf.bootstrap.uninstall-authority.v1",
|
|
111
|
+
"instance_id": marker["instance_id"],
|
|
112
|
+
"profile": marker["profile"],
|
|
113
|
+
"installation_root": str(root),
|
|
114
|
+
"target_python": str(target),
|
|
115
|
+
"cogniflow_home": str(home),
|
|
116
|
+
"accepted_digests": accepted_digests,
|
|
117
|
+
"installed_distributions": manifest["installed"],
|
|
118
|
+
"semantic": manifest.get("semantic"),
|
|
119
|
+
}
|