cf-bootstrap-core 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_core/__init__.py +35 -0
- cf_bootstrap_core/model.py +234 -0
- cf_bootstrap_core/provider.py +65 -0
- cf_bootstrap_core/semantics/package.trig +13 -0
- cf_bootstrap_core-0.1.8.dist-info/METADATA +37 -0
- cf_bootstrap_core-0.1.8.dist-info/RECORD +8 -0
- cf_bootstrap_core-0.1.8.dist-info/WHEEL +4 -0
- cf_bootstrap_core-0.1.8.dist-info/entry_points.txt +8 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Pure bootstrap planning primitives."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
PACKAGE_DISTRIBUTION = "cf-bootstrap-core"
|
|
6
|
+
PYTHON_PACKAGE = "cf_bootstrap_core"
|
|
7
|
+
PACKAGE_VERSION = "0.1.8"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def package_root() -> Path:
|
|
11
|
+
return Path(__file__).resolve().parent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def semantics_dir() -> Path:
|
|
15
|
+
return package_root() / "semantics"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def semantic_files() -> tuple[Path, ...]:
|
|
19
|
+
return (semantics_dir() / "package.trig",)
|
|
20
|
+
|
|
21
|
+
from .model import (
|
|
22
|
+
ArtifactSource,
|
|
23
|
+
BootstrapPlan,
|
|
24
|
+
BootstrapRequest,
|
|
25
|
+
LifecycleStage,
|
|
26
|
+
PlanError,
|
|
27
|
+
build_plan,
|
|
28
|
+
canonical_json,
|
|
29
|
+
validate_lifecycle_stages,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ArtifactSource", "BootstrapPlan", "BootstrapRequest", "LifecycleStage",
|
|
34
|
+
"PlanError", "build_plan", "canonical_json", "validate_lifecycle_stages",
|
|
35
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Strict, side-effect-free bootstrap request and plan models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import PurePosixPath, PureWindowsPath
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
REQUEST_SCHEMA = "cf.bootstrap.request.v1"
|
|
12
|
+
PLAN_SCHEMA = "cf.bootstrap.plan.v1"
|
|
13
|
+
REQUEST_SCHEMA_V2 = "cf.bootstrap.request.v2"
|
|
14
|
+
PLAN_SCHEMA_V2 = "cf.bootstrap.plan.v2"
|
|
15
|
+
PROTOCOL = "cf.bootstrap.provider.v1"
|
|
16
|
+
STAGES = (
|
|
17
|
+
"validate-inputs",
|
|
18
|
+
"select-artifact-source",
|
|
19
|
+
"prepare-installation",
|
|
20
|
+
"install-artifacts",
|
|
21
|
+
"verify-installation",
|
|
22
|
+
"handoff-to-service",
|
|
23
|
+
)
|
|
24
|
+
MAX_INSTANCE_ID_LENGTH = 64
|
|
25
|
+
_IDENTIFIER = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?\Z")
|
|
26
|
+
_VERSION = re.compile(r"[0-9]+(?:\.[0-9]+){0,2}(?:[-+][A-Za-z0-9.-]+)?\Z")
|
|
27
|
+
_IRI = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:[^\s]+\Z")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PlanError(ValueError):
|
|
31
|
+
"""A request cannot be represented as a canonical plan."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _object(value: object, fields: set[str], label: str) -> dict[str, Any]:
|
|
35
|
+
if not isinstance(value, dict) or any(not isinstance(k, str) for k in value):
|
|
36
|
+
raise PlanError(f"{label} must be an object")
|
|
37
|
+
unknown = set(value) - fields
|
|
38
|
+
if unknown:
|
|
39
|
+
raise PlanError(f"{label} contains unknown fields: {sorted(unknown)}")
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _text(value: object, label: str, pattern: re.Pattern[str] | None = None) -> str:
|
|
44
|
+
if not isinstance(value, str) or not value or value != value.strip() or "\x00" in value:
|
|
45
|
+
raise PlanError(f"{label} must be a nonempty string")
|
|
46
|
+
if pattern and not pattern.fullmatch(value):
|
|
47
|
+
raise PlanError(f"invalid {label}")
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _canonical_path(value: object, label: str) -> str:
|
|
52
|
+
text = _text(value, label)
|
|
53
|
+
if "://" in text:
|
|
54
|
+
raise PlanError(f"invalid {label}")
|
|
55
|
+
windows = PureWindowsPath(text)
|
|
56
|
+
is_windows = bool(windows.drive) or "\\" in text
|
|
57
|
+
path = windows if is_windows else PurePosixPath(text)
|
|
58
|
+
if not path.is_absolute() or str(path) == path.anchor:
|
|
59
|
+
raise PlanError(f"{label} must be an absolute non-root path")
|
|
60
|
+
# PurePath removes ``.`` lexically; ``..`` is rejected before containment.
|
|
61
|
+
if ".." in path.parts:
|
|
62
|
+
raise PlanError(f"{label} cannot contain '..' segments")
|
|
63
|
+
normalized = str(path).replace("/", "\\") if is_windows else str(path)
|
|
64
|
+
return normalized
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _path_contains(parent: str, child: str) -> bool:
|
|
68
|
+
wp, wc = PureWindowsPath(parent), PureWindowsPath(child)
|
|
69
|
+
if wp.drive or wc.drive:
|
|
70
|
+
if wp.drive.lower() != wc.drive.lower():
|
|
71
|
+
return False
|
|
72
|
+
try:
|
|
73
|
+
wc.relative_to(wp)
|
|
74
|
+
return True
|
|
75
|
+
except ValueError:
|
|
76
|
+
return False
|
|
77
|
+
try:
|
|
78
|
+
PurePosixPath(child).relative_to(PurePosixPath(parent))
|
|
79
|
+
return True
|
|
80
|
+
except ValueError:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _identity(value: object) -> str:
|
|
85
|
+
result = _text(value, "instance_id", _IDENTIFIER)
|
|
86
|
+
if len(result) > MAX_INSTANCE_ID_LENGTH:
|
|
87
|
+
raise PlanError(f"instance_id must be at most {MAX_INSTANCE_ID_LENGTH} ASCII characters")
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True)
|
|
92
|
+
class ArtifactSource:
|
|
93
|
+
mode: str
|
|
94
|
+
data: dict[str, str]
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_dict(cls, value: object) -> "ArtifactSource":
|
|
98
|
+
raw = _object(value, {"mode", "checkout_path", "package", "version"}, "artifact_source")
|
|
99
|
+
mode = raw.get("mode")
|
|
100
|
+
if mode == "local-repository":
|
|
101
|
+
if set(raw) != {"mode", "checkout_path"}:
|
|
102
|
+
raise PlanError("local-repository source requires only checkout_path")
|
|
103
|
+
return cls(mode, {"checkout_path": _canonical_path(raw["checkout_path"], "checkout_path")})
|
|
104
|
+
if mode == "pypi":
|
|
105
|
+
if set(raw) != {"mode", "package", "version"}:
|
|
106
|
+
raise PlanError("pypi source requires package and version")
|
|
107
|
+
package = _text(raw["package"], "package", _IDENTIFIER)
|
|
108
|
+
version = _text(raw["version"], "version", _VERSION)
|
|
109
|
+
return cls(mode, {"package": package, "version": version})
|
|
110
|
+
raise PlanError("source mode must be local-repository or pypi")
|
|
111
|
+
|
|
112
|
+
def as_dict(self) -> dict[str, str]:
|
|
113
|
+
return {"mode": self.mode, **self.data}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class LifecycleStage:
|
|
118
|
+
name: str
|
|
119
|
+
ordinal: int
|
|
120
|
+
handoff: bool = False
|
|
121
|
+
|
|
122
|
+
def as_dict(self) -> dict[str, Any]:
|
|
123
|
+
return {"name": self.name, "ordinal": self.ordinal, "handoff": self.handoff}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_lifecycle_stages(value: object) -> tuple[LifecycleStage, ...]:
|
|
127
|
+
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
|
|
128
|
+
raise PlanError("stages must be a list of objects")
|
|
129
|
+
names: list[str] = []
|
|
130
|
+
result: list[LifecycleStage] = []
|
|
131
|
+
for index, item in enumerate(value):
|
|
132
|
+
raw = _object(item, {"name", "ordinal", "handoff"}, "stage")
|
|
133
|
+
name = _text(raw.get("name"), "stage name")
|
|
134
|
+
ordinal = raw.get("ordinal")
|
|
135
|
+
handoff = raw.get("handoff")
|
|
136
|
+
if name not in STAGES or name in names:
|
|
137
|
+
raise PlanError("stages contain an unknown or duplicate stage")
|
|
138
|
+
if type(ordinal) is not int or type(handoff) is not bool:
|
|
139
|
+
raise PlanError("stage ordinal and handoff marker have invalid types")
|
|
140
|
+
if ordinal != index or handoff is not (name == "handoff-to-service"):
|
|
141
|
+
raise PlanError("stage ordinal or handoff marker is invalid")
|
|
142
|
+
names.append(name)
|
|
143
|
+
result.append(LifecycleStage(name, index, name == "handoff-to-service"))
|
|
144
|
+
if tuple(names) != STAGES:
|
|
145
|
+
raise PlanError("stages must contain the canonical ordered lifecycle including handoff-to-service")
|
|
146
|
+
return tuple(result)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class BootstrapRequest:
|
|
151
|
+
instance_id: str
|
|
152
|
+
installation_root: str
|
|
153
|
+
cogniflow_home: str
|
|
154
|
+
artifact_source: ArtifactSource
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(cls, value: object) -> "BootstrapRequest":
|
|
158
|
+
raw = _object(value, {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source"}, "request")
|
|
159
|
+
if raw.get("schema") != REQUEST_SCHEMA:
|
|
160
|
+
raise PlanError("unsupported request schema version")
|
|
161
|
+
home = _canonical_path(raw.get("cogniflow_home"), "cogniflow_home")
|
|
162
|
+
install = _canonical_path(raw.get("installation_root"), "installation_root")
|
|
163
|
+
if install == home or _path_contains(home, install):
|
|
164
|
+
raise PlanError("installation_root must be outside Cogniflow home")
|
|
165
|
+
return cls(_identity(raw.get("instance_id")), install, home, ArtifactSource.from_dict(raw.get("artifact_source")))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True, slots=True)
|
|
169
|
+
class BootstrapRequestV2(BootstrapRequest):
|
|
170
|
+
profile: str
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_dict(cls, value: object) -> "BootstrapRequestV2":
|
|
174
|
+
raw = _object(value, {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "profile"}, "request")
|
|
175
|
+
if raw.get("schema") != REQUEST_SCHEMA_V2:
|
|
176
|
+
raise PlanError("unsupported request schema version")
|
|
177
|
+
profile = _text(raw.get("profile"), "profile")
|
|
178
|
+
if not _IRI.fullmatch(profile):
|
|
179
|
+
raise PlanError("profile must be an absolute IRI")
|
|
180
|
+
base = BootstrapRequest.from_dict({key: value for key, value in raw.items() if key != "profile"} | {"schema": REQUEST_SCHEMA})
|
|
181
|
+
return cls(base.instance_id, base.installation_root, base.cogniflow_home, base.artifact_source, profile)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True)
|
|
185
|
+
class BootstrapPlan:
|
|
186
|
+
instance_id: str
|
|
187
|
+
installation_root: str
|
|
188
|
+
cogniflow_home: str
|
|
189
|
+
artifact_source: ArtifactSource
|
|
190
|
+
stages: tuple[LifecycleStage, ...]
|
|
191
|
+
|
|
192
|
+
def as_dict(self) -> dict[str, Any]:
|
|
193
|
+
return {
|
|
194
|
+
"schema": PLAN_SCHEMA,
|
|
195
|
+
"instance_id": self.instance_id,
|
|
196
|
+
"installation_root": self.installation_root,
|
|
197
|
+
"cogniflow_home": self.cogniflow_home,
|
|
198
|
+
"artifact_source": self.artifact_source.as_dict(),
|
|
199
|
+
"stages": [stage.as_dict() for stage in self.stages],
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True, slots=True)
|
|
204
|
+
class BootstrapPlanV2(BootstrapPlan):
|
|
205
|
+
profile: str
|
|
206
|
+
|
|
207
|
+
def as_dict(self) -> dict[str, Any]:
|
|
208
|
+
return {
|
|
209
|
+
"schema": PLAN_SCHEMA_V2,
|
|
210
|
+
"instance_id": self.instance_id,
|
|
211
|
+
"installation_root": self.installation_root,
|
|
212
|
+
"cogniflow_home": self.cogniflow_home,
|
|
213
|
+
"artifact_source": self.artifact_source.as_dict(),
|
|
214
|
+
"profile": self.profile,
|
|
215
|
+
"stages": [stage.as_dict() for stage in self.stages],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build_plan(request: BootstrapRequest | dict[str, Any]) -> BootstrapPlan:
|
|
220
|
+
if isinstance(request, (BootstrapRequest, BootstrapRequestV2)):
|
|
221
|
+
checked = request
|
|
222
|
+
elif isinstance(request, dict) and request.get("schema") == REQUEST_SCHEMA_V2:
|
|
223
|
+
checked = BootstrapRequestV2.from_dict(request)
|
|
224
|
+
else:
|
|
225
|
+
checked = BootstrapRequest.from_dict(request)
|
|
226
|
+
stages = tuple(LifecycleStage(name, index, name == "handoff-to-service") for index, name in enumerate(STAGES))
|
|
227
|
+
if isinstance(checked, BootstrapRequestV2):
|
|
228
|
+
return BootstrapPlanV2(checked.instance_id, checked.installation_root, checked.cogniflow_home, checked.artifact_source, stages, checked.profile)
|
|
229
|
+
return BootstrapPlan(checked.instance_id, checked.installation_root, checked.cogniflow_home, checked.artifact_source, stages)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def canonical_json(value: object) -> bytes:
|
|
233
|
+
payload = value.as_dict() if hasattr(value, "as_dict") else value
|
|
234
|
+
return (json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Internal versioned JSON-lines bootstrap-plan provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .model import PROTOCOL, REQUEST_SCHEMA, PlanError, build_plan
|
|
9
|
+
|
|
10
|
+
_FIELDS = {"jsonrpc", "protocol", "id", "method", "params"}
|
|
11
|
+
_REQUEST_FIELDS = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "profile"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _envelope(request_id: object, **body: object) -> dict[str, object]:
|
|
15
|
+
return {"jsonrpc": "2.0", "protocol": PROTOCOL, "id": request_id, **body}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handle(payload: object) -> dict[str, object]:
|
|
19
|
+
request_id: object = None
|
|
20
|
+
try:
|
|
21
|
+
if not isinstance(payload, dict):
|
|
22
|
+
return _envelope(None, error={"code": "INVALID_ENVELOPE", "message": "request envelope must be an object"})
|
|
23
|
+
unknown = set(payload) - _FIELDS
|
|
24
|
+
if unknown:
|
|
25
|
+
return _envelope(payload.get("id"), error={"code": "UNKNOWN_ENVELOPE_FIELD", "message": "request envelope contains unknown fields"})
|
|
26
|
+
missing = _FIELDS - set(payload)
|
|
27
|
+
if missing:
|
|
28
|
+
return _envelope(payload.get("id"), error={"code": "INVALID_ENVELOPE", "message": "request envelope is missing required fields"})
|
|
29
|
+
request_id = payload["id"]
|
|
30
|
+
if payload["jsonrpc"] != "2.0" or not isinstance(request_id, (str, int)) or isinstance(request_id, bool):
|
|
31
|
+
return _envelope(request_id, error={"code": "INVALID_ENVELOPE", "message": "request envelope field types are invalid"})
|
|
32
|
+
if not isinstance(payload["protocol"], str) or not isinstance(payload["method"], str):
|
|
33
|
+
return _envelope(request_id, error={"code": "INVALID_ENVELOPE", "message": "request envelope field types are invalid"})
|
|
34
|
+
if payload["protocol"] != PROTOCOL:
|
|
35
|
+
return _envelope(request_id, error={"code": "PROTOCOL_VERSION_UNSUPPORTED", "message": "unsupported bootstrap protocol version"})
|
|
36
|
+
if payload["method"] != "bootstrap/plan":
|
|
37
|
+
return _envelope(request_id, error={"code": "UNKNOWN_OPERATION", "message": "unknown bootstrap operation"})
|
|
38
|
+
params = payload["params"]
|
|
39
|
+
if isinstance(params, dict) and params.get("schema") == REQUEST_SCHEMA and "profile" in params:
|
|
40
|
+
return _envelope(request_id, error={"code": "SCHEMA_INVALID", "message": "v1 request cannot contain profile"})
|
|
41
|
+
if isinstance(params, dict) and (set(params) - _REQUEST_FIELDS):
|
|
42
|
+
return _envelope(request_id, error={"code": "UNKNOWN_REQUEST_FIELD", "message": "request body contains unknown fields"})
|
|
43
|
+
return _envelope(request_id, result=build_plan(params).as_dict())
|
|
44
|
+
except PlanError as error:
|
|
45
|
+
return _envelope(request_id, error={"code": "SCHEMA_INVALID", "message": str(error)})
|
|
46
|
+
except Exception:
|
|
47
|
+
return _envelope(request_id, error={"code": "INTERNAL_ERROR", "message": "bootstrap provider failed unexpectedly"})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> int:
|
|
51
|
+
# Exit code 0 is stable for every processed line, including structured errors.
|
|
52
|
+
for line in sys.stdin:
|
|
53
|
+
try:
|
|
54
|
+
payload = json.loads(line)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
response = _envelope(None, error={"code": "MALFORMED_JSON", "message": "request must be valid JSON"})
|
|
57
|
+
else:
|
|
58
|
+
response = handle(payload)
|
|
59
|
+
sys.stdout.write(json.dumps(response, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
|
|
60
|
+
sys.stdout.flush()
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
raise SystemExit(main())
|
|
@@ -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_core {
|
|
8
|
+
pkg:cf_bootstrap_core a cfpkg:CfPackage ;
|
|
9
|
+
cfpkg:hasPackageManifest [ a cfpkg:PackageManifest ; cfpkg:hasDistributionName [ a cfpkg:DistributionName ; rdf:value "cf-bootstrap-core" ] ; cfpkg:hasPythonPackageName [ a cfpkg:PythonPackageName ; rdf:value "cf_bootstrap_core" ] ; 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-core" ; skos:definition "An adopted Cogniflow package conforming to the basic package template." ; skos:scopeNote "Package semantics were restored through the package conformance repair service." ; skos:example "The installed package publishes its package.trig semantic source." .
|
|
13
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cf-bootstrap-core
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: Pure canonical Cogniflow bootstrap lifecycle and instance-plan model.
|
|
5
|
+
Author: Cogniflow Maintainers
|
|
6
|
+
License: GPL-3
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# cf-bootstrap-core
|
|
13
|
+
|
|
14
|
+
`cf_bootstrap_core` is a pure, standard-library-only model for the deterministic bootstrap plan. It validates versioned requests and returns a canonical plan without creating files, directories, locks, environments, manifests, or runtime state.
|
|
15
|
+
|
|
16
|
+
## Boundary
|
|
17
|
+
|
|
18
|
+
A bootstrap plan is intended lifecycle data before side effects: identity, caller-supplied Cogniflow home context, external installation root, artifact source, and the ordered stages `validate-inputs`, `select-artifact-source`, `prepare-installation`, `install-artifacts`, `verify-installation`, and `handoff-to-service`. The handoff is ordinally explicit and is the end of imperative bootstrap responsibility. Stage names describe future work only; this package does not perform it.
|
|
19
|
+
|
|
20
|
+
A runtime manifest is operational state owned solely by `cf_runtime`. This package never creates or interprets one, writes beneath `.cogniflow`, duplicates `CogniflowHome` or `RuntimePaths`, or derives the internal `.cogniflow` layout. The installation root is an explicit external location and is not created during planning. Paths are validated lexically without filesystem access; `.` segments are normalized by `PurePath`, while `..` segments are rejected before component-aware containment, so nonexistent targets remain valid and traversal cannot bypass containment.
|
|
21
|
+
|
|
22
|
+
## Identity and protocol
|
|
23
|
+
|
|
24
|
+
`instance_id` is caller-supplied ASCII text of 1-64 characters matching `[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?`. It is case-sensitive, may contain internal dots, hyphens, and underscores, and has no path or environment meaning. No random, timestamp, process, host, working-directory, or checkout-derived identity is generated.
|
|
25
|
+
|
|
26
|
+
The package-local `cf-bootstrap-core-provider` accepts `cf.bootstrap.provider.v1` JSON-RPC-shaped JSON-lines requests with method `bootstrap/plan`. Each input line produces one response line. Exit code is `0` for empty input, successful requests, schema/protocol errors, malformed JSON, and unexpected internal errors after the lines are processed. stdout contains only compact canonical JSON; diagnostics are not written to stdout. Error codes are `MALFORMED_JSON`, `INVALID_ENVELOPE`, `PROTOCOL_VERSION_UNSUPPORTED`, `UNKNOWN_OPERATION`, `SCHEMA_INVALID`, and `INTERNAL_ERROR`.
|
|
27
|
+
|
|
28
|
+
`UNKNOWN_ENVELOPE_FIELD` means an envelope contains an unrecognized field; `INVALID_ENVELOPE` means its object shape, required fields, or field types are invalid; `UNKNOWN_REQUEST_FIELD` means the plan request body contains an unrecognized field; `SCHEMA_INVALID` means the request values violate the versioned model. `MALFORMED_JSON`, `PROTOCOL_VERSION_UNSUPPORTED`, `UNKNOWN_OPERATION`, and `INTERNAL_ERROR` retain their literal meanings.
|
|
29
|
+
|
|
30
|
+
Canonical JSON is UTF-8 JSON with sorted object keys, compact separators, deterministic list order, no volatile values, and exactly one LF terminator. Only `local-repository` and `pypi` source modes are accepted. No package is built, resolved, downloaded, installed, or started.
|
|
31
|
+
|
|
32
|
+
The public `cogniflow` launcher discovers this provider through the
|
|
33
|
+
`cogniflow.bootstrap_providers.v1` distribution entry-point group and invokes
|
|
34
|
+
the matching `cf-bootstrap-core-provider` console script as an external
|
|
35
|
+
process. The launcher does not import this package. Artifact resolver and
|
|
36
|
+
frontend discovery groups are reserved as `cogniflow.artifact_resolvers.v1`
|
|
37
|
+
and `cogniflow.frontends.v1`; they are not implemented here.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cf_bootstrap_core/__init__.py,sha256=klFSZWs6XvEgZyrlvYvO9yudMT3ftPYhUl9thpJBOeE,802
|
|
2
|
+
cf_bootstrap_core/model.py,sha256=N0Y_g-yUzNKFDJOBIfHygE0ekKw4yEtstDMZOQs9CNE,9810
|
|
3
|
+
cf_bootstrap_core/provider.py,sha256=6e0vYGISRbd8nwv9L2Rescdgbaypo3Z1aMm_z3J5iyE,3517
|
|
4
|
+
cf_bootstrap_core/semantics/package.trig,sha256=8nbEh50HlUAC2fgG53YaGe0wkNf95H1plBqeIbhHD7E,1208
|
|
5
|
+
cf_bootstrap_core-0.1.8.dist-info/METADATA,sha256=aErPfztwF2SRf9GFoLQGiPCBr36Vc7BKaemjQCBcBXA,3743
|
|
6
|
+
cf_bootstrap_core-0.1.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
7
|
+
cf_bootstrap_core-0.1.8.dist-info/entry_points.txt,sha256=Y8GzfgXclRNi9yeIu8hWhdg6nTQgwJ6ugSKB5Rwqy0c,259
|
|
8
|
+
cf_bootstrap_core-0.1.8.dist-info/RECORD,,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
cf-bootstrap-core-provider = cf_bootstrap_core.provider:main
|
|
3
|
+
|
|
4
|
+
[cogniflow.bootstrap_providers.v1]
|
|
5
|
+
cf-bootstrap-core-provider = cf_bootstrap_core.provider:main
|
|
6
|
+
|
|
7
|
+
[cogniflow.semantic_sources]
|
|
8
|
+
cf-bootstrap-core = cf_bootstrap_core:semantic_files
|