cf-bootstrap-instance 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,358 @@
1
+ """Independent target-environment inventory, provenance, and semantic checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ import re
8
+
9
+ from .model import InstanceError
10
+ from .process import run
11
+
12
+
13
+ _CHECK = r'''
14
+ import json, os, pathlib, stat, subprocess, sys
15
+ from importlib import metadata
16
+
17
+ environment = pathlib.Path(sys.argv[1]).resolve()
18
+ expected = json.loads(sys.argv[2])
19
+ checkout = pathlib.Path(sys.argv[3]).resolve() if sys.argv[3] else None
20
+ native_expected = json.loads(sys.argv[5])
21
+ native_by_name = {item["name"]: item for item in native_expected}
22
+ site = pathlib.Path(next(item for item in sys.path if item and "site-packages" in pathlib.Path(item).parts)).resolve()
23
+ scripts = environment / ("Scripts" if sys.platform == "win32" else "bin")
24
+ if pathlib.Path(sys.executable).resolve() != pathlib.Path(sys.argv[4]).resolve() or pathlib.Path(sys.prefix).resolve() != environment:
25
+ raise RuntimeError("target interpreter is not bound to the target environment")
26
+ if checkout and any(pathlib.Path(item).resolve() == checkout or checkout in pathlib.Path(item).resolve().parents for item in sys.path):
27
+ raise RuntimeError("checkout appears in target sys.path")
28
+
29
+ distributions = list(metadata.distributions())
30
+ actual = sorted([{"name": d.metadata.get("Name"), "version": d.version} for d in distributions], key=lambda item: item["name"].lower())
31
+ if actual != expected:
32
+ raise RuntimeError("installed distribution inventory differs from artifact set")
33
+ for distribution in distributions:
34
+ location = pathlib.Path(distribution.locate_file("")).resolve()
35
+ if site not in location.parents and location != site:
36
+ raise RuntimeError("distribution is outside target site-packages")
37
+ for file in distribution.files or ():
38
+ path = (location / file).resolve()
39
+ if site not in path.parents and path != site and scripts not in path.parents:
40
+ raise RuntimeError("distribution file escaped site-packages")
41
+ for file in distribution.files or ():
42
+ if file.name in ("direct_url.json",) or file.suffix == ".pth" or file.name.endswith(".egg-link"):
43
+ raise RuntimeError("forbidden editable/provenance metadata found")
44
+ for entry in distribution.entry_points:
45
+ if entry.group == "console_scripts":
46
+ command = scripts / entry.name
47
+ if sys.platform == "win32":
48
+ command = command.with_suffix(".exe")
49
+ if not command.is_file() or command.resolve().parent != scripts.resolve():
50
+ raise RuntimeError("console script is not in target Scripts directory")
51
+
52
+ semantic_entries = list(metadata.entry_points(group="cogniflow.semantic_sources"))
53
+ by_distribution = {}
54
+ semantic_files = {}
55
+ for entry in semantic_entries:
56
+ dist = entry.dist
57
+ if dist is None or not dist.metadata.get("Name") or entry.name in by_distribution:
58
+ raise RuntimeError("semantic source entry point is ambiguous")
59
+ by_distribution[entry.name] = entry
60
+ for expected_item in expected:
61
+ name = expected_item["name"]
62
+ if name in native_by_name or name == "cf-service-mcp-server":
63
+ continue
64
+ if not (name.lower().startswith("cf-") or name.lower() == "cogniflow"):
65
+ continue
66
+ matches = [entry for entry in semantic_entries if entry.dist.metadata.get("Name") == name]
67
+ if len(matches) != 1:
68
+ raise RuntimeError(f"expected distribution has no unique semantic source: {name}")
69
+ files = matches[0].load()()
70
+ if not files:
71
+ raise RuntimeError("semantic source is empty")
72
+ for file in files:
73
+ path = pathlib.Path(file).resolve()
74
+ if site not in path.parents or not path.is_file() or path.suffix != ".trig":
75
+ raise RuntimeError(f"semantic source escaped target package: {path} (site={site})")
76
+ semantic_files[name] = [str(path) for path in files]
77
+
78
+ for item in native_expected:
79
+ name = item["name"]
80
+ distribution = metadata.distribution(name)
81
+ if distribution.version != item["version"]:
82
+ raise RuntimeError("native distribution version differs from artifact metadata")
83
+ command = scripts / pathlib.PurePosixPath(item["executable"]).name
84
+ try:
85
+ info = command.lstat()
86
+ except OSError as error:
87
+ raise RuntimeError("native executable is missing") from error
88
+ if command.is_symlink() or not stat.S_ISREG(info.st_mode) or (sys.platform == "win32" and getattr(info, "st_file_attributes", 0) & 0x400):
89
+ raise RuntimeError("native executable is not a regular target file")
90
+ binary = command.read_bytes()
91
+ if (sys.platform == "win32" and not binary.startswith(b"MZ")) or (sys.platform != "win32" and not binary.startswith(b"\x7fELF")):
92
+ raise RuntimeError("installed native executable has the wrong file type")
93
+ version_env = os.environ.copy()
94
+ version_env.pop("COGNIFLOW_RUNTIME_MANIFEST", None)
95
+ version_env["PATH"] = str(scripts)
96
+ version_env["CARGO_HOME"] = str(environment / "missing-cargo-home")
97
+ version_env["RUSTUP_HOME"] = str(environment / "missing-rustup-home")
98
+ version_env.pop("RUSTUP_TOOLCHAIN", None)
99
+ result = subprocess.run([str(command), "--version"], cwd=environment, env=version_env, capture_output=True, text=True, check=False, timeout=10)
100
+ if result.returncode != 0 or result.stderr or result.stdout != f"{name} {item['version']}\n":
101
+ raise RuntimeError("installed native executable version check failed")
102
+ semantic_files["__native__"] = native_expected
103
+ print(json.dumps({"inventory": actual, "semantic_files": semantic_files}, sort_keys=True))
104
+ '''
105
+
106
+
107
+ def inventory(executable: Path, environment: Path, artifact_set: dict[str, object], checkout: Path | None = None) -> list[dict[str, str]]:
108
+ expected = sorted([{"name": item["distribution"], "version": item["version"]} for item in artifact_set["artifacts"]], key=lambda item: item["name"].lower())
109
+ native_expected = [{"name": item["distribution"], "version": item["version"], "executable": item["executable"]} for item in artifact_set["artifacts"] if item.get("artifact_kind") == "native"]
110
+ result = run([str(executable), "-c", _CHECK, str(environment), json.dumps(expected, sort_keys=True), str(checkout or ""), str(executable), json.dumps(native_expected, sort_keys=True)], timeout=60, cwd=environment)
111
+ try:
112
+ report = json.loads(result.stdout)
113
+ actual = report["inventory"]
114
+ except ValueError as error:
115
+ raise InstanceError("VERIFY_FAILED", "target verification was not JSON") from error
116
+ if actual != expected:
117
+ raise InstanceError("VERIFY_FAILED", "target inventory differs from expected inventory")
118
+ _verify_semantics(report["semantic_files"], expected)
119
+ return actual
120
+
121
+
122
+ def _verify_semantics(files_by_distribution: object, expected: list[dict[str, str]]) -> None:
123
+ if not isinstance(files_by_distribution, dict):
124
+ raise InstanceError("VERIFY_FAILED", "semantic source report is invalid")
125
+ try:
126
+ from rdflib import Dataset, Literal, URIRef
127
+ except ModuleNotFoundError:
128
+ _verify_semantics_without_rdflib(files_by_distribution, expected)
129
+ return
130
+ cf = URIRef("https://cogniflow.odea-project.org/cf#")
131
+ rdf_type = URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
132
+ rdf_value = URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#value")
133
+ def objects(subject: object, predicate: object) -> list[object]:
134
+ return [obj for _, _, obj, _ in dataset.quads((subject, predicate, None, None))]
135
+ def subjects(predicate: object, obj: object) -> list[object]:
136
+ return [subject for subject, _, _, _ in dataset.quads((None, predicate, obj, None))]
137
+ for item in expected:
138
+ name = item["name"]
139
+ if name == "cf-service-mcp-server" or any(item.get("name") == name for item in files_by_distribution.get("__native__", [])):
140
+ continue
141
+ if not (name.lower().startswith("cf-") or name.lower() == "cogniflow"):
142
+ continue
143
+ files = files_by_distribution.get(name)
144
+ if not isinstance(files, list) or not files:
145
+ raise InstanceError("VERIFY_FAILED", f"semantic source is empty for {name}")
146
+ package_files = [Path(file) for file in files if Path(file).name == "package.trig"]
147
+ if len(package_files) != 1:
148
+ raise InstanceError("VERIFY_FAILED", f"semantic package manifest is not unique for {name}")
149
+ path = package_files[0]
150
+ try:
151
+ dataset = Dataset()
152
+ dataset.parse(path.as_posix(), format="trig")
153
+ except Exception as error:
154
+ raise InstanceError("VERIFY_FAILED", "semantic TriG is not parseable") from error
155
+ packages = subjects(rdf_type, cf + "CfPackage")
156
+ if len(packages) != 1:
157
+ raise InstanceError("VERIFY_FAILED", "semantic source does not contain exactly one CfPackage")
158
+ manifests = objects(packages[0], cf + "hasPackageManifest")
159
+ if len(manifests) != 1:
160
+ raise InstanceError("VERIFY_FAILED", "semantic source has no unique package manifest")
161
+ distributions = objects(manifests[0], cf + "hasDistributionName")
162
+ versions = objects(manifests[0], cf + "hasPackageVersion")
163
+ values = objects(distributions[0], rdf_value) if len(distributions) == 1 else []
164
+ version_values = objects(versions[0], rdf_value) if len(versions) == 1 else []
165
+ if values != [Literal(name)] or version_values != [Literal(item["version"])] or path.name != "package.trig":
166
+ raise InstanceError("VERIFY_FAILED", "semantic package manifest does not match distribution")
167
+
168
+
169
+ def _verify_semantics_without_rdflib(files_by_distribution: dict[str, object], expected: list[dict[str, str]]) -> None:
170
+ """Keep installed-provider verification dependency-free when rdflib is absent."""
171
+ for item in expected:
172
+ name = item["name"]
173
+ if any(item.get("name") == name for item in files_by_distribution.get("__native__", [])):
174
+ continue
175
+ if not (name.lower().startswith("cf-") or name.lower() == "cogniflow"):
176
+ continue
177
+ files = files_by_distribution.get(name)
178
+ if not isinstance(files, list) or not files:
179
+ raise InstanceError("VERIFY_FAILED", f"semantic source is empty for {name}")
180
+ package_files = [Path(file) for file in files if Path(file).name == "package.trig"]
181
+ if len(package_files) != 1:
182
+ raise InstanceError("VERIFY_FAILED", f"semantic package manifest is not unique for {name}")
183
+ path = package_files[0]
184
+ if not path.is_file():
185
+ raise InstanceError("VERIFY_FAILED", "semantic source is not a package.trig file")
186
+ text = path.read_text(encoding="utf-8")
187
+ if not _minimal_trig_structure(text, name, item["version"]):
188
+ raise InstanceError("VERIFY_FAILED", "semantic source is missing required manifest data")
189
+
190
+
191
+ def _minimal_trig_structure(text: str, name: str, version: str) -> bool:
192
+ """Parse the strict package-manifest TriG subset without rdflib."""
193
+ prefixes, body = _parse_prefixes(text)
194
+ if prefixes is None:
195
+ return False
196
+ tokens = _trig_tokens(body)
197
+ if tokens is None:
198
+ return False
199
+ parser = _ManifestParser(tokens, name, version)
200
+ return parser.parse() and parser.position == len(tokens)
201
+
202
+
203
+ def _parse_prefixes(text: str) -> tuple[dict[str, str], str] | tuple[None, str]:
204
+ prefix_pattern = re.compile(r"@prefix ([A-Za-z_][A-Za-z0-9_-]*): <([^>]+)> \.")
205
+ prefixes: dict[str, str] = {}
206
+ lines = text.splitlines()
207
+ body_start = 0
208
+ for index, line in enumerate(lines):
209
+ if not line.strip():
210
+ continue
211
+ match = prefix_pattern.fullmatch(line.strip())
212
+ if match is None:
213
+ body_start = index
214
+ break
215
+ prefix, namespace = match.groups()
216
+ if prefix in prefixes or prefix not in {"cf", "cfproc", "cfservice", "cfpkg", "rdf", "pkg", "contribution", "dcterms", "skos", "xsd"}:
217
+ return None, ""
218
+ prefixes[prefix] = namespace
219
+ body_start = index + 1
220
+ expected = {"cf": "https://cogniflow.odea-project.org/cf#", "cfproc": "https://cogniflow.odea-project.org/cf#", "cfservice": "https://cogniflow.odea-project.org/cf#", "cfpkg": "https://cogniflow.odea-project.org/cf#", "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "pkg": "urn:cf:pkg:", "contribution": "urn:cf:contribution:", "dcterms": "http://purl.org/dc/terms/", "skos": "http://www.w3.org/2004/02/skos/core#", "xsd": "http://www.w3.org/2001/XMLSchema#"}
221
+ if any(prefix in prefixes and prefixes[prefix] != value for prefix, value in expected.items()) or not {"cfpkg", "rdf", "pkg"} <= prefixes.keys():
222
+ return None, ""
223
+ return prefixes, "\n".join(lines[body_start:])
224
+
225
+
226
+ def _trig_tokens(text: str) -> list[str] | None:
227
+ token_pattern = re.compile(r'\s+|"(?:\\.|[^"\\])*"(?:\^\^[A-Za-z_][A-Za-z0-9_.-]*:[A-Za-z0-9_.-]+)?|<[^>]*>|[{}\[\];.]|[A-Za-z_][A-Za-z0-9_.-]*:[A-Za-z0-9_.-]+|a\b')
228
+ tokens: list[str] = []
229
+ position = 0
230
+ while position < len(text):
231
+ match = token_pattern.match(text, position)
232
+ if match is None:
233
+ return None
234
+ token = match.group(0)
235
+ if not token.isspace():
236
+ tokens.append(token)
237
+ position = match.end()
238
+ return tokens
239
+
240
+
241
+ class _ManifestParser:
242
+ _manifest_fields = {"cfpkg:hasDistributionName": "DistributionName", "cfpkg:hasPythonPackageName": "PythonPackageName", "cfpkg:hasPackageVersion": "PackageVersion", "cfpkg:hasImplementationLanguage": "ImplementationLanguage"}
243
+ _top_fields = {"cfpkg:hasPackageRole", "cfpkg:hasPackageContribution", "skos:prefLabel", "skos:definition", "skos:scopeNote", "skos:example", "dcterms:created"}
244
+
245
+ def __init__(self, tokens: list[str], name: str, version: str) -> None:
246
+ self.tokens = tokens
247
+ self.position = 0
248
+ self.name = name
249
+ self.version = version
250
+
251
+ def take(self) -> str | None:
252
+ if self.position >= len(self.tokens):
253
+ return None
254
+ token = self.tokens[self.position]
255
+ self.position += 1
256
+ return token
257
+
258
+ def expect(self, expected: str) -> bool:
259
+ return self.take() == expected
260
+
261
+ def parse(self) -> bool:
262
+ if self.tokens.count("cfpkg:CfPackage") != 1:
263
+ return False
264
+ graph = self.take()
265
+ if graph is None or not graph.startswith("pkg:") or not self.expect("{") or self.take() != graph or not self.expect("a") or not self.expect("cfpkg:CfPackage") or not self.expect(";"):
266
+ return False
267
+ manifest_count = 0
268
+ package_terminated = False
269
+ while self.position < len(self.tokens) and self.tokens[self.position] != "}":
270
+ predicate = self.take()
271
+ if predicate == "cfpkg:hasPackageManifest":
272
+ manifest_count += 1
273
+ if not self.parse_manifest():
274
+ return False
275
+ elif predicate in self._top_fields:
276
+ if not self.skip_object():
277
+ return False
278
+ else:
279
+ return False
280
+ terminator = self.take()
281
+ if terminator not in {";", "."}:
282
+ return False
283
+ if terminator == ".":
284
+ package_terminated = True
285
+ break
286
+ if not package_terminated or manifest_count != 1:
287
+ return False
288
+ while self.position < len(self.tokens) and self.tokens[self.position] != "}":
289
+ if self.take() is None or not self.expect("a") or self.take() is None or not self.expect(";"):
290
+ return False
291
+ statement_terminated = False
292
+ while self.position < len(self.tokens):
293
+ if self.tokens[self.position] == ".":
294
+ self.take()
295
+ statement_terminated = True
296
+ break
297
+ if self.tokens[self.position] == "}":
298
+ return False
299
+ if self.take() is None or not self.skip_object():
300
+ return False
301
+ terminator = self.take()
302
+ if terminator not in {";", "."}:
303
+ return False
304
+ if terminator == ".":
305
+ statement_terminated = True
306
+ break
307
+ if not statement_terminated:
308
+ return False
309
+ return self.expect("}")
310
+
311
+ def parse_manifest(self) -> bool:
312
+ if not self.expect("[") or not self.expect("a") or not self.expect("cfpkg:PackageManifest") or not self.expect(";"):
313
+ return False
314
+ fields: dict[str, str] = {}
315
+ closed = False
316
+ while self.position < len(self.tokens) and self.tokens[self.position] != "]":
317
+ predicate = self.take()
318
+ expected_type = self._manifest_fields.get(predicate or "")
319
+ if expected_type is None or predicate in fields:
320
+ return False
321
+ value = self.parse_value_resource(expected_type)
322
+ if value is None:
323
+ return False
324
+ fields[predicate] = value
325
+ terminator = self.take()
326
+ if terminator not in {";", "]"}:
327
+ return False
328
+ if terminator == "]":
329
+ closed = True
330
+ break
331
+ required = {"cfpkg:hasDistributionName", "cfpkg:hasPackageVersion", "cfpkg:hasImplementationLanguage"}
332
+ if not closed and self.expect("]"):
333
+ closed = True
334
+ return closed and required <= fields.keys() and fields["cfpkg:hasDistributionName"] == self.name and fields["cfpkg:hasPackageVersion"] == self.version and fields["cfpkg:hasImplementationLanguage"] == "Python"
335
+
336
+ def parse_value_resource(self, expected_type: str) -> str | None:
337
+ if not self.expect("[") or not self.expect("a") or not self.expect(f"cfpkg:{expected_type}") or not self.expect(";") or not self.expect("rdf:value"):
338
+ return None
339
+ value = self.take()
340
+ if value is None or not value.startswith('"') or not self.expect("]"):
341
+ return None
342
+ return value[1:-1]
343
+
344
+ def skip_object(self) -> bool:
345
+ if self.position >= len(self.tokens):
346
+ return False
347
+ if self.tokens[self.position] != "[":
348
+ return self.take() is not None
349
+ depth = 0
350
+ while self.position < len(self.tokens):
351
+ token = self.take()
352
+ if token == "[":
353
+ depth += 1
354
+ elif token == "]":
355
+ depth -= 1
356
+ if depth == 0:
357
+ return True
358
+ return False
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: cf-bootstrap-instance
3
+ Version: 0.1.8
4
+ Summary: Stable, non-editable Cogniflow instance environment bootstrapper.
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: cf-service-client==0.1.8
@@ -0,0 +1,18 @@
1
+ cf_bootstrap_instance/__init__.py,sha256=w8v2lK8uQkR8mUszqfItidsgEARz1GerYfqd1Sy_ELw,300
2
+ cf_bootstrap_instance/handoff.py,sha256=YeevOTt8sfp0yuZlhyP-sKS8Q20p04VjdB3ju2YzO1M,10504
3
+ cf_bootstrap_instance/install.py,sha256=MM6ASCEEL7wIczn_HTNcGws9UkV_8dQ15jcbdKV5GS4,5393
4
+ cf_bootstrap_instance/layout.py,sha256=doBDoAOmrmEJ9BS6i0Ocsy2ETjdIuIfddPsJqx5l2QQ,8581
5
+ cf_bootstrap_instance/model.py,sha256=D3I8X0D-MbNFPI-uzZQr_uTeZ3-WF7B4-j9itgXuV3Y,24606
6
+ cf_bootstrap_instance/process.py,sha256=_ZtyZF6RP2PHn3WcMuxA1wUzE7JKNiqXaSwdsgBliE0,2007
7
+ cf_bootstrap_instance/provider.py,sha256=9B0L4sm8NAODZ8veTL4bUG7xRjjdKJtv6pml7XIbZJU,41754
8
+ cf_bootstrap_instance/recovery.py,sha256=Q1iD3l4jJXV6JIj6IE4hERB_A4BzBR5DbbBJEcNe2J0,21703
9
+ cf_bootstrap_instance/service_executor.py,sha256=Wk2JYPRS4F5D5pg08RYouOiGcP3Ml5Y5ggS8zVmqD68,12139
10
+ cf_bootstrap_instance/transition_authority.py,sha256=CWDdUhnWgOK1OSglq3g7F26Gh8i5-gKHCYSlqrAp200,8471
11
+ cf_bootstrap_instance/uninstall_authority.py,sha256=EXUl3P0lL4LPRlUYvTw9qxRCKk4e237tWuWiCj3Cw9E,6134
12
+ cf_bootstrap_instance/verify.py,sha256=H5zHj59uu5uTY81RJZ6zvYd0QeqHfVMWXrLRq19To9Q,18836
13
+ cf_bootstrap_instance/semantics/package.trig,sha256=JORaCC2YX_8TkTV457YbZmh2i8x5z0EJAGz-s1ufkow,1436
14
+ cf_bootstrap_instance/semantics/service.trig,sha256=-27WTCCrUxubqyFF76s66Vv3fT7bofjRCfZyBCY9adY,13321
15
+ cf_bootstrap_instance-0.1.8.dist-info/METADATA,sha256=GNW6wI6En6k3f-sQfakys2wBFg_kb6ZoCwErSKUPx_s,204
16
+ cf_bootstrap_instance-0.1.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
17
+ cf_bootstrap_instance-0.1.8.dist-info/entry_points.txt,sha256=tcUsw9ZwYrt2PcaU1vxfybowlsvbWvRF5y7cNJiL3Zk,415
18
+ cf_bootstrap_instance-0.1.8.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,10 @@
1
+ [console_scripts]
2
+ cf-bootstrap-instance-handoff = cf_bootstrap_instance.handoff:main
3
+ cf-bootstrap-instance-provider = cf_bootstrap_instance.provider:main
4
+ cf-bootstrap-runtime-repair = cf_bootstrap_instance.recovery:main
5
+
6
+ [cogniflow.instance_providers.v1]
7
+ cf-bootstrap-instance-provider = cf_bootstrap_instance.provider:main
8
+
9
+ [cogniflow.semantic_sources]
10
+ cf-bootstrap-instance = cf_bootstrap_instance:semantic_files