cf-bootstrap-orchestrator 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_orchestrator/__init__.py +11 -0
- cf_bootstrap_orchestrator/cli.py +51 -0
- cf_bootstrap_orchestrator/discovery.py +74 -0
- cf_bootstrap_orchestrator/model.py +261 -0
- cf_bootstrap_orchestrator/orchestrator.py +172 -0
- cf_bootstrap_orchestrator/process.py +66 -0
- cf_bootstrap_orchestrator/semantics/package.trig +13 -0
- cf_bootstrap_orchestrator-0.1.8.dist-info/METADATA +6 -0
- cf_bootstrap_orchestrator-0.1.8.dist-info/RECORD +11 -0
- cf_bootstrap_orchestrator-0.1.8.dist-info/WHEEL +4 -0
- cf_bootstrap_orchestrator-0.1.8.dist-info/entry_points.txt +8 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Standard-library-only profile installation orchestration boundary."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
PACKAGE_DISTRIBUTION = "cf-bootstrap-orchestrator"
|
|
6
|
+
PACKAGE_VERSION = "0.1.8"
|
|
7
|
+
__version__ = PACKAGE_VERSION
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def semantic_files() -> tuple[Path, ...]:
|
|
11
|
+
return (Path(__file__).resolve().parent / "semantics" / "package.trig",)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""JSON-lines process entry point for the installation orchestrator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .model import OrchestrationError, PROTOCOL, canonical, validate_result
|
|
10
|
+
from .orchestrator import run
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _response(identifier: object, **body: object) -> dict[str, object]:
|
|
14
|
+
return {"jsonrpc": "2.0", "protocol": PROTOCOL, "id": identifier, **body}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def handle(value: object) -> dict[str, object]:
|
|
18
|
+
identifier = value.get("id") if isinstance(value, dict) else None
|
|
19
|
+
try:
|
|
20
|
+
if not isinstance(value, dict) or set(value) != {"jsonrpc", "protocol", "id", "method", "params"} or value.get("jsonrpc") != "2.0" or value.get("protocol") != PROTOCOL or value.get("method") != "installation/run" or not isinstance(value.get("id"), (str, int)) or isinstance(value.get("id"), bool):
|
|
21
|
+
raise OrchestrationError("INVALID_ENVELOPE", "orchestrator request envelope is invalid")
|
|
22
|
+
params = value["params"]
|
|
23
|
+
result = run(params)
|
|
24
|
+
bootstrap = params.get("bootstrap_request") if isinstance(params, dict) else None
|
|
25
|
+
profile = bootstrap.get("profile") if isinstance(bootstrap, dict) else None
|
|
26
|
+
if not isinstance(profile, str):
|
|
27
|
+
raise OrchestrationError("SCHEMA_INVALID", "orchestration profile is invalid")
|
|
28
|
+
return _response(identifier, result=validate_result(result, params))
|
|
29
|
+
except OrchestrationError as error:
|
|
30
|
+
return _response(identifier, error={"code": error.code, "message": str(error)})
|
|
31
|
+
except Exception:
|
|
32
|
+
if os.environ.get("CF_BOOTSTRAP_ORCHESTRATOR_DEBUG") == "1":
|
|
33
|
+
raise
|
|
34
|
+
return _response(identifier, error={"code": "INTERNAL_ERROR", "message": "install orchestration failed"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> int:
|
|
38
|
+
for line in sys.stdin:
|
|
39
|
+
try:
|
|
40
|
+
value = json.loads(line)
|
|
41
|
+
except json.JSONDecodeError:
|
|
42
|
+
response = _response(None, error={"code": "MALFORMED_JSON", "message": "request must be valid JSON"})
|
|
43
|
+
else:
|
|
44
|
+
response = handle(value)
|
|
45
|
+
sys.stdout.buffer.write(canonical(response))
|
|
46
|
+
sys.stdout.buffer.flush()
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Metadata-only discovery for lower-level provider console scripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from importlib import metadata
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DiscoveryError(RuntimeError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Script:
|
|
18
|
+
name: str
|
|
19
|
+
distribution: str
|
|
20
|
+
target: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_SAFE = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?\Z")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _dist(entry: object) -> str:
|
|
27
|
+
value = getattr(getattr(entry, "dist", None), "metadata", {}).get("Name")
|
|
28
|
+
if not isinstance(value, str) or not value:
|
|
29
|
+
raise DiscoveryError("provider metadata has no distribution name")
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _entries(group: str) -> list[object]:
|
|
34
|
+
return [entry for dist in metadata.distributions() for entry in dist.entry_points if entry.group == group]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def discover(group: str, name: str | None = None) -> tuple[Script, ...]:
|
|
38
|
+
custom = [item for item in _entries(group) if name is None or item.name == name]
|
|
39
|
+
console = _entries("console_scripts")
|
|
40
|
+
result: list[Script] = []
|
|
41
|
+
seen: set[str] = set()
|
|
42
|
+
for entry in custom:
|
|
43
|
+
if not isinstance(entry.name, str) or not _SAFE.fullmatch(entry.name) or entry.name in seen:
|
|
44
|
+
raise DiscoveryError("provider metadata contains an unsafe or duplicate name")
|
|
45
|
+
distribution = _dist(entry)
|
|
46
|
+
matches = [item for item in console if _dist(item) == distribution and item.value == entry.value]
|
|
47
|
+
if len(matches) != 1:
|
|
48
|
+
raise DiscoveryError("provider metadata has no unique matching console script")
|
|
49
|
+
result.append(Script(matches[0].name, distribution, entry.value))
|
|
50
|
+
seen.add(entry.name)
|
|
51
|
+
return tuple(sorted(result, key=lambda item: (item.name, item.distribution)))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def one(group: str, name: str | None = None) -> Script:
|
|
55
|
+
values = discover(group, name)
|
|
56
|
+
if len(values) != 1:
|
|
57
|
+
raise DiscoveryError("exactly one provider is required")
|
|
58
|
+
return values[0]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def executable(script: Script, scripts: Path | None = None) -> Path:
|
|
62
|
+
directory = (scripts or Path(sys.executable).parent).resolve()
|
|
63
|
+
candidates = [directory / script.name]
|
|
64
|
+
if sys.platform == "win32":
|
|
65
|
+
candidates.insert(0, directory / f"{script.name}.exe")
|
|
66
|
+
for candidate in candidates:
|
|
67
|
+
if candidate.is_file() and not candidate.is_symlink():
|
|
68
|
+
resolved = candidate.resolve()
|
|
69
|
+
try:
|
|
70
|
+
resolved.relative_to(directory)
|
|
71
|
+
except ValueError as error:
|
|
72
|
+
raise DiscoveryError("provider executable escaped Scripts") from error
|
|
73
|
+
return resolved
|
|
74
|
+
raise DiscoveryError("provider executable is not adjacent to the active interpreter")
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Strict public orchestration request and result validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib.resources
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
REQUEST_SCHEMA = "cf.bootstrap.install-orchestration.request.v1"
|
|
14
|
+
PROTOCOL = "cf.bootstrap.install-orchestrator.v1"
|
|
15
|
+
RESULT_SCHEMA = "cogniflow.install.result.v3"
|
|
16
|
+
_IRI = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:[^\s]+\Z")
|
|
17
|
+
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OrchestrationError(ValueError):
|
|
21
|
+
def __init__(self, code: str, message: str):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.code = code
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def canonical(value: object) -> bytes:
|
|
27
|
+
return (json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def digest(value: object) -> str:
|
|
31
|
+
return "sha256:" + hashlib.sha256(canonical(value)).hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _path(value: object, label: str) -> str:
|
|
35
|
+
if not isinstance(value, str) or not value or value != value.strip() or "\x00" in value:
|
|
36
|
+
raise OrchestrationError("SCHEMA_INVALID", f"{label} is invalid")
|
|
37
|
+
path = Path(value)
|
|
38
|
+
if not path.is_absolute() or path == Path(path.anchor) or ".." in path.parts:
|
|
39
|
+
raise OrchestrationError("SCHEMA_INVALID", f"{label} must be an absolute non-root path")
|
|
40
|
+
return str(path)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _under(left: str, right: str) -> bool:
|
|
44
|
+
try:
|
|
45
|
+
Path(left).resolve(strict=False).relative_to(Path(right).resolve(strict=False))
|
|
46
|
+
return True
|
|
47
|
+
except ValueError:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _normal(name: object) -> str:
|
|
52
|
+
return re.sub(r"[-_.]+", "-", str(name)).lower()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _pypi_resolution_digest(resolver: dict[str, Any], environment: dict[str, Any], artifacts: list[dict[str, Any]]) -> str:
|
|
56
|
+
selected = [{key: item[key] for key in ("distribution", "normalized_distribution", "version", "filename", "sha256", "requested")} for item in sorted(artifacts, key=lambda item: (item["normalized_distribution"], item["version"], item["filename"]))]
|
|
57
|
+
value = {"pip_report_version": resolver["pip_report_version"], "pip_version": resolver["pip_version"], "environment": environment, "artifacts": selected}
|
|
58
|
+
raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
|
59
|
+
return "sha256:" + hashlib.sha256(raw).hexdigest()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_request(value: object) -> dict[str, Any]:
|
|
63
|
+
if not isinstance(value, dict) or value.get("schema") != REQUEST_SCHEMA:
|
|
64
|
+
raise OrchestrationError("SCHEMA_INVALID", "orchestration request fields are invalid")
|
|
65
|
+
bootstrap = value.get("bootstrap_request")
|
|
66
|
+
bootstrap_fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "profile"}
|
|
67
|
+
if not isinstance(bootstrap, dict) or set(bootstrap) != bootstrap_fields or bootstrap.get("schema") != "cf.bootstrap.request.v2":
|
|
68
|
+
raise OrchestrationError("SCHEMA_INVALID", "v2 bootstrap request is invalid")
|
|
69
|
+
profile = bootstrap.get("profile")
|
|
70
|
+
if not isinstance(profile, str) or not _IRI.fullmatch(profile):
|
|
71
|
+
raise OrchestrationError("SCHEMA_INVALID", "profile must be an explicit absolute IRI")
|
|
72
|
+
instance_id = bootstrap.get("instance_id")
|
|
73
|
+
if not isinstance(instance_id, str) or not re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?\Z", instance_id) or len(instance_id) > 64:
|
|
74
|
+
raise OrchestrationError("SCHEMA_INVALID", "instance_id is invalid")
|
|
75
|
+
paths = {key: _path(bootstrap[key], key) for key in ("installation_root", "cogniflow_home")}
|
|
76
|
+
paths["artifact_output_root"] = _path(value.get("artifact_output_root"), "artifact_output_root")
|
|
77
|
+
source = bootstrap["artifact_source"]
|
|
78
|
+
mode = source.get("mode") if isinstance(source, dict) else None
|
|
79
|
+
if mode == "local-repository":
|
|
80
|
+
fields = {"schema", "bootstrap_request", "artifact_output_root", "external_wheelhouse_path"}
|
|
81
|
+
if set(value) != fields or set(source) != {"mode", "checkout_path"}:
|
|
82
|
+
raise OrchestrationError("SCHEMA_INVALID", "local-repository fields are invalid")
|
|
83
|
+
paths["checkout_path"] = _path(source["checkout_path"], "checkout_path")
|
|
84
|
+
paths["external_wheelhouse_path"] = _path(value["external_wheelhouse_path"], "external_wheelhouse_path")
|
|
85
|
+
protected = ("installation_root", "cogniflow_home", "checkout_path")
|
|
86
|
+
for key in ("artifact_output_root", "external_wheelhouse_path"):
|
|
87
|
+
if any(_under(paths[key], paths[other]) or _under(paths[other], paths[key]) for other in protected):
|
|
88
|
+
raise OrchestrationError("ROOT_INVALID", f"{key} overlaps a protected root")
|
|
89
|
+
if _under(paths["artifact_output_root"], paths["external_wheelhouse_path"]) or _under(paths["external_wheelhouse_path"], paths["artifact_output_root"]):
|
|
90
|
+
raise OrchestrationError("ROOT_INVALID", "artifact output and external wheelhouse must be disjoint")
|
|
91
|
+
elif mode == "pypi":
|
|
92
|
+
fields = {"schema", "bootstrap_request", "artifact_output_root", "index_url"}
|
|
93
|
+
if set(value) != fields or set(source) != {"mode", "package", "version"}:
|
|
94
|
+
raise OrchestrationError("SCHEMA_INVALID", "pypi fields are invalid")
|
|
95
|
+
if not isinstance(source["package"], str) or not re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?\Z", source["package"]):
|
|
96
|
+
raise OrchestrationError("SCHEMA_INVALID", "pypi package is invalid")
|
|
97
|
+
if not isinstance(source["version"], str) or not re.fullmatch(r"[0-9]+(?:\.[0-9]+){0,2}(?:[-+][A-Za-z0-9.-]+)?\Z", source["version"]):
|
|
98
|
+
raise OrchestrationError("SCHEMA_INVALID", "pypi version is invalid")
|
|
99
|
+
if value["index_url"] != "https://pypi.org/simple":
|
|
100
|
+
raise OrchestrationError("SCHEMA_INVALID", "pypi index must be the public index")
|
|
101
|
+
if any(_under(paths["artifact_output_root"], paths[key]) or _under(paths[key], paths["artifact_output_root"]) for key in ("installation_root", "cogniflow_home")):
|
|
102
|
+
raise OrchestrationError("ROOT_INVALID", "artifact output overlaps a protected root")
|
|
103
|
+
else:
|
|
104
|
+
raise OrchestrationError("PROFILED_SOURCE_UNAVAILABLE", "artifact source mode is unsupported")
|
|
105
|
+
if paths["installation_root"] == paths["cogniflow_home"] or _under(paths["installation_root"], paths["cogniflow_home"]) or _under(paths["cogniflow_home"], paths["installation_root"]):
|
|
106
|
+
raise OrchestrationError("ROOT_INVALID", "installation root and Cogniflow home must be disjoint")
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_result(value: object, request: object) -> dict[str, Any]:
|
|
111
|
+
request = validate_request(request)
|
|
112
|
+
bootstrap = request["bootstrap_request"]
|
|
113
|
+
profile = bootstrap["profile"]
|
|
114
|
+
fields = {"schema", "status", "profile", "selection_digest", "plan", "artifact_resolution", "post_handoff_resolution", "instance", "handoff", "profile_installation"}
|
|
115
|
+
if not isinstance(value, dict) or set(value) != fields or value.get("schema") != RESULT_SCHEMA or value.get("status") not in {"ready", "unchanged"} or value.get("profile") != profile or not isinstance(value.get("selection_digest"), str) or not _DIGEST.fullmatch(value["selection_digest"]):
|
|
116
|
+
raise OrchestrationError("RESULT_INVALID", "orchestrator result is invalid")
|
|
117
|
+
plan = value["plan"]
|
|
118
|
+
plan_fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "profile", "stages"}
|
|
119
|
+
if (not isinstance(plan, dict) or set(plan) != plan_fields or plan.get("schema") != "cf.bootstrap.plan.v2"
|
|
120
|
+
or any(plan.get(key) != bootstrap.get(key) for key in ("instance_id", "installation_root", "cogniflow_home", "artifact_source", "profile"))):
|
|
121
|
+
raise OrchestrationError("RESULT_INVALID", "result plan is not bound to the submitted request")
|
|
122
|
+
resolution = value["artifact_resolution"]
|
|
123
|
+
post_resolution = value["post_handoff_resolution"]
|
|
124
|
+
if not isinstance(resolution, dict) or not isinstance(resolution.get("artifact_set"), dict) or not isinstance(post_resolution, dict) or not isinstance(post_resolution.get("artifact_set"), dict):
|
|
125
|
+
raise OrchestrationError("RESULT_INVALID", "result resolution is invalid")
|
|
126
|
+
artifact_set = resolution["artifact_set"]
|
|
127
|
+
post_set = post_resolution["artifact_set"]
|
|
128
|
+
mode = plan["artifact_source"].get("mode")
|
|
129
|
+
try:
|
|
130
|
+
release_bytes = importlib.resources.files("cogniflow").joinpath("release-profile.json").read_bytes()
|
|
131
|
+
release_profile = json.loads(release_bytes)
|
|
132
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
133
|
+
raise OrchestrationError("RESULT_INVALID", "authoritative release profile is unavailable") from error
|
|
134
|
+
release_distributions = release_profile.get("distributions") if isinstance(release_profile, dict) else None
|
|
135
|
+
if not isinstance(release_distributions, list):
|
|
136
|
+
raise OrchestrationError("RESULT_INVALID", "release distribution authority is invalid")
|
|
137
|
+
expected_first_party = {_normal(item.get("name")): str(item.get("version")) for item in release_distributions if isinstance(item, dict)}
|
|
138
|
+
if len(expected_first_party) != len(release_distributions):
|
|
139
|
+
raise OrchestrationError("RESULT_INVALID", "release distribution authority is ambiguous")
|
|
140
|
+
if mode == "local-repository":
|
|
141
|
+
if (resolution.get("schema") != "cf.bootstrap.local-artifact-resolution.v2" or resolution.get("wheelhouse_path") != request["artifact_output_root"]
|
|
142
|
+
or artifact_set.get("schema") != "cf.bootstrap.artifact-set.v2" or artifact_set.get("profile") != profile
|
|
143
|
+
or artifact_set.get("plan_digest") != digest(plan) or artifact_set.get("selection_digest") != value["selection_digest"]
|
|
144
|
+
or post_set.get("plan_digest") != digest(plan) or post_set.get("profile") != profile):
|
|
145
|
+
raise OrchestrationError("RESULT_INVALID", "result local selection binding is invalid")
|
|
146
|
+
elif mode == "pypi":
|
|
147
|
+
source = plan["artifact_source"]
|
|
148
|
+
root = artifact_set.get("root_requirement")
|
|
149
|
+
resolver = artifact_set.get("resolver")
|
|
150
|
+
environment = artifact_set.get("target_environment")
|
|
151
|
+
artifacts = artifact_set.get("artifacts")
|
|
152
|
+
artifact_set_fields = {"schema", "source_mode", "root_requirement", "resolver", "target_environment", "resolution_digest", "lock_sha256", "artifacts"}
|
|
153
|
+
artifact_fields = {"distribution", "normalized_distribution", "version", "filename", "sha256", "size_bytes", "requested"}
|
|
154
|
+
environment_fields = {"implementation_name", "implementation_version", "os_name", "platform_machine", "platform_system", "python_full_version", "python_version", "platform_python_implementation", "sys_platform"}
|
|
155
|
+
if (set(resolution) != {"schema", "wheelhouse_path", "artifact_set", "lock_filename"} or resolution.get("schema") != "cf.bootstrap.pypi-artifact-resolution.v1"
|
|
156
|
+
or resolution.get("wheelhouse_path") != request["artifact_output_root"] or resolution.get("lock_filename") != "requirements.lock"
|
|
157
|
+
or set(artifact_set) != artifact_set_fields or artifact_set.get("schema") != "cf.bootstrap.pypi-artifact-set.v1" or artifact_set.get("source_mode") != "pypi"
|
|
158
|
+
or not isinstance(root, dict) or set(root) != {"distribution", "normalized_distribution", "version"}
|
|
159
|
+
or root.get("distribution") != source.get("package") or root.get("version") != source.get("version") or root.get("normalized_distribution") != _normal(source.get("package"))
|
|
160
|
+
or not isinstance(resolver, dict) or set(resolver) != {"pip_report_version", "pip_version"} or resolver.get("pip_report_version") != "1" or not isinstance(resolver.get("pip_version"), str) or not resolver["pip_version"]
|
|
161
|
+
or not isinstance(environment, dict) or set(environment) != environment_fields or any(not isinstance(item, str) or not item for item in environment.values())
|
|
162
|
+
or not isinstance(artifact_set.get("resolution_digest"), str) or not _DIGEST.fullmatch(artifact_set["resolution_digest"])
|
|
163
|
+
or not isinstance(artifact_set.get("lock_sha256"), str) or not re.fullmatch(r"[0-9a-f]{64}", artifact_set["lock_sha256"])
|
|
164
|
+
or not isinstance(artifacts, list) or not artifacts or any(not isinstance(item, dict) or set(item) != artifact_fields for item in artifacts)):
|
|
165
|
+
raise OrchestrationError("RESULT_INVALID", "result PyPI selection binding is invalid")
|
|
166
|
+
normalized: set[str] = set()
|
|
167
|
+
filenames: set[str] = set()
|
|
168
|
+
requested: list[dict[str, Any]] = []
|
|
169
|
+
for item in artifacts:
|
|
170
|
+
filename = item["filename"]
|
|
171
|
+
wheel = filename[:-4].split("-") if isinstance(filename, str) and filename.endswith(".whl") else []
|
|
172
|
+
name = _normal(item["distribution"])
|
|
173
|
+
if (not isinstance(item["distribution"], str) or not item["distribution"] or item["normalized_distribution"] != name
|
|
174
|
+
or not isinstance(item["version"], str) or not item["version"] or len(wheel) < 5 or _normal(wheel[0]) != name or wheel[1] != item["version"]
|
|
175
|
+
or Path(filename).name != filename or filename in filenames or name in normalized
|
|
176
|
+
or not re.fullmatch(r"[0-9a-f]{64}", str(item["sha256"])) or type(item["size_bytes"]) is not int or item["size_bytes"] <= 0 or type(item["requested"]) is not bool):
|
|
177
|
+
raise OrchestrationError("RESULT_INVALID", "result PyPI artifact identity is invalid")
|
|
178
|
+
normalized.add(name)
|
|
179
|
+
filenames.add(filename)
|
|
180
|
+
if item["requested"]:
|
|
181
|
+
requested.append(item)
|
|
182
|
+
if (len(requested) != 1 or requested[0]["normalized_distribution"] != root["normalized_distribution"] or requested[0]["version"] != root["version"]
|
|
183
|
+
or artifact_set["resolution_digest"] != _pypi_resolution_digest(resolver, environment, artifacts)
|
|
184
|
+
or value["selection_digest"] != digest(artifact_set) or post_resolution != resolution):
|
|
185
|
+
raise OrchestrationError("RESULT_INVALID", "result PyPI selection binding is invalid")
|
|
186
|
+
artifact_first_party = {name: str(item["version"]) for name, item in ((_normal(item["distribution"]), item) for item in artifacts) if name.startswith(("cf-", "cogniflow"))}
|
|
187
|
+
if artifact_first_party != expected_first_party:
|
|
188
|
+
raise OrchestrationError("RESULT_INVALID", "public artifact closure differs from the release profile")
|
|
189
|
+
else:
|
|
190
|
+
raise OrchestrationError("RESULT_INVALID", "result source mode is invalid")
|
|
191
|
+
instance = value["instance"]
|
|
192
|
+
root_path = Path(plan["installation_root"]).resolve()
|
|
193
|
+
expected_python = root_path / "environment" / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python")
|
|
194
|
+
instance_fields = {"schema", "status", "marker", "installation_root", "environment_root", "python_executable", "installed_distributions", "profile", "selection_digest"}
|
|
195
|
+
expected_instance_schema = "cf.bootstrap.instance-apply.result.v1" if mode == "pypi" else "cf.bootstrap.instance-apply.result.v2"
|
|
196
|
+
if (not isinstance(instance, dict) or set(instance) != instance_fields or instance.get("schema") != expected_instance_schema or instance.get("status") not in {"ready", "unchanged"} or instance.get("profile") != profile
|
|
197
|
+
or instance.get("marker") != str(root_path / ".cogniflow-instance.json") or instance.get("installation_root") != str(root_path) or instance.get("environment_root") != str(root_path / "environment")
|
|
198
|
+
or instance.get("python_executable") != str(expected_python) or instance.get("selection_digest") != value["selection_digest"]
|
|
199
|
+
or not isinstance(instance.get("installed_distributions"), list)):
|
|
200
|
+
raise OrchestrationError("RESULT_INVALID", "result instance binding is invalid")
|
|
201
|
+
installed: dict[str, str] = {}
|
|
202
|
+
for item in instance["installed_distributions"]:
|
|
203
|
+
if not isinstance(item, dict) or set(item) != {"name", "version"} or not isinstance(item["name"], str) or not isinstance(item["version"], str) or _normal(item["name"]) in installed:
|
|
204
|
+
raise OrchestrationError("RESULT_INVALID", "result installed inventory is invalid")
|
|
205
|
+
installed[_normal(item["name"])] = item["version"]
|
|
206
|
+
if mode == "pypi":
|
|
207
|
+
installed_first_party = {name: version for name, version in installed.items() if name.startswith(("cf-", "cogniflow"))}
|
|
208
|
+
accepted = {_normal(item["distribution"]): str(item["version"]) for item in artifact_set["artifacts"]}
|
|
209
|
+
if installed_first_party != expected_first_party or any(installed.get(name) != version for name, version in accepted.items()):
|
|
210
|
+
raise OrchestrationError("RESULT_INVALID", "result installed inventory differs from the accepted public closure")
|
|
211
|
+
handoff = value["handoff"]
|
|
212
|
+
receipt = handoff.get("handoff_receipt") if isinstance(handoff, dict) else None
|
|
213
|
+
if (not isinstance(handoff, dict) or set(handoff) != {"schema", "status", "handoff_receipt"} or handoff.get("schema") != "cf.bootstrap.service-handoff.result.v2" or handoff.get("status") != "accepted" or not isinstance(receipt, dict)
|
|
214
|
+
or receipt.get("instance_id") != plan["instance_id"] or receipt.get("profile") != profile or receipt.get("installation_root") != str(root_path)
|
|
215
|
+
or receipt.get("target_python") != str(expected_python) or receipt.get("cogniflow_home") != str(Path(plan["cogniflow_home"]).resolve())
|
|
216
|
+
or receipt.get("plan_digest") != digest(plan) or receipt.get("bootstrap_host_artifact_set_digest") != digest(artifact_set)
|
|
217
|
+
or receipt.get("bootstrap_selection_digest") != value["selection_digest"] or receipt.get("post_handoff_artifact_set_digest") != digest(post_set)
|
|
218
|
+
or receipt.get("record_digest") != digest({key: item for key, item in receipt.items() if key != "record_digest"})
|
|
219
|
+
or (mode == "pypi" and (set(receipt) != {"schema", "instance_id", "installation_root", "target_python", "cogniflow_home", "artifact_source", "plan_digest", "bootstrap_host_artifact_set_digest", "profile", "bootstrap_selection_digest", "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", "record_digest"}
|
|
220
|
+
or receipt.get("schema") != "cf.bootstrap.accepted-handoff.v3" or receipt.get("artifact_source") != plan["artifact_source"] or receipt.get("post_handoff_artifact_root") != str(Path(resolution["wheelhouse_path"]).resolve())
|
|
221
|
+
or receipt.get("post_handoff_selection_digest") != digest(post_set) or receipt.get("post_handoff_profile_digest") != digest(artifact_set) or receipt.get("post_handoff_catalog_digest") != digest(artifact_set)
|
|
222
|
+
or receipt.get("post_handoff_repository_source_digest") != digest(plan["artifact_source"]) or receipt.get("post_handoff_index_digest") != digest("https://pypi.org/simple") or "checkout_path" in receipt))
|
|
223
|
+
or (mode == "local-repository" and receipt.get("checkout_path") != plan["artifact_source"]["checkout_path"])):
|
|
224
|
+
raise OrchestrationError("RESULT_INVALID", "result handoff binding is invalid")
|
|
225
|
+
installation = value["profile_installation"]
|
|
226
|
+
if (not isinstance(installation, dict) or set(installation) != {"schema", "profile", "instance_id", "target_python", "plan_digest", "selection_digest", "release_profile_digest", "managed_tools", "preview", "installation"}
|
|
227
|
+
or installation.get("schema") != "cogniflow.profile-installation.result.v1" or installation.get("profile") != profile
|
|
228
|
+
or installation.get("instance_id") != plan["instance_id"] or installation.get("target_python") != str(expected_python)
|
|
229
|
+
or installation.get("plan_digest") != digest(plan) or installation.get("selection_digest") != value["selection_digest"] or not isinstance(installation.get("managed_tools"), dict)):
|
|
230
|
+
raise OrchestrationError("RESULT_INVALID", "profile installation binding is invalid")
|
|
231
|
+
if mode == "pypi":
|
|
232
|
+
service_fields = {"schema", "status", "profile", "selection_digest", "comparison", "artifact_set", "semantic_changes"}
|
|
233
|
+
desired = [{"name": str(item["distribution"]), "version": str(item["version"])} for item in artifact_set["artifacts"]]
|
|
234
|
+
for name, result, status in (("preview", installation["preview"], "preview"), ("installation", installation["installation"], "unchanged")):
|
|
235
|
+
comparison = result.get("comparison") if isinstance(result, dict) else None
|
|
236
|
+
if (not isinstance(result, dict) or set(result) != service_fields or result.get("schema") != "cf.installation-profile.result.v1" or result.get("status") != status
|
|
237
|
+
or result.get("profile") != profile or result.get("selection_digest") != value["selection_digest"] or result.get("artifact_set") != artifact_set
|
|
238
|
+
or not isinstance(comparison, dict) or comparison.get("desired") != desired or comparison.get("installed") != instance["installed_distributions"] or comparison.get("host") != instance["installed_distributions"]
|
|
239
|
+
or comparison.get("added") != [] or comparison.get("changed") != [] or comparison.get("removed") != [] or comparison.get("unchanged") is not True):
|
|
240
|
+
raise OrchestrationError("RESULT_INVALID", f"profile installation {name} is not bound to the public closure")
|
|
241
|
+
if installation["release_profile_digest"] != "sha256:" + hashlib.sha256(release_bytes).hexdigest():
|
|
242
|
+
raise OrchestrationError("RESULT_INVALID", "profile installation release authority is invalid")
|
|
243
|
+
records = release_profile.get("managed_tools") if isinstance(release_profile, dict) else None
|
|
244
|
+
if not isinstance(records, list):
|
|
245
|
+
raise OrchestrationError("RESULT_INVALID", "managed-tool release authority is invalid")
|
|
246
|
+
platform_name = "windows" if sys.platform == "win32" else "linux"
|
|
247
|
+
selected = {item.get("name"): item for item in records if isinstance(item, dict)}
|
|
248
|
+
expected_release = {"java": selected.get(f"java-{platform_name}-x86_64"), "fuseki": selected.get("fuseki")}
|
|
249
|
+
managed = installation["managed_tools"]
|
|
250
|
+
if set(managed) != {"schema", "status", "java", "fuseki"} or managed.get("schema") != "cf.runtime.managed-tools.result.v1" or managed.get("status") not in {"ready", "unchanged"}:
|
|
251
|
+
raise OrchestrationError("RESULT_INVALID", "managed-tool evidence is invalid")
|
|
252
|
+
for name in ("java", "fuseki"):
|
|
253
|
+
evidence = managed[name]
|
|
254
|
+
authority = expected_release[name]
|
|
255
|
+
path_key = "executable" if name == "java" else "server_artifact"
|
|
256
|
+
expected_record = None if not isinstance(authority, dict) else {"name": name, "version": authority.get("version"), "platform": platform_name if name == "java" else "any", "architecture": "x86_64", "archive_format": "zip" if name == "fuseki" or platform_name == "windows" else "tar.gz", "url": authority.get("url"), "sha256": authority.get("sha256"), "size": authority.get("size")}
|
|
257
|
+
if (not isinstance(evidence, dict) or set(evidence) != {"version", "home", "artifact_sha256", "lock_record", "lock_digest", path_key}
|
|
258
|
+
or evidence.get("lock_record") != expected_record or evidence.get("version") != expected_record.get("version")
|
|
259
|
+
or evidence.get("artifact_sha256") != expected_record.get("sha256") or evidence.get("lock_digest") != digest(expected_record)):
|
|
260
|
+
raise OrchestrationError("RESULT_INVALID", "managed-tool evidence differs from the release authority")
|
|
261
|
+
return value
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""The profile-driven Cogniflow installation sequence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .discovery import executable, one
|
|
12
|
+
from .model import OrchestrationError, digest, validate_request
|
|
13
|
+
from .process import call
|
|
14
|
+
|
|
15
|
+
BOOTSTRAP_GROUP = "cogniflow.bootstrap_providers.v1"
|
|
16
|
+
RESOLVER_GROUP = "cogniflow.artifact_resolvers.v1"
|
|
17
|
+
INSTANCE_GROUP = "cogniflow.instance_providers.v1"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _script(group: str, name: str | None = None) -> Path:
|
|
21
|
+
return executable(one(group, name))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _plan(request: dict[str, object]) -> dict[str, object]:
|
|
25
|
+
result = call(_script(BOOTSTRAP_GROUP, "cf-bootstrap-core-provider"), "cf.bootstrap.provider.v1", "bootstrap/plan", request, 30)
|
|
26
|
+
fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "profile", "stages"}
|
|
27
|
+
if set(result) != fields or result.get("schema") != "cf.bootstrap.plan.v2" or result.get("profile") != request.get("profile"):
|
|
28
|
+
raise OrchestrationError("PLAN_INVALID", "planner returned a non-canonical v2 plan")
|
|
29
|
+
names = ("validate-inputs", "select-artifact-source", "prepare-installation", "install-artifacts", "verify-installation", "handoff-to-service")
|
|
30
|
+
stages = result.get("stages")
|
|
31
|
+
if not isinstance(stages, list) or len(stages) != len(names) or any(not isinstance(stage, dict) or set(stage) != {"name", "ordinal", "handoff"} or stage.get("name") != name or stage.get("ordinal") != index or stage.get("handoff") is not (index == 5) for index, (stage, name) in enumerate(zip(stages, names))):
|
|
32
|
+
raise OrchestrationError("PLAN_INVALID", "planner returned non-canonical lifecycle stages")
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve(plan: dict[str, object], output: str, external: str | None = None, *, phase: str = "bootstrap", index_url: str | None = None) -> dict[str, object]:
|
|
37
|
+
source = plan.get("artifact_source")
|
|
38
|
+
mode = source.get("mode") if isinstance(source, dict) else None
|
|
39
|
+
if mode == "pypi":
|
|
40
|
+
if external is not None or phase != "bootstrap" or index_url != "https://pypi.org/simple":
|
|
41
|
+
raise OrchestrationError("RESOLUTION_INVALID", "PyPI resolution inputs are invalid")
|
|
42
|
+
params = {"schema": "cf.bootstrap.pypi-artifact-resolution.request.v1", "bootstrap_plan": plan, "artifact_output_root": output, "index_url": index_url}
|
|
43
|
+
result = call(_script(RESOLVER_GROUP, "pypi"), "cf.bootstrap.artifact-resolver.v1", "artifacts/resolve", params, 900)
|
|
44
|
+
required = {"schema", "wheelhouse_path", "artifact_set", "lock_filename"}
|
|
45
|
+
artifact_set = result.get("artifact_set") if isinstance(result, dict) else None
|
|
46
|
+
root = artifact_set.get("root_requirement") if isinstance(artifact_set, dict) else None
|
|
47
|
+
if set(result) != required or result.get("schema") != "cf.bootstrap.pypi-artifact-resolution.v1" or result.get("wheelhouse_path") != output or result.get("lock_filename") != "requirements.lock" or not isinstance(artifact_set, dict) or artifact_set.get("schema") != "cf.bootstrap.pypi-artifact-set.v1" or artifact_set.get("source_mode") != "pypi" or not isinstance(root, dict) or root.get("distribution") != source.get("package") or root.get("version") != source.get("version"):
|
|
48
|
+
raise OrchestrationError("RESOLUTION_INVALID", "PyPI resolver returned an unbound result")
|
|
49
|
+
return result
|
|
50
|
+
if mode != "local-repository" or external is None or index_url is not None:
|
|
51
|
+
raise OrchestrationError("RESOLUTION_INVALID", "local resolution inputs are invalid")
|
|
52
|
+
params = {"schema": "cf.bootstrap.artifact-resolution.request.v2", "bootstrap_plan": plan, "artifact_output_root": output, "external_wheelhouse_path": external, "phase": phase}
|
|
53
|
+
result = call(_script(RESOLVER_GROUP, "local-repository"), "cf.bootstrap.artifact-resolver.v1", "artifacts/resolve", params, 900)
|
|
54
|
+
required = {"schema", "wheelhouse_path", "artifact_set", "selection", "provider_map", "evidence"}
|
|
55
|
+
if set(result) != required or result.get("schema") != "cf.bootstrap.local-artifact-resolution.v2" or result.get("wheelhouse_path") != output:
|
|
56
|
+
raise OrchestrationError("RESOLUTION_INVALID", "resolver returned a non-canonical v2 result")
|
|
57
|
+
artifact_set = result["artifact_set"]
|
|
58
|
+
if not isinstance(artifact_set, dict) or artifact_set.get("schema") != "cf.bootstrap.artifact-set.v2" or artifact_set.get("plan_digest") != digest(plan) or artifact_set.get("profile") != plan.get("profile") or artifact_set.get("selection_digest") != digest(result["selection"]):
|
|
59
|
+
raise OrchestrationError("RESOLUTION_INVALID", "resolver plan/profile/selection bindings are invalid")
|
|
60
|
+
evidence = result["evidence"]
|
|
61
|
+
if not isinstance(evidence, dict) or evidence.get("unselected_build_count") != 0 or evidence.get("native_selected") is not False:
|
|
62
|
+
raise OrchestrationError("RESOLUTION_INVALID", "resolver selected an unsafe artifact set")
|
|
63
|
+
artifacts = artifact_set.get("artifacts")
|
|
64
|
+
if not isinstance(artifacts, list) or any(isinstance(item, dict) and item.get("source_kind") == "external-wheelhouse" and str(item.get("normalized_distribution", "")).startswith("cf-") for item in artifacts):
|
|
65
|
+
raise OrchestrationError("RESOLUTION_INVALID", "resolver returned forbidden external artifacts")
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _apply(plan: dict[str, object], resolution: dict[str, object]) -> dict[str, object]:
|
|
70
|
+
pypi = plan["artifact_source"]["mode"] == "pypi"
|
|
71
|
+
keys = ("schema", "wheelhouse_path", "artifact_set", "lock_filename") if pypi else ("schema", "wheelhouse_path", "artifact_set")
|
|
72
|
+
params = {"schema": "cf.bootstrap.instance-apply.request.v1" if pypi else "cf.bootstrap.instance-apply.request.v2", "bootstrap_plan": plan, "artifact_resolution": {key: resolution[key] for key in keys}}
|
|
73
|
+
result = call(_script(INSTANCE_GROUP, "cf-bootstrap-instance-provider"), "cf.bootstrap.instance-provider.v1", "instance/apply", params, 900)
|
|
74
|
+
base = {"schema", "status", "marker", "installation_root", "environment_root", "python_executable", "installed_distributions"}
|
|
75
|
+
required = base if pypi else base | {"profile", "selection_digest"}
|
|
76
|
+
selection_digest = digest(resolution["artifact_set"]) if pypi else resolution["artifact_set"]["selection_digest"]
|
|
77
|
+
if set(result) != required or result.get("schema") != ("cf.bootstrap.instance-apply.result.v1" if pypi else "cf.bootstrap.instance-apply.result.v2") or result.get("status") not in {"ready", "unchanged"}:
|
|
78
|
+
raise OrchestrationError("INSTANCE_INVALID", "instance provider returned a non-canonical result")
|
|
79
|
+
result = {**result, "profile": plan["profile"], "selection_digest": selection_digest}
|
|
80
|
+
root = Path(str(result["installation_root"])).resolve()
|
|
81
|
+
environment = Path(str(result["environment_root"])).resolve()
|
|
82
|
+
python = Path(str(result["python_executable"])).resolve()
|
|
83
|
+
if root != Path(str(plan["installation_root"])).resolve() or environment != root / "environment" or python != environment / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python"):
|
|
84
|
+
raise OrchestrationError("INSTANCE_INVALID", "instance provider returned an unbound target")
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _handoff(plan: dict[str, object], resolution: dict[str, object], instance: dict[str, object], post_handoff: dict[str, object] | None = None) -> dict[str, object]:
|
|
89
|
+
scripts = Path(str(instance["environment_root"])).resolve() / ("Scripts" if sys.platform == "win32" else "bin")
|
|
90
|
+
target = scripts / ("cf-bootstrap-instance-handoff.exe" if sys.platform == "win32" else "cf-bootstrap-instance-handoff")
|
|
91
|
+
if not target.is_file() or target.is_symlink() or target.resolve().parent != scripts.resolve():
|
|
92
|
+
raise OrchestrationError("HANDOFF_INVALID", "target handoff executable is not confined to the target Scripts directory")
|
|
93
|
+
artifact_set = resolution["artifact_set"]
|
|
94
|
+
post = post_handoff or resolution
|
|
95
|
+
post_set = post["artifact_set"]
|
|
96
|
+
source = plan["artifact_source"]
|
|
97
|
+
if source["mode"] == "pypi":
|
|
98
|
+
selection_digest = digest(artifact_set)
|
|
99
|
+
binding = {"cogniflow_home": plan["cogniflow_home"], "artifact_source": source, "post_handoff_artifact_root": str(Path(str(post["wheelhouse_path"])).resolve()), "post_handoff_artifact_set_digest": digest(post_set), "post_handoff_selection_digest": digest(post_set)}
|
|
100
|
+
request = {"schema": "cf.bootstrap.service-handoff.request.v3", "instance_id": plan["instance_id"], "installation_root": instance["installation_root"], "plan_digest": digest(plan), "artifact_set_digest": digest(artifact_set), "profile": plan["profile"], "selection_digest": selection_digest, **binding, "post_handoff_binding_digest": digest(binding)}
|
|
101
|
+
else:
|
|
102
|
+
binding = {"cogniflow_home": plan["cogniflow_home"], "checkout_path": source["checkout_path"], "post_handoff_artifact_root": str(Path(str(post["wheelhouse_path"])).resolve()), "post_handoff_artifact_set_digest": digest(post_set), "post_handoff_selection_digest": post_set["selection_digest"], "post_handoff_profile_digest": post_set["profile_digest"], "post_handoff_catalog_digest": post_set["catalog_digest"], "post_handoff_repository_source_digest": post_set["repository_source_digest"], "post_handoff_index_digest": post_set["index_digest"]}
|
|
103
|
+
request = {"schema": "cf.bootstrap.service-handoff.request.v2", "instance_id": plan["instance_id"], "installation_root": instance["installation_root"], "plan_digest": digest(plan), "artifact_set_digest": digest(artifact_set), "profile": plan["profile"], "profile_digest": artifact_set["profile_digest"], "selection_digest": artifact_set["selection_digest"], **binding, "post_handoff_binding_digest": digest(binding)}
|
|
104
|
+
try:
|
|
105
|
+
process = subprocess.Popen([str(target)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
|
|
106
|
+
stdout, stderr = process.communicate((json.dumps(request, sort_keys=True, separators=(",", ":")) + "\n").encode(), timeout=120)
|
|
107
|
+
except (OSError, subprocess.TimeoutExpired) as error:
|
|
108
|
+
if "process" in locals():
|
|
109
|
+
process.kill(); process.communicate()
|
|
110
|
+
raise OrchestrationError("HANDOFF_START_FAILED", "target handoff could not complete") from error
|
|
111
|
+
finally:
|
|
112
|
+
if "process" in locals():
|
|
113
|
+
for stream in (process.stdin, process.stdout, process.stderr):
|
|
114
|
+
if stream is not None:
|
|
115
|
+
stream.close()
|
|
116
|
+
if process.returncode != 0 or stderr or stdout.count(b"\n") != 1 or not stdout.endswith(b"\n"):
|
|
117
|
+
raise OrchestrationError("HANDOFF_INVALID", "target handoff process or output was invalid")
|
|
118
|
+
try:
|
|
119
|
+
result = json.loads(stdout)
|
|
120
|
+
except json.JSONDecodeError as error:
|
|
121
|
+
raise OrchestrationError("HANDOFF_INVALID", "target handoff returned invalid JSON") from error
|
|
122
|
+
if not isinstance(result, dict) or result.get("schema") != "cf.bootstrap.service-handoff.result.v2" or result.get("status") != "accepted" or not isinstance(result.get("handoff_receipt"), dict):
|
|
123
|
+
raise OrchestrationError("HANDOFF_INVALID", "target handoff was not accepted")
|
|
124
|
+
receipt = result["handoff_receipt"]
|
|
125
|
+
if receipt.get("instance_id") != request["instance_id"] or receipt.get("plan_digest") != request["plan_digest"] or receipt.get("profile") != request["profile"] or receipt.get("installation_root") != request["installation_root"] or receipt.get("bootstrap_host_artifact_set_digest") != request["artifact_set_digest"] or receipt.get("bootstrap_selection_digest") != request["selection_digest"] or receipt.get("post_handoff_artifact_set_digest") != request["post_handoff_artifact_set_digest"] or receipt.get("post_handoff_selection_digest") != request["post_handoff_selection_digest"] or (source["mode"] == "pypi" and (receipt.get("schema") != "cf.bootstrap.accepted-handoff.v3" or receipt.get("artifact_source") != source or "checkout_path" in receipt)) or (source["mode"] == "local-repository" and receipt.get("checkout_path") != source["checkout_path"]):
|
|
126
|
+
raise OrchestrationError("HANDOFF_INVALID", "persisted accepted handoff is not bound to the request")
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _install_profile(plan: dict[str, object], instance: dict[str, object], handoff: dict[str, object], selection_digest: str) -> dict[str, object]:
|
|
131
|
+
service_client = __import__("cf_service_client", fromlist=["ServiceClient"])
|
|
132
|
+
release_module = __import__("cogniflow.release", fromlist=["load_profile", "profile_bytes"])
|
|
133
|
+
release = release_module.load_profile()
|
|
134
|
+
release_profile_digest = "sha256:" + hashlib.sha256(release_module.profile_bytes()).hexdigest()
|
|
135
|
+
runtime = __import__("cf_runtime", fromlist=["CogniflowHome", "ManagedTools"])
|
|
136
|
+
managed_module = __import__("cf_runtime.managed_tools", fromlist=["bind_release_lock"])
|
|
137
|
+
locked = managed_module.bind_release_lock([dict(item) for item in release.tools])
|
|
138
|
+
managed_tools = runtime.ManagedTools(runtime.CogniflowHome.at(plan["cogniflow_home"]), artifacts=locked).provision("21.0.10+7", "6.0.0")
|
|
139
|
+
inputs = {"profile": plan["profile"], "target_python": instance["python_executable"], "bootstrap_plan": plan, "handoff_receipt": handoff["handoff_receipt"]}
|
|
140
|
+
source = plan["artifact_source"]
|
|
141
|
+
inputs["checkout_path" if source["mode"] == "local-repository" else "artifact_source"] = source["checkout_path"] if source["mode"] == "local-repository" else source
|
|
142
|
+
client = service_client.ServiceClient.for_runtime("production", home=plan["cogniflow_home"])
|
|
143
|
+
if source["mode"] == "pypi":
|
|
144
|
+
preview_capability = "urn:cf:service:PublicPyPIProfileInstallationPreview"
|
|
145
|
+
install_capability = "urn:cf:service:PublicPyPIProfileInstallation"
|
|
146
|
+
else:
|
|
147
|
+
preview_capability = "urn:cf:service:ProfileInstallationPreview"
|
|
148
|
+
install_capability = "urn:cf:service:ProfileInstallation"
|
|
149
|
+
preview = client.call_service(preview_capability, inputs=inputs)
|
|
150
|
+
installation = client.call_service(install_capability, inputs=inputs)
|
|
151
|
+
if preview.is_error or installation.is_error:
|
|
152
|
+
raise OrchestrationError("PROFILE_INSTALL_FAILED", "service-driven profile installation failed")
|
|
153
|
+
return {"schema": "cogniflow.profile-installation.result.v1", "profile": plan["profile"], "instance_id": plan["instance_id"], "target_python": instance["python_executable"], "plan_digest": digest(plan), "selection_digest": selection_digest, "release_profile_digest": release_profile_digest, "managed_tools": managed_tools, "preview": preview.content, "installation": installation.content}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run(value: object) -> dict[str, object]:
|
|
157
|
+
request = validate_request(value)
|
|
158
|
+
plan = _plan(request["bootstrap_request"])
|
|
159
|
+
mode = plan["artifact_source"]["mode"]
|
|
160
|
+
if mode == "pypi":
|
|
161
|
+
resolution = _resolve(plan, str(request["artifact_output_root"]), index_url=str(request["index_url"]))
|
|
162
|
+
post_handoff = resolution
|
|
163
|
+
else:
|
|
164
|
+
external = str(request["external_wheelhouse_path"])
|
|
165
|
+
resolution = _resolve(plan, str(request["artifact_output_root"]), external)
|
|
166
|
+
output = Path(str(request["artifact_output_root"])).resolve()
|
|
167
|
+
post_handoff = _resolve(plan, str(output.with_name(output.name + "-post-handoff")), external, phase="post-handoff")
|
|
168
|
+
instance = _apply(plan, resolution)
|
|
169
|
+
selection_digest = digest(resolution["artifact_set"]) if mode == "pypi" else resolution["artifact_set"]["selection_digest"]
|
|
170
|
+
handoff = _handoff(plan, resolution, instance, post_handoff)
|
|
171
|
+
profile_installation = _install_profile(plan, instance, handoff, selection_digest)
|
|
172
|
+
return {"schema": "cogniflow.install.result.v3", "status": instance["status"], "profile": plan["profile"], "selection_digest": selection_digest, "plan": plan, "artifact_resolution": resolution, "post_handoff_resolution": post_handoff, "instance": instance, "handoff": handoff, "profile_installation": profile_installation}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Bounded JSON-lines subprocess calls used by the orchestrator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .model import OrchestrationError, PROTOCOL, canonical
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _identifier(value: object) -> str:
|
|
15
|
+
return "request-" + hashlib.sha256(canonical(value)).hexdigest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def call(executable: Path, protocol: str, method: str, params: dict[str, object], timeout: float) -> dict[str, object]:
|
|
19
|
+
identifier = _identifier(params)
|
|
20
|
+
request = {"jsonrpc": "2.0", "protocol": protocol, "id": identifier, "method": method, "params": params}
|
|
21
|
+
process: subprocess.Popen[bytes] | None = None
|
|
22
|
+
try:
|
|
23
|
+
process = subprocess.Popen([str(executable)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
|
|
24
|
+
stdout, stderr = process.communicate(canonical(request), timeout=timeout)
|
|
25
|
+
except subprocess.TimeoutExpired as error:
|
|
26
|
+
if process is not None:
|
|
27
|
+
process.kill()
|
|
28
|
+
process.communicate()
|
|
29
|
+
raise OrchestrationError("PROCESS_TIMEOUT", "provider process timed out") from error
|
|
30
|
+
except OSError as error:
|
|
31
|
+
raise OrchestrationError("PROCESS_START_FAILED", "provider process could not start") from error
|
|
32
|
+
finally:
|
|
33
|
+
if process is not None:
|
|
34
|
+
failures = []
|
|
35
|
+
for name, stream in (("stdin", process.stdin), ("stdout", process.stdout), ("stderr", process.stderr)):
|
|
36
|
+
if stream is not None:
|
|
37
|
+
try:
|
|
38
|
+
stream.close()
|
|
39
|
+
except Exception as cleanup_error:
|
|
40
|
+
failures.append(f"{name}: {cleanup_error}")
|
|
41
|
+
if failures:
|
|
42
|
+
primary = sys.exception()
|
|
43
|
+
if primary is not None:
|
|
44
|
+
primary.add_note("provider pipe cleanup also failed: " + "; ".join(failures))
|
|
45
|
+
else:
|
|
46
|
+
raise OrchestrationError("PROCESS_CLEANUP_FAILED", "; ".join(failures))
|
|
47
|
+
if process is None or process.returncode != 0 or stderr or stdout.count(b"\n") != 1 or not stdout.endswith(b"\n"):
|
|
48
|
+
raise OrchestrationError("PROCESS_INVALID", "provider process or output was invalid")
|
|
49
|
+
try:
|
|
50
|
+
response = json.loads(stdout)
|
|
51
|
+
except json.JSONDecodeError as error:
|
|
52
|
+
raise OrchestrationError("MALFORMED_JSON", "provider returned invalid JSON") from error
|
|
53
|
+
if not isinstance(response, dict) or set(response) not in ({"jsonrpc", "protocol", "id", "result"}, {"jsonrpc", "protocol", "id", "error"}) or response.get("jsonrpc") != "2.0" or response.get("protocol") != protocol or response.get("id") != identifier:
|
|
54
|
+
raise OrchestrationError("PROTOCOL_INVALID", "provider response identity was invalid")
|
|
55
|
+
if "error" in response:
|
|
56
|
+
error = response["error"]
|
|
57
|
+
if not isinstance(error, dict) or set(error) - {"code", "message", "details"} or not isinstance(error.get("code"), str) or not isinstance(error.get("message"), str):
|
|
58
|
+
raise OrchestrationError("PROTOCOL_INVALID", "provider returned an invalid structured error")
|
|
59
|
+
details = error.get("details")
|
|
60
|
+
diagnostic = details.get("diagnostic") if isinstance(details, dict) else None
|
|
61
|
+
message = f"{error['message']}: {diagnostic}" if isinstance(diagnostic, str) and diagnostic else error["message"]
|
|
62
|
+
raise OrchestrationError(error["code"], message)
|
|
63
|
+
result = response.get("result")
|
|
64
|
+
if not isinstance(result, dict):
|
|
65
|
+
raise OrchestrationError("RESULT_INVALID", "provider result was not an object")
|
|
66
|
+
return result
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@prefix cfpkg: <https://cogniflow.odea-project.org/cf#> .
|
|
2
|
+
@prefix contribution: <urn:cf:contribution:> .
|
|
3
|
+
@prefix pkg: <urn:cf:pkg:> .
|
|
4
|
+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
5
|
+
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
|
|
6
|
+
|
|
7
|
+
pkg:cf_bootstrap_orchestrator {
|
|
8
|
+
pkg:cf_bootstrap_orchestrator a cfpkg:CfPackage ;
|
|
9
|
+
cfpkg:hasPackageManifest [ a cfpkg:PackageManifest ; cfpkg:hasDistributionName [ a cfpkg:DistributionName ; rdf:value "cf-bootstrap-orchestrator" ] ; cfpkg:hasPythonPackageName [ a cfpkg:PythonPackageName ; rdf:value "cf_bootstrap_orchestrator" ] ; cfpkg:hasPackageVersion [ a cfpkg:PackageVersion ; rdf:value "0.1.8" ] ; cfpkg:hasImplementationLanguage [ a cfpkg:ImplementationLanguage ; rdf:value "Python" ] ] ;
|
|
10
|
+
cfpkg:hasPackageRole [ a cfpkg:PackageRole ; rdf:value cfpkg:SpecificationPackageRole ] ;
|
|
11
|
+
cfpkg:hasPackageContribution contribution:adopted_package ;
|
|
12
|
+
skos:prefLabel "cf-bootstrap-orchestrator" ; skos:definition "Profile-aware installation orchestration process." .
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
cf_bootstrap_orchestrator/__init__.py,sha256=jHsf4pNtiKFJV9G2LVCyXcRosKhnNBDq2JLoB60HMXI,339
|
|
2
|
+
cf_bootstrap_orchestrator/cli.py,sha256=lroqlMI_68nCCOIEQL76DAajZzAkWKugNHXkBTY-yRg,2242
|
|
3
|
+
cf_bootstrap_orchestrator/discovery.py,sha256=qU29J3t-I2luX3JtyfviylTIZAI7RO2amG-cpMEyQuw,2765
|
|
4
|
+
cf_bootstrap_orchestrator/model.py,sha256=X5Q5Vs7jXdRasx2GGbM-29btMp25Xxv12xX2P_CadRo,23626
|
|
5
|
+
cf_bootstrap_orchestrator/orchestrator.py,sha256=lF-rw2mLmkRZ3a6MRmQ0GjqK9D4YjVOj_r4-BhAxdio,15950
|
|
6
|
+
cf_bootstrap_orchestrator/process.py,sha256=Acp1popHNAGsHYg11dkx2XBJehpLs9-EQStCYbGgow4,3643
|
|
7
|
+
cf_bootstrap_orchestrator/semantics/package.trig,sha256=5hsZww9ebkBTCalbtMydt7aaK3_bwTnfm2v6RriH5gE,1045
|
|
8
|
+
cf_bootstrap_orchestrator-0.1.8.dist-info/METADATA,sha256=6D5U9iKX_9zro8gbVmmUJjYMUICeMcCVEDGzdTbnD24,178
|
|
9
|
+
cf_bootstrap_orchestrator-0.1.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
10
|
+
cf_bootstrap_orchestrator-0.1.8.dist-info/entry_points.txt,sha256=H8XxWkfRx0_RsiJO4NiegwNC1X4rTNaNUs6D0oR6tkU,281
|
|
11
|
+
cf_bootstrap_orchestrator-0.1.8.dist-info/RECORD,,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
cf-bootstrap-orchestrator = cf_bootstrap_orchestrator.cli:main
|
|
3
|
+
|
|
4
|
+
[cogniflow.install_orchestrators.v1]
|
|
5
|
+
cf-bootstrap-orchestrator = cf_bootstrap_orchestrator.cli:main
|
|
6
|
+
|
|
7
|
+
[cogniflow.semantic_sources]
|
|
8
|
+
cf-bootstrap-orchestrator = cf_bootstrap_orchestrator:semantic_files
|