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.
@@ -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
+ }