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,11 @@
|
|
|
1
|
+
"""Stable Cogniflow instance bootstrap package."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def semantics_dir() -> Path:
|
|
7
|
+
return Path(__file__).resolve().parent / "semantics"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def semantic_files() -> tuple[Path, ...]:
|
|
11
|
+
return (semantics_dir() / "package.trig", semantics_dir() / "service.trig")
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Target-side JSON-lines handoff to the installed MCP service client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .transition_authority import classify, recover, rebind
|
|
10
|
+
from .uninstall_authority import classify_uninstall
|
|
11
|
+
|
|
12
|
+
CAPABILITY = "urn:cf:service:BootstrapInstallationHandoff"
|
|
13
|
+
PROFILED_OPERATION = "urn:cf:service:accept_profiled_bootstrap_handoff"
|
|
14
|
+
PROFILED_CAPABILITY = "urn:cf:service:ProfiledBootstrapInstallationHandoff"
|
|
15
|
+
PYPI_PROFILED_CAPABILITY = "urn:cf:service:PublicPyPIProfiledBootstrapInstallationHandoff"
|
|
16
|
+
_AUTHORITY_REQUEST = "cf.bootstrap.profile-transition-authority.request.v1"
|
|
17
|
+
_AUTHORITY_RESULT = "cf.bootstrap.profile-transition-authority.result.v1"
|
|
18
|
+
_UNINSTALL_BINDING_FIELDS = {"instance_id", "profile", "installation_root", "target_python", "cogniflow_home", "accepted_digests"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _uninstall_reauthentication(request: dict[str, object]) -> dict[str, object]:
|
|
22
|
+
if set(request) != {"schema", "target_python", "cogniflow_home", "expected"} or any(not isinstance(request.get(key), str) for key in ("target_python", "cogniflow_home")):
|
|
23
|
+
raise ValueError("invalid uninstall reauthentication request")
|
|
24
|
+
expected = request.get("expected")
|
|
25
|
+
if not isinstance(expected, dict) or set(expected) != _UNINSTALL_BINDING_FIELDS:
|
|
26
|
+
raise ValueError("invalid uninstall reauthentication binding")
|
|
27
|
+
digests = expected.get("accepted_digests")
|
|
28
|
+
if (expected.get("target_python") != request["target_python"] or expected.get("cogniflow_home") != request["cogniflow_home"]
|
|
29
|
+
or not isinstance(digests, dict) or not digests
|
|
30
|
+
or any(not isinstance(value, str) or len(value) != 71 or not value.startswith("sha256:") or any(character not in "0123456789abcdef" for character in value[7:]) for value in digests.values())):
|
|
31
|
+
raise ValueError("invalid uninstall reauthentication binding")
|
|
32
|
+
classified = classify_uninstall(Path(request["target_python"]), Path(request["cogniflow_home"]))
|
|
33
|
+
authority = {key: classified[key] for key in _UNINSTALL_BINDING_FIELDS}
|
|
34
|
+
if authority != expected:
|
|
35
|
+
raise ValueError("accepted instance no longer matches uninstall authorization")
|
|
36
|
+
return authority
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _authority_request(request: dict[str, object]) -> dict[str, object] | None:
|
|
40
|
+
if request.get("schema") != _AUTHORITY_REQUEST:
|
|
41
|
+
return None
|
|
42
|
+
operation = request.get("operation")
|
|
43
|
+
basic = {"schema", "operation", "installation_root"}
|
|
44
|
+
rebind_fields = basic | {"current", "target", "marker", "handoff", "manifest", "recovery", "target_handoff", "target_manifest", "target_recovery", "target_marker"}
|
|
45
|
+
expected = rebind_fields if operation == "rebind" else basic
|
|
46
|
+
if operation not in {"classify", "recover", "rebind"} or set(request) != expected:
|
|
47
|
+
raise ValueError("invalid profile transition authority request")
|
|
48
|
+
root_value = request["installation_root"]
|
|
49
|
+
if not isinstance(root_value, str) or not root_value or not Path(root_value).is_absolute() or Path(root_value) == Path(Path(root_value).anchor):
|
|
50
|
+
raise ValueError("invalid profile transition authority root")
|
|
51
|
+
root = Path(root_value).resolve()
|
|
52
|
+
try:
|
|
53
|
+
if operation == "classify":
|
|
54
|
+
status = classify(root)
|
|
55
|
+
elif operation == "recover":
|
|
56
|
+
status = recover(root)
|
|
57
|
+
else:
|
|
58
|
+
mappings = {name: request[name] for name in rebind_fields - basic}
|
|
59
|
+
if any(not isinstance(value, dict) for value in mappings.values()):
|
|
60
|
+
raise ValueError("profile transition authority values must be objects")
|
|
61
|
+
rebind(root, mappings["current"], mappings["target"], marker=mappings["marker"], handoff=mappings["handoff"], manifest=mappings["manifest"], recovery=mappings["recovery"], target_handoff=mappings["target_handoff"], target_manifest=mappings["target_manifest"], target_recovery=mappings["target_recovery"], target_marker=mappings["target_marker"])
|
|
62
|
+
status = "complete-b"
|
|
63
|
+
except ValueError as error:
|
|
64
|
+
return {"schema": _AUTHORITY_RESULT, "operation": operation, "error": {"code": "AUTHORITY_INVALID", "message": str(error)}}
|
|
65
|
+
return {"schema": _AUTHORITY_RESULT, "operation": operation, "status": status}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main() -> int:
|
|
69
|
+
for line in sys.stdin:
|
|
70
|
+
request = json.loads(line)
|
|
71
|
+
if not isinstance(request, dict):
|
|
72
|
+
raise ValueError("invalid handoff request")
|
|
73
|
+
if request.get("schema") == "cf.bootstrap.uninstall-reauthentication.request.v1":
|
|
74
|
+
try:
|
|
75
|
+
authority = _uninstall_reauthentication(request)
|
|
76
|
+
response = {"schema": "cf.bootstrap.uninstall-reauthentication.result.v1", "status": "accepted", "authority": authority}
|
|
77
|
+
except ValueError as error:
|
|
78
|
+
response = {"schema": "cf.bootstrap.uninstall-reauthentication.result.v1", "status": "rejected", "error": {"code": "AUTHORITY_INVALID", "message": str(error)}}
|
|
79
|
+
sys.stdout.write(json.dumps(response, sort_keys=True, separators=(",", ":")) + "\n")
|
|
80
|
+
sys.stdout.flush()
|
|
81
|
+
continue
|
|
82
|
+
if request.get("schema") == "cf.bootstrap.uninstall-authority.request.v1":
|
|
83
|
+
if set(request) != {"schema", "target_python", "cogniflow_home"} or any(not isinstance(request.get(key), str) for key in ("target_python", "cogniflow_home")):
|
|
84
|
+
raise ValueError("invalid uninstall authority request")
|
|
85
|
+
try:
|
|
86
|
+
authority = classify_uninstall(Path(request["target_python"]), Path(request["cogniflow_home"]))
|
|
87
|
+
response = {"schema": "cf.bootstrap.uninstall-authority.result.v1", "status": "accepted", "authority": authority}
|
|
88
|
+
except ValueError as error:
|
|
89
|
+
response = {"schema": "cf.bootstrap.uninstall-authority.result.v1", "status": "rejected", "error": {"code": "AUTHORITY_INVALID", "message": str(error)}}
|
|
90
|
+
sys.stdout.write(json.dumps(response, sort_keys=True, separators=(",", ":")) + "\n")
|
|
91
|
+
sys.stdout.flush()
|
|
92
|
+
continue
|
|
93
|
+
authority_result = _authority_request(request)
|
|
94
|
+
if authority_result is not None:
|
|
95
|
+
sys.stdout.write(json.dumps(authority_result, sort_keys=True, separators=(",", ":")) + "\n")
|
|
96
|
+
sys.stdout.flush()
|
|
97
|
+
continue
|
|
98
|
+
if request.get("schema") in {"cf.bootstrap.service-handoff.request.v2", "cf.bootstrap.service-handoff.request.v3"}:
|
|
99
|
+
local_fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "checkout_path", "plan_digest", "artifact_set_digest", "profile", "profile_digest", "selection_digest", "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"}
|
|
100
|
+
pypi_fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "plan_digest", "artifact_set_digest", "profile", "selection_digest", "post_handoff_artifact_root", "post_handoff_artifact_set_digest", "post_handoff_selection_digest", "post_handoff_binding_digest"}
|
|
101
|
+
fields = pypi_fields if request["schema"].endswith(".v3") else local_fields
|
|
102
|
+
if set(request) != fields:
|
|
103
|
+
raise ValueError("invalid profiled handoff request")
|
|
104
|
+
service_client = __import__("cf_" + "service_client", fromlist=["ServiceClient"])
|
|
105
|
+
client = service_client.ServiceClient.for_runtime("production", home=request["cogniflow_home"])
|
|
106
|
+
capability = PYPI_PROFILED_CAPABILITY if request["schema"].endswith(".v3") else PROFILED_CAPABILITY
|
|
107
|
+
result = client.call_service(capability, inputs={key: request[key] for key in fields if key != "schema"})
|
|
108
|
+
if result.is_error or not isinstance(result.content, dict) or result.content.get("ok") is not True:
|
|
109
|
+
raise RuntimeError(f"installer service did not accept the profiled handoff: {result.content}")
|
|
110
|
+
outputs = result.content.get("outputs", []) if isinstance(result.content, dict) else []
|
|
111
|
+
record = next((item.get("value", {}).get("data") for item in outputs if isinstance(item, dict) and item.get("key") == "accepted_handoff_record"), None)
|
|
112
|
+
if not isinstance(record, dict):
|
|
113
|
+
raise RuntimeError("handoff service did not return its persisted accepted record")
|
|
114
|
+
sys.stdout.write(json.dumps({"schema": "cf.bootstrap.service-handoff.result.v2", "status": "accepted", "profile": request["profile"], "profile_digest": request.get("profile_digest"), "selection_digest": request["selection_digest"], "handoff_receipt": record, "content": result.content}, sort_keys=True, separators=(",", ":")) + "\n")
|
|
115
|
+
sys.stdout.flush()
|
|
116
|
+
continue
|
|
117
|
+
if set(request) != {"schema", "cogniflow_home", "instance_id", "installation_root", "plan_digest", "artifact_set_digest"} or request["schema"] != "cf.bootstrap.service-handoff.request.v1":
|
|
118
|
+
raise ValueError("invalid handoff request")
|
|
119
|
+
service_client = __import__("cf_" + "service_client", fromlist=["ServiceClient"])
|
|
120
|
+
client = service_client.ServiceClient.for_runtime("production", home=request["cogniflow_home"])
|
|
121
|
+
result = client.call_service(CAPABILITY, inputs={key: request[key] for key in ("instance_id", "installation_root", "plan_digest", "artifact_set_digest")})
|
|
122
|
+
if result.is_error:
|
|
123
|
+
raise RuntimeError(str(result.content))
|
|
124
|
+
if not isinstance(result.content, dict) or set(result.content) != {"protocol", "ok", "outputs", "diagnostics", "metadata"} or result.content.get("protocol") != "cf.service.result.v1" or result.content.get("ok") is not True:
|
|
125
|
+
raise RuntimeError("service returned an invalid result envelope")
|
|
126
|
+
outputs = {item.get("key"): item.get("value", {}).get("data") for item in result.content["outputs"] if isinstance(item, dict) and isinstance(item.get("value"), dict)}
|
|
127
|
+
if outputs.get("status") != "accepted":
|
|
128
|
+
raise RuntimeError("installer service did not accept the handoff")
|
|
129
|
+
sys.stdout.write(json.dumps({"schema": "cf.bootstrap.service-handoff.result.v1", "content": result.content}, sort_keys=True, separators=(",", ":")) + "\n")
|
|
130
|
+
sys.stdout.flush()
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Validated wheel-only target environment installation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import tempfile
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .model import InstanceError, _validate_artifacts
|
|
12
|
+
from .process import run
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _entries(artifact_set: dict[str, object]) -> list[dict[str, object]]:
|
|
16
|
+
entries = artifact_set.get("artifacts")
|
|
17
|
+
if not isinstance(entries, list) or not entries:
|
|
18
|
+
raise InstanceError("ARTIFACT_INVALID", "artifact set is empty")
|
|
19
|
+
mode = "pypi" if isinstance(entries[0], dict) and "requested" in entries[0] else "local-repository"
|
|
20
|
+
_validate_artifacts(entries, mode)
|
|
21
|
+
return entries
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def requirements(wheelhouse: Path, artifact_set: dict[str, object], temp_dir: Path | None = None) -> Path:
|
|
25
|
+
lines: list[str] = []
|
|
26
|
+
for item in _entries(artifact_set):
|
|
27
|
+
wheel = wheelhouse / item["filename"]
|
|
28
|
+
if wheel.is_symlink() or not wheel.is_file() or wheel.stat().st_size != item["size_bytes"] or hashlib.sha256(wheel.read_bytes()).hexdigest() != item["sha256"]:
|
|
29
|
+
raise InstanceError("ARTIFACT_INVALID", f"wheel validation failed: {item['filename']}")
|
|
30
|
+
lines.append(f"{item['normalized_distribution']}=={item['version']} --hash=sha256:{item['sha256']}")
|
|
31
|
+
fd, name = tempfile.mkstemp(prefix="cf-bootstrap-", suffix=".lock", dir=str(temp_dir) if temp_dir else None)
|
|
32
|
+
try:
|
|
33
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as stream:
|
|
34
|
+
stream.write("\n".join(lines) + "\n")
|
|
35
|
+
stream.flush()
|
|
36
|
+
os.fsync(stream.fileno())
|
|
37
|
+
except BaseException:
|
|
38
|
+
try:
|
|
39
|
+
os.close(fd)
|
|
40
|
+
except OSError:
|
|
41
|
+
pass
|
|
42
|
+
Path(name).unlink(missing_ok=True)
|
|
43
|
+
raise
|
|
44
|
+
return Path(name)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_environment(environment: Path, temp_root: Path | None = None) -> Path:
|
|
48
|
+
run([sys.executable, "-m", "venv", "--copies", str(environment)], timeout=120, temp_root=temp_root)
|
|
49
|
+
executable = environment / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python")
|
|
50
|
+
if not executable.is_file():
|
|
51
|
+
raise InstanceError("VENV_INVALID", "venv interpreter was not created")
|
|
52
|
+
return executable
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def install(executable: Path, wheelhouse: Path, artifact_set: dict[str, object], temp_dir: Path | None = None) -> None:
|
|
56
|
+
lock = requirements(wheelhouse, artifact_set, temp_dir)
|
|
57
|
+
try:
|
|
58
|
+
count_file = os.environ.get("CF_BOOTSTRAP_TEST_PIP_COUNT_FILE")
|
|
59
|
+
if count_file:
|
|
60
|
+
count = Path(count_file)
|
|
61
|
+
count.write_text(str(int(count.read_text(encoding="utf-8") if count.exists() else "0") + 1), encoding="utf-8")
|
|
62
|
+
run([str(executable), "-m", "pip", "--isolated", "install", "--no-index", "--find-links", str(wheelhouse), "--require-hashes", "--no-deps", "--no-cache-dir", "--disable-pip-version-check", "--no-input", "--requirement", str(lock)], timeout=300, temp_root=temp_dir)
|
|
63
|
+
finally:
|
|
64
|
+
lock.unlink(missing_ok=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_LAUNCHER_CHECK = r'''
|
|
68
|
+
from importlib import metadata
|
|
69
|
+
from pathlib import Path
|
|
70
|
+
from pip._vendor.distlib.scripts import ScriptMaker
|
|
71
|
+
import os
|
|
72
|
+
import sys
|
|
73
|
+
|
|
74
|
+
scripts = Path(sys.argv[1]).resolve()
|
|
75
|
+
final_executable = Path(sys.argv[2]).resolve()
|
|
76
|
+
staging_executable = Path(sys.argv[3]).resolve()
|
|
77
|
+
entries = []
|
|
78
|
+
names = set()
|
|
79
|
+
for distribution in metadata.distributions():
|
|
80
|
+
for entry in distribution.entry_points:
|
|
81
|
+
if entry.group != "console_scripts":
|
|
82
|
+
continue
|
|
83
|
+
if entry.name in names:
|
|
84
|
+
raise RuntimeError("duplicate console script: " + entry.name)
|
|
85
|
+
names.add(entry.name)
|
|
86
|
+
entries.append(entry.name + " = " + entry.value)
|
|
87
|
+
maker = ScriptMaker(str(scripts), str(scripts), add_launchers=True)
|
|
88
|
+
maker.executable = str(final_executable)
|
|
89
|
+
maker.clobber = True
|
|
90
|
+
maker.variants = {""}
|
|
91
|
+
written = maker.make_multiple(sorted(entries))
|
|
92
|
+
written_names = {Path(item).stem for item in written}
|
|
93
|
+
if written_names != names:
|
|
94
|
+
raise RuntimeError("launcher set differs from console-script metadata")
|
|
95
|
+
final_bytes = str(final_executable).encode()
|
|
96
|
+
staging_bytes = str(staging_executable).encode()
|
|
97
|
+
for item in written:
|
|
98
|
+
path = Path(item)
|
|
99
|
+
data = path.read_bytes()
|
|
100
|
+
if staging_bytes in data:
|
|
101
|
+
raise RuntimeError("launcher contains staging interpreter path: " + str(path))
|
|
102
|
+
if os.name == "nt":
|
|
103
|
+
if final_bytes not in data and final_bytes.replace(b"\\", b"/") not in data:
|
|
104
|
+
raise RuntimeError("Windows launcher is not bound to final interpreter: " + str(path))
|
|
105
|
+
elif data.splitlines()[0] != b"#!" + str(final_executable).encode():
|
|
106
|
+
raise RuntimeError("POSIX launcher is not bound to final interpreter: " + str(path))
|
|
107
|
+
'''
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def prepare_launchers(staging_executable: Path, final_executable: Path) -> None:
|
|
111
|
+
"""Generate deterministic final-path-bound launchers inside staging."""
|
|
112
|
+
scripts = staging_executable.parent
|
|
113
|
+
run([str(staging_executable), "-c", _LAUNCHER_CHECK, str(scripts), str(final_executable), str(staging_executable)], timeout=60)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def remove_build_tools(executable: Path) -> None:
|
|
117
|
+
# pip is only an installation tool; leaving it would violate the exact
|
|
118
|
+
# validated-distribution inventory of the non-editable instance.
|
|
119
|
+
run([str(executable), "-m", "pip", "--isolated", "uninstall", "--yes", "pip", "setuptools", "wheel"], timeout=60)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""External instance layout validation and atomic marker publication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import tempfile
|
|
9
|
+
import re
|
|
10
|
+
import stat
|
|
11
|
+
|
|
12
|
+
from .model import InstanceError, MARKER_SCHEMA, MARKER_SCHEMA_V2, _IDENTIFIER, _VERSION, canonical_json, normalize_distribution
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_root(root: Path, checkout: Path | None, wheelhouse: Path, home: Path) -> None:
|
|
16
|
+
root = _lexical_absolute(root, "installation_root")
|
|
17
|
+
wheelhouse = _lexical_absolute(wheelhouse, "wheelhouse_path")
|
|
18
|
+
home = _lexical_absolute(home, "cogniflow_home")
|
|
19
|
+
checkout = _lexical_absolute(checkout, "checkout_path") if checkout is not None else None
|
|
20
|
+
_assert_link_free(root, "installation_root")
|
|
21
|
+
_assert_link_free(root.parent, "installation_root parent")
|
|
22
|
+
_assert_link_free(wheelhouse, "wheelhouse_path")
|
|
23
|
+
_assert_link_free(wheelhouse.parent, "wheelhouse parent")
|
|
24
|
+
_assert_link_free(home, "cogniflow_home")
|
|
25
|
+
_assert_link_free(home.parent, "cogniflow_home parent")
|
|
26
|
+
if checkout is not None:
|
|
27
|
+
_assert_link_free(checkout, "checkout_path")
|
|
28
|
+
_assert_link_free(checkout.parent, "checkout parent")
|
|
29
|
+
if root == Path(root.anchor):
|
|
30
|
+
raise InstanceError("ROOT_INVALID", "installation root must be a real non-root directory")
|
|
31
|
+
root_canonical = root.resolve(strict=False)
|
|
32
|
+
wheelhouse_canonical = wheelhouse.resolve(strict=False)
|
|
33
|
+
home_canonical = home.resolve(strict=False)
|
|
34
|
+
checkout_canonical = checkout.resolve(strict=False) if checkout is not None else None
|
|
35
|
+
if checkout_canonical is not None and (_under(wheelhouse_canonical, checkout_canonical) or _under(checkout_canonical, wheelhouse_canonical)):
|
|
36
|
+
raise InstanceError("ROOT_INVALID", "wheelhouse and checkout must be disjoint")
|
|
37
|
+
if root_canonical == Path(root_canonical.anchor):
|
|
38
|
+
raise InstanceError("ROOT_INVALID", "installation root must be a real non-root directory")
|
|
39
|
+
for protected, label in ((home_canonical, "Cogniflow home"), (wheelhouse_canonical, "artifact output"), (checkout_canonical, "checkout")):
|
|
40
|
+
if protected is not None and (_under(root_canonical, protected) or _under(protected, root_canonical)):
|
|
41
|
+
raise InstanceError("ROOT_INVALID", f"installation root must be disjoint from {label}")
|
|
42
|
+
if root.exists() and not root.is_dir():
|
|
43
|
+
raise InstanceError("ROOT_INVALID", "installation root is not a directory")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _lexical_absolute(path: Path | None, label: str) -> Path:
|
|
47
|
+
if path is None:
|
|
48
|
+
raise InstanceError("SCHEMA_INVALID", f"{label} is missing")
|
|
49
|
+
value = os.fspath(path)
|
|
50
|
+
if not isinstance(value, str) or not value or value != value.strip() or "\x00" in value or any(ord(char) < 32 for char in value) or ".." in Path(value).parts:
|
|
51
|
+
raise InstanceError("SCHEMA_INVALID", f"{label} is unsafe")
|
|
52
|
+
lexical = Path(os.path.abspath(value))
|
|
53
|
+
if not lexical.is_absolute() or lexical == Path(lexical.anchor):
|
|
54
|
+
raise InstanceError("SCHEMA_INVALID", f"{label} must be an absolute non-root path")
|
|
55
|
+
return lexical
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _components(path: Path) -> tuple[Path, ...]:
|
|
59
|
+
current = path
|
|
60
|
+
result: list[Path] = []
|
|
61
|
+
while current != Path(current.anchor):
|
|
62
|
+
result.append(current)
|
|
63
|
+
current = current.parent
|
|
64
|
+
result.reverse()
|
|
65
|
+
return tuple(result)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_redirecting_directory(path: Path) -> bool:
|
|
69
|
+
if path.is_symlink():
|
|
70
|
+
return True
|
|
71
|
+
if os.name == "nt" and path.exists():
|
|
72
|
+
try:
|
|
73
|
+
attributes = os.lstat(path).st_file_attributes
|
|
74
|
+
except OSError as error:
|
|
75
|
+
raise InstanceError("ROOT_INVALID", f"cannot inspect path component: {path}") from error
|
|
76
|
+
return bool(attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT)
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _assert_link_free(path: Path, label: str) -> None:
|
|
81
|
+
for component in _components(path):
|
|
82
|
+
if component.is_symlink() or (component.exists() and _is_redirecting_directory(component)):
|
|
83
|
+
raise InstanceError("ROOT_INVALID", f"{label} contains a symbolic link or redirecting reparse point")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _under(left: Path, right: Path) -> bool:
|
|
87
|
+
try:
|
|
88
|
+
left.relative_to(right)
|
|
89
|
+
return True
|
|
90
|
+
except ValueError:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def atomic_write(path: Path, data: bytes) -> None:
|
|
95
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
|
97
|
+
try:
|
|
98
|
+
with os.fdopen(fd, "wb") as stream:
|
|
99
|
+
stream.write(data)
|
|
100
|
+
stream.flush()
|
|
101
|
+
os.fsync(stream.fileno())
|
|
102
|
+
Path(temporary).replace(path)
|
|
103
|
+
except OSError:
|
|
104
|
+
try:
|
|
105
|
+
Path(temporary).unlink()
|
|
106
|
+
except OSError:
|
|
107
|
+
pass
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def read_marker(path: Path) -> tuple[dict[str, object], bytes]:
|
|
112
|
+
expected_v1 = {"schema", "instance_id", "installation_root", "environment_root", "python_executable", "plan_digest", "artifact_set_digest", "installed_distributions", "state"}
|
|
113
|
+
expected_v2 = {"schema", "instance_id", "installation_root", "environment_root", "python_executable", "profile", "plan_digest", "profile_digest", "catalog_digest", "selection_digest", "artifact_set_digest", "installed_distributions", "recovery_manifest_path", "recovery_manifest_digest"}
|
|
114
|
+
try:
|
|
115
|
+
before = os.lstat(path)
|
|
116
|
+
if not stat.S_ISREG(before.st_mode) or path.is_symlink() or (os.name != "nt" and before.st_mode & 0o077):
|
|
117
|
+
raise OSError("marker is not a regular safe file")
|
|
118
|
+
descriptor = os.open(os.fspath(path), os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
119
|
+
try:
|
|
120
|
+
raw = os.read(descriptor, 16 * 1024 * 1024)
|
|
121
|
+
finally:
|
|
122
|
+
os.close(descriptor)
|
|
123
|
+
value = json.loads(raw)
|
|
124
|
+
except (OSError, ValueError) as error:
|
|
125
|
+
raise InstanceError("MARKER_INVALID", "instance marker is unreadable") from error
|
|
126
|
+
schema = value.get("schema") if isinstance(value, dict) else None
|
|
127
|
+
expected = expected_v2 if schema == MARKER_SCHEMA_V2 else expected_v1
|
|
128
|
+
if not isinstance(value, dict) or set(value) != expected or (schema == MARKER_SCHEMA and value.get("state") != "ready") or schema not in {MARKER_SCHEMA, MARKER_SCHEMA_V2} or raw != canonical_json(value):
|
|
129
|
+
raise InstanceError("MARKER_INVALID", "instance marker is not canonical")
|
|
130
|
+
for key in ("instance_id",):
|
|
131
|
+
if not isinstance(value[key], str) or not _IDENTIFIER.fullmatch(value[key]):
|
|
132
|
+
raise InstanceError("MARKER_INVALID", f"marker {key} is invalid")
|
|
133
|
+
for key in ("installation_root", "environment_root", "python_executable"):
|
|
134
|
+
if not isinstance(value[key], str):
|
|
135
|
+
raise InstanceError("MARKER_INVALID", f"marker {key} is invalid")
|
|
136
|
+
marker_path = Path(value[key])
|
|
137
|
+
if not marker_path.is_absolute() or ".." in marker_path.parts or marker_path == Path(marker_path.anchor):
|
|
138
|
+
raise InstanceError("MARKER_INVALID", f"marker {key} is invalid")
|
|
139
|
+
for key in ("plan_digest", "artifact_set_digest"):
|
|
140
|
+
if not isinstance(value[key], str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", value[key]):
|
|
141
|
+
raise InstanceError("MARKER_INVALID", f"marker {key} is invalid")
|
|
142
|
+
if schema == MARKER_SCHEMA_V2:
|
|
143
|
+
if not isinstance(value["profile"], str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9+.-]*:[^\s]+", value["profile"]):
|
|
144
|
+
raise InstanceError("MARKER_INVALID", "marker profile is invalid")
|
|
145
|
+
for key in ("profile_digest", "catalog_digest", "selection_digest", "recovery_manifest_digest"):
|
|
146
|
+
if not isinstance(value[key], str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", value[key]):
|
|
147
|
+
raise InstanceError("MARKER_INVALID", f"marker {key} is invalid")
|
|
148
|
+
installed = value["installed_distributions"]
|
|
149
|
+
if not isinstance(installed, list) or installed != sorted(installed, key=lambda item: item.get("name", "").lower() if isinstance(item, dict) else ""):
|
|
150
|
+
raise InstanceError("MARKER_INVALID", "marker inventory is not sorted")
|
|
151
|
+
seen: set[str] = set()
|
|
152
|
+
for item in installed:
|
|
153
|
+
if not isinstance(item, dict) or set(item) != {"name", "version"} or not isinstance(item["name"], str) or not isinstance(item["version"], str) or not _IDENTIFIER.fullmatch(item["name"]) or not _VERSION.fullmatch(item["version"]) or normalize_distribution(item["name"]) in seen:
|
|
154
|
+
raise InstanceError("MARKER_INVALID", "marker inventory is invalid")
|
|
155
|
+
seen.add(normalize_distribution(item["name"]))
|
|
156
|
+
return value, raw
|