cf-bootstrap-source-local 0.1.0__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,255 @@
1
+ """Deterministic direct-project inventory and index validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ import tomllib
11
+ from packaging.requirements import InvalidRequirement, Requirement
12
+ from packaging.specifiers import SpecifierSet
13
+ from packaging.version import Version
14
+
15
+ from .model import ResolverError, canonical_json, normalize_distribution
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class Project:
20
+ name: str
21
+ normalized_name: str
22
+ version: str
23
+ path: Path
24
+ relative: str
25
+ dependencies: tuple[str, ...] = ()
26
+ optional_dependencies: tuple[tuple[str, tuple[str, ...]], ...] = ()
27
+ build_requirements: tuple[str, ...] = ()
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class NativeProject:
32
+ crate_path: Path
33
+ relative: str
34
+ distribution: str
35
+ binary: str
36
+ version: str
37
+ platforms: tuple[str, ...]
38
+ wheel_tags: tuple[str, ...] = ()
39
+ executable: str | None = None
40
+ discovery_group: str | None = None
41
+ entry_point: str | None = None
42
+
43
+ @property
44
+ def normalized_name(self) -> str:
45
+ return normalize_distribution(self.distribution)
46
+
47
+
48
+ def normalize(name: str) -> str:
49
+ return normalize_distribution(name)
50
+
51
+
52
+ def _validate_symlinks(root: Path) -> None:
53
+ for current, directories, files in os.walk(root, followlinks=False):
54
+ for name in [*directories, *files]:
55
+ path = Path(current) / name
56
+ if path.is_symlink():
57
+ try:
58
+ path.resolve().relative_to(root.resolve())
59
+ except ValueError as error:
60
+ raise ResolverError("SOURCE_PROJECT_INVALID", "project symlink escapes source tree") from error
61
+
62
+
63
+ def _validate_package_conformance(path: Path, name: str, version: str) -> None:
64
+ try:
65
+ data = tomllib.loads((path / "pyproject.toml").read_text(encoding="utf-8"))
66
+ package = next(item.split("/")[-1] for item in data.get("tool", {}).get("hatch", {}).get("build", {}).get("targets", {}).get("wheel", {}).get("packages", []) if isinstance(item, str) and item.startswith("src/"))
67
+ except (OSError, KeyError, TypeError, StopIteration, tomllib.TOMLDecodeError) as error:
68
+ raise ResolverError("SOURCE_CONFORMANCE_INVALID", "project package layout is not conforming") from error
69
+ semantic = path / "src" / package / "semantics" / "package.trig"
70
+ init = path / "src" / package / "__init__.py"
71
+ if not semantic.is_file() or not init.is_file():
72
+ raise ResolverError("SOURCE_CONFORMANCE_INVALID", f"project {path.name} has no package semantic source")
73
+ text = semantic.read_text(encoding="utf-8")
74
+ if text.count("cfpkg:CfPackage") != 1 or str(name) not in text or str(version) not in text or f'rdf:value "{package}"' not in text or 'rdf:value "Python"' not in text:
75
+ raise ResolverError("SOURCE_CONFORMANCE_INVALID", f"project {path.name} package semantic manifest is invalid")
76
+ entries = data.get("project", {}).get("entry-points", {}).get("cogniflow.semantic_sources", {})
77
+ if not isinstance(entries, dict) or entries.get(name) != f"{package}:semantic_files" or "def semantic_files" not in init.read_text(encoding="utf-8"):
78
+ raise ResolverError("SOURCE_CONFORMANCE_INVALID", f"project {path.name} has no semantic source entry point")
79
+
80
+
81
+ def inventory(checkout: Path) -> tuple[tuple[Project, ...], str, str]:
82
+ root = checkout / "stonecastle"
83
+ if not root.is_dir():
84
+ raise ResolverError("SOURCE_INVALID", "checkout has no stonecastle directory")
85
+ projects: list[Project] = []
86
+ for child in sorted(root.iterdir(), key=lambda p: p.name.casefold()):
87
+ if not child.is_dir() or not (child / "pyproject.toml").is_file():
88
+ continue
89
+ try:
90
+ real = child.resolve()
91
+ real.relative_to(root.resolve())
92
+ data = tomllib.loads((child / "pyproject.toml").read_text(encoding="utf-8"))
93
+ project = data["project"]
94
+ name, version = project["name"], project["version"]
95
+ except (OSError, KeyError, TypeError, tomllib.TOMLDecodeError) as error:
96
+ raise ResolverError("SOURCE_PROJECT_INVALID", f"invalid project metadata: {child.name}") from error
97
+ if not isinstance(name, str) or not isinstance(version, str) or not name or not version:
98
+ raise ResolverError("SOURCE_PROJECT_INVALID", "project name and version must be strings")
99
+ normalized = normalize(name)
100
+ relative = child.relative_to(checkout).as_posix()
101
+ if any(item.normalized_name == normalized for item in projects):
102
+ raise ResolverError("SOURCE_PROJECT_INVALID", "duplicate normalized distribution name")
103
+ _validate_package_conformance(child, name, version)
104
+ dependencies = project.get("dependencies", [])
105
+ optional = data.get("project", {}).get("optional-dependencies", {})
106
+ build = data.get("build-system", {}).get("requires", [])
107
+ if not isinstance(dependencies, list) or any(not isinstance(item, str) for item in dependencies):
108
+ raise ResolverError("SOURCE_PROJECT_INVALID", f"project {child.name} dependencies are invalid")
109
+ if not isinstance(build, list) or any(not isinstance(item, str) for item in build):
110
+ raise ResolverError("SOURCE_PROJECT_INVALID", f"project {child.name} build requirements are invalid")
111
+ optional_items = tuple(sorted((str(group), tuple(values)) for group, values in optional.items() if isinstance(group, str) and isinstance(values, list) and all(isinstance(item, str) for item in values)))
112
+ projects.append(Project(name, normalized, version, child, relative, tuple(dependencies), optional_items, tuple(build)))
113
+ projects.sort(key=lambda item: (item.normalized_name, item.relative))
114
+ for project in projects:
115
+ _validate_symlinks(project.path)
116
+ index_path = root / "stonecastle.index.json"
117
+ try:
118
+ index = json.loads(index_path.read_text(encoding="utf-8"))
119
+ entries = [item for item in index["files"] if item.get("kind") == "project-metadata"]
120
+ except (OSError, KeyError, TypeError, json.JSONDecodeError) as error:
121
+ raise ResolverError("SOURCE_INDEX_INVALID", "stonecastle index is invalid") from error
122
+ expected = {item.relative + "/pyproject.toml" for item in projects}
123
+ all_index_paths = [item.get("path") for item in entries]
124
+ if any(not isinstance(path, str) or path.replace("\\", "/") != path or Path(path).parts[:1] != ("stonecastle",) or len(Path(path).parts) != 3 or Path(path).parts[-1] != "pyproject.toml" for path in all_index_paths):
125
+ raise ResolverError("SOURCE_INDEX_INVALID", "project metadata path is not a direct pyproject")
126
+ if len(all_index_paths) != len(set(all_index_paths)) or set(all_index_paths) != expected or any(not (checkout / path).is_file() for path in all_index_paths):
127
+ raise ResolverError("SOURCE_INDEX_INVALID", "index and direct project inventory differ")
128
+ source_data = []
129
+ ignored = {".git", ".venv", "__pycache__", ".pytest_cache", "build", "dist"}
130
+ for project in projects:
131
+ files = []
132
+ for path in sorted(project.path.rglob("*")):
133
+ if not path.is_file() or any(part in ignored or part.endswith(".egg-info") for part in path.relative_to(project.path).parts):
134
+ continue
135
+ files.append({"path": path.relative_to(project.path).as_posix(), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
136
+ source_data.append({"path": project.relative, "name": project.name, "version": project.version, "files": files})
137
+ for native in inventory_natives(checkout):
138
+ native_files = []
139
+ for path in sorted(native.crate_path.rglob("*")):
140
+ if path.is_file() and not any(part in {"target", ".git"} for part in path.relative_to(native.crate_path).parts):
141
+ native_files.append({"path": path.relative_to(native.crate_path).as_posix(), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
142
+ source_data.append({"path": native.relative, "distribution": native.distribution, "binary": native.binary, "version": native.version, "platforms": list(native.platforms), "wheel_tags": list(native.wheel_tags), "executable": native.executable, "discovery_group": native.discovery_group, "entry_point": native.entry_point, "files": native_files})
143
+ source_digest = "sha256:" + hashlib.sha256(canonical_json(source_data)).hexdigest()
144
+ index_digest = "sha256:" + hashlib.sha256(index_path.read_bytes()).hexdigest()
145
+ return tuple(projects), source_digest, index_digest
146
+
147
+
148
+ def inventory_natives(checkout: Path, *, selected_kind: str = "native-service-artifact") -> tuple[NativeProject, ...]:
149
+ """Read one indexed native category; target-instance natives are the default."""
150
+ if selected_kind not in {"native-service-artifact", "native-frontend-artifact"}:
151
+ raise ResolverError("NATIVE_PROJECT_INVALID", "native project category is invalid")
152
+ declarations = {
153
+ "stonecastle/cf_service_mcp_server": ("native-service-artifact", "cf_service_mcp_server", "cf-service-mcp-server", "cf-service-mcp-server"),
154
+ "stonecastle/cf_install_tui_ratatui": ("native-frontend-artifact", "cf_install_tui_ratatui", "cogniflow-tui", "cogniflow-tui"),
155
+ }
156
+ try:
157
+ index = json.loads((checkout / "stonecastle/stonecastle.index.json").read_text(encoding="utf-8"))
158
+ indexed = sorted(item["path"] for item in index.get("files", []) if item.get("kind") == "native-project-metadata")
159
+ expected_paths = sorted(f"{path}/native-artifact.toml" for path in declarations if (checkout / path / "native-artifact.toml").is_file())
160
+ if indexed != expected_paths:
161
+ raise ValueError
162
+ projects = []
163
+ for metadata_relative in indexed:
164
+ metadata_path = checkout / metadata_relative
165
+ data = tomllib.loads(metadata_path.read_text(encoding="utf-8"))
166
+ native = data["native"]
167
+ crate_relative = native["crate_path"]
168
+ expected = declarations.get(crate_relative)
169
+ if expected is None:
170
+ raise ValueError
171
+ crate = checkout / crate_relative
172
+ cargo = tomllib.loads((crate / "Cargo.toml").read_text(encoding="utf-8"))
173
+ package = cargo["package"]
174
+ kind, package_name, distribution, binary = expected
175
+ fields = {"kind", "crate_path", "distribution", "binary", "version_source", "platforms"}
176
+ wheel_tags: tuple[str, ...] = ()
177
+ executable = discovery_group = entry_point = None
178
+ if kind == "native-frontend-artifact":
179
+ fields |= {"wheel_tags", "executable", "discovery_group", "entry_point"}
180
+ wheel_tags = tuple(native.get("wheel_tags", ()))
181
+ executable = native.get("executable")
182
+ discovery_group = native.get("discovery_group")
183
+ entry_point = native.get("entry_point")
184
+ if wheel_tags != ("py3-none-manylinux_2_39_x86_64", "py3-none-win_amd64") or (executable, discovery_group, entry_point) != (binary, "cogniflow.frontends.v1", binary):
185
+ raise ValueError
186
+ if set(native) != fields or native["kind"] != kind or native["version_source"] != "cargo.package.version" or metadata_relative != f"{crate_relative}/native-artifact.toml" or not crate.is_dir() or not (crate / "Cargo.lock").is_file() or package["name"] != package_name or package["version"] != str(package["version"]) or native["distribution"] != distribution or native["binary"] != binary or tuple(native["platforms"]) != ("linux-x86_64", "windows-x86_64"):
187
+ raise ValueError
188
+ if selected_kind == native["kind"]:
189
+ projects.append(NativeProject(crate, crate_relative, distribution, binary, str(package["version"]), tuple(native["platforms"]), wheel_tags, executable, discovery_group, entry_point))
190
+ except (OSError, KeyError, TypeError, ValueError, tomllib.TOMLDecodeError) as error:
191
+ raise ResolverError("NATIVE_PROJECT_INVALID", "native project metadata is invalid") from error
192
+ return tuple(projects)
193
+
194
+
195
+ def inventory_stage0_frontends(checkout: Path) -> tuple[NativeProject, ...]:
196
+ """Read Stage-0 presentation adapters without granting target authority."""
197
+ return inventory_natives(checkout, selected_kind="native-frontend-artifact")
198
+
199
+
200
+ def dependency_closure(projects: tuple[Project, ...], roots: tuple[str, ...], environment: dict[str, str] | None = None) -> tuple[tuple[Project, ...], tuple[str, ...]]:
201
+ """Resolve first-party runtime dependencies without importing project code."""
202
+ by_name = {project.normalized_name: project for project in projects}
203
+ selected: dict[str, Project] = {}
204
+ external: dict[str, list[tuple[str, SpecifierSet]]] = {}
205
+ visiting: set[str] = set()
206
+
207
+ def visit(name: str) -> None:
208
+ normalized = normalize_distribution(name)
209
+ if normalized in visiting:
210
+ raise ResolverError("DEPENDENCY_INVALID", "first-party dependency cycle")
211
+ project = by_name.get(normalized)
212
+ if project is None:
213
+ if normalized.startswith("cf-"):
214
+ raise ResolverError("DEPENDENCY_INVALID", f"unresolved first-party dependency: {name}")
215
+ external[normalized] = (name, SpecifierSet())
216
+ return
217
+ if normalized in selected:
218
+ return
219
+ visiting.add(normalized)
220
+ selected[normalized] = project
221
+ for raw in project.dependencies:
222
+ try:
223
+ requirement = Requirement(raw)
224
+ except InvalidRequirement as error:
225
+ raise ResolverError("DEPENDENCY_INVALID", f"invalid PEP 508 requirement: {raw}") from error
226
+ if requirement.marker is not None and not requirement.marker.evaluate(environment):
227
+ continue
228
+ normalized_requirement = normalize_distribution(requirement.name)
229
+ if normalized_requirement in by_name:
230
+ visit(requirement.name)
231
+ continue
232
+ if normalized_requirement.startswith("cf-"):
233
+ raise ResolverError("DEPENDENCY_INVALID", f"unresolved first-party dependency: {requirement}")
234
+ canonical = str(requirement)
235
+ current = external.setdefault(normalized_requirement, [])
236
+ combined = SpecifierSet(",".join(str(item[1]) for item in [*current, (canonical, requirement.specifier)] if str(item[1])))
237
+ if current and not any(_specifier_has_candidate(combined) for _ in (None,)):
238
+ raise ResolverError("DEPENDENCY_INVALID", f"contradictory external requirements for {normalized_requirement}")
239
+ current.append((canonical, SpecifierSet(str(requirement.specifier))))
240
+ visiting.remove(normalized)
241
+
242
+ for root in roots:
243
+ if normalize_distribution(root) not in by_name:
244
+ raise ResolverError("DEPENDENCY_INVALID", f"selected provider root is not inventoried: {root}")
245
+ visit(root)
246
+ return tuple(sorted(selected.values(), key=lambda project: (project.normalized_name, project.relative))), tuple(item[0] for _, values in sorted(external.items()) for item in sorted(values))
247
+
248
+
249
+ def _specifier_has_candidate(specifier: SpecifierSet) -> bool:
250
+ """Reject an intersection that has no plausible released version."""
251
+ for major in range(0, 100):
252
+ for minor in range(0, 100):
253
+ if Version(f"{major}.{minor}") in specifier:
254
+ return True
255
+ return not str(specifier)
@@ -0,0 +1,91 @@
1
+ """Strict protocol and request models for the local resolver."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path, PurePosixPath, PureWindowsPath
7
+ import re
8
+ from typing import Any
9
+
10
+ PROTOCOL = "cf.bootstrap.artifact-resolver.v1"
11
+ REQUEST_SCHEMA = "cf.bootstrap.artifact-resolution.request.v2"
12
+ RESULT_SCHEMA = "cf.bootstrap.local-artifact-resolution.v2"
13
+ ARTIFACT_SET_SCHEMA = "cf.bootstrap.artifact-set.v2"
14
+ PLAN_SCHEMA = "cf.bootstrap.plan.v2"
15
+
16
+
17
+ class ResolverError(ValueError):
18
+ def __init__(self, code: str, message: str):
19
+ super().__init__(message)
20
+ self.code = code
21
+
22
+
23
+ def canonical_json(value: object) -> bytes:
24
+ return (json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
25
+
26
+
27
+ def normalize_distribution(name: str) -> str:
28
+ """Apply the canonical PyPA distribution-name normalization."""
29
+ return re.sub(r"[-_.]+", "-", name).lower()
30
+
31
+
32
+ def _path(value: object, label: str) -> Path:
33
+ if not isinstance(value, str) or not value or "\x00" in value or "://" in value:
34
+ raise ResolverError("SCHEMA_INVALID", f"{label} is invalid")
35
+ windows = PureWindowsPath(value)
36
+ posix = PurePosixPath(value)
37
+ if ".." in windows.parts or ".." in posix.parts:
38
+ raise ResolverError("SCHEMA_INVALID", f"{label} must be an absolute non-root path")
39
+ path = windows if windows.drive or "\\" in value else posix
40
+ if not path.is_absolute() or str(path) == path.anchor:
41
+ raise ResolverError("SCHEMA_INVALID", f"{label} must be an absolute non-root path")
42
+ return Path(value)
43
+
44
+
45
+ def _contained(child: Path, parent: Path) -> bool:
46
+ try:
47
+ child.resolve().relative_to(parent.resolve())
48
+ return True
49
+ except ValueError:
50
+ return False
51
+
52
+
53
+ def parse_request(payload: object) -> tuple[dict[str, Any], Path]:
54
+ if not isinstance(payload, dict):
55
+ raise ResolverError("SCHEMA_INVALID", "request must be an object")
56
+ fields = {"schema", "bootstrap_plan", "artifact_output_root", "external_wheelhouse_path", "phase"}
57
+ if payload.get("phase") == "post-handoff-verify":
58
+ fields |= {"bound_artifact_set_digest", "bound_selection_digest", "bound_profile_digest", "bound_catalog_digest", "bound_repository_source_digest", "bound_index_digest"}
59
+ unknown = set(payload) - fields
60
+ if unknown:
61
+ raise ResolverError("UNKNOWN_REQUEST_FIELD", "request contains unknown fields")
62
+ if set(payload) != fields or payload.get("schema") != REQUEST_SCHEMA:
63
+ raise ResolverError("SCHEMA_INVALID", "request schema or required fields are invalid")
64
+ if payload.get("phase") not in {"bootstrap", "post-handoff", "post-handoff-verify"}:
65
+ raise ResolverError("SCHEMA_INVALID", "resolution phase is invalid")
66
+ plan = payload["bootstrap_plan"]
67
+ plan_fields = {"schema", "instance_id", "installation_root", "cogniflow_home", "artifact_source", "profile", "stages"}
68
+ if not isinstance(plan, dict) or set(plan) != plan_fields or plan.get("schema") != PLAN_SCHEMA:
69
+ raise ResolverError("SCHEMA_INVALID", "bootstrap plan schema is invalid")
70
+ profile = plan.get("profile")
71
+ if not isinstance(profile, str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9+.-]*:[^\s]+", profile):
72
+ raise ResolverError("SCHEMA_INVALID", "plan profile is not an absolute IRI")
73
+ source = plan.get("artifact_source")
74
+ if not isinstance(source, dict) or set(source) != {"mode", "checkout_path"} or source.get("mode") != "local-repository":
75
+ raise ResolverError("SOURCE_INVALID", "plan source must be local-repository")
76
+ checkout = _path(source.get("checkout_path"), "checkout_path")
77
+ output = _path(payload["artifact_output_root"], "artifact_output_root")
78
+ wheelhouse = _path(payload["external_wheelhouse_path"], "external_wheelhouse_path")
79
+ if _contained(output, checkout) or _contained(wheelhouse, checkout):
80
+ raise ResolverError("SCHEMA_INVALID", "artifact paths must be outside checkout")
81
+ for key in ("cogniflow_home", "installation_root"):
82
+ protected = _path(plan[key], key)
83
+ if _contained(output, protected) or _contained(wheelhouse, protected):
84
+ raise ResolverError("SCHEMA_INVALID", "artifact paths are inside a protected path")
85
+ if not checkout.is_dir() or not wheelhouse.is_dir() or wheelhouse.is_symlink():
86
+ raise ResolverError("SOURCE_INVALID", "checkout or external wheelhouse does not exist")
87
+ if payload.get("phase") != "post-handoff-verify":
88
+ for item in wheelhouse.iterdir():
89
+ if item.is_symlink() or not item.is_file() or item.suffix != ".whl":
90
+ raise ResolverError("EXTERNAL_SOURCE_FORBIDDEN", "external wheelhouse must contain regular wheels only")
91
+ return plan, output
@@ -0,0 +1,163 @@
1
+ """Locked Rust build and deterministic platform wheel assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import csv
7
+ import hashlib
8
+ import io
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ import platform
13
+ import shutil
14
+ import subprocess
15
+ import zipfile
16
+
17
+ from .inventory import NativeProject
18
+ from .model import ResolverError
19
+
20
+
21
+ def _platform() -> tuple[str, str, str]:
22
+ system = platform.system().lower()
23
+ machine = platform.machine().lower()
24
+ if system == "windows" and machine in {"amd64", "x86_64"}:
25
+ return "windows-x86_64", "win_amd64", ".exe"
26
+ if system == "linux" and machine in {"amd64", "x86_64"}:
27
+ return "linux-x86_64", "manylinux_2_39_x86_64", ""
28
+ raise ResolverError("UNSUPPORTED_PLATFORM", "native gateway supports only Windows and Linux x86_64")
29
+
30
+
31
+ def _record(files: dict[str, bytes], record_name: str) -> bytes:
32
+ rows: list[list[str]] = []
33
+ for name in sorted(files):
34
+ digest = base64.urlsafe_b64encode(hashlib.sha256(files[name]).digest()).rstrip(b"=").decode("ascii")
35
+ rows.append([name, "sha256=" + digest, str(len(files[name]))])
36
+ rows.append([record_name, "", ""])
37
+ stream = io.StringIO(newline="")
38
+ csv.writer(stream, lineterminator="\n").writerows(rows)
39
+ return stream.getvalue().encode("utf-8")
40
+
41
+
42
+ def package_native(project: NativeProject, binary: Path, staging: Path, accepted_sha256: str | None = None, authority: dict[str, str] | None = None) -> dict[str, object]:
43
+ """Assemble the canonical wheel from an authority-bound native executable."""
44
+ normalized_platform, platform_tag, suffix = _platform()
45
+ if normalized_platform not in project.platforms:
46
+ raise ResolverError("UNSUPPORTED_PLATFORM", "native metadata does not declare this platform")
47
+ if not binary.is_file() or binary.is_symlink():
48
+ raise ResolverError("NATIVE_BINARY_MISSING", "expected native executable is missing or unsafe")
49
+ payload = binary.read_bytes()
50
+ payload_sha256 = hashlib.sha256(payload).hexdigest()
51
+ if accepted_sha256 is not None and payload_sha256 != accepted_sha256:
52
+ raise ResolverError("NATIVE_AUTHORITY_MISMATCH", "installed native executable differs from accepted bootstrap authority")
53
+ if (suffix and not payload.startswith(b"MZ")) or (not suffix and not payload.startswith(b"\x7fELF")) or str(project.crate_path.resolve()).encode() in payload:
54
+ raise ResolverError("NATIVE_BINARY_INVALID", "native executable identity is invalid")
55
+ authority = {project.normalized_name: payload_sha256} if authority is None else dict(authority)
56
+ if (
57
+ authority.get(project.normalized_name) != payload_sha256
58
+ or any(
59
+ not isinstance(name, str)
60
+ or not name
61
+ or not isinstance(digest, str)
62
+ or len(digest) != 64
63
+ or any(char not in "0123456789abcdef" for char in digest)
64
+ for name, digest in authority.items()
65
+ )
66
+ ):
67
+ raise ResolverError("NATIVE_AUTHORITY_INVALID", "native packaging authority is invalid")
68
+ wheel_name = project.normalized_name.replace("-", "_")
69
+ dist_info = f"{wheel_name}-{project.version}.dist-info"
70
+ executable_name = project.binary
71
+ if project.normalized_name == "cogniflow-tui":
72
+ executable_name += "-native"
73
+ executable = f"{wheel_name}-{project.version}.data/scripts/{executable_name}{suffix}"
74
+ metadata = (f"Metadata-Version: 2.1\nName: {project.distribution}\nVersion: {project.version}\n\n").encode()
75
+ wheel = (f"Wheel-Version: 1.0\nGenerator: stonecastle-native-wheel\nRoot-Is-Purelib: false\nTag: py3-none-{platform_tag}\n\n").encode()
76
+ files = {f"{dist_info}/METADATA": metadata, f"{dist_info}/WHEEL": wheel, executable: payload}
77
+ if project.normalized_name == "cogniflow-tui":
78
+ if (project.executable, project.discovery_group, project.entry_point) != (project.binary, "cogniflow.frontends.v1", project.binary):
79
+ raise ResolverError("NATIVE_METADATA_INVALID", "frontend registration metadata is invalid")
80
+ target = "cogniflow_tui:main"
81
+ files[f"{dist_info}/entry_points.txt"] = (
82
+ f"[console_scripts]\n{project.entry_point} = {target}\n"
83
+ f"[{project.discovery_group}]\n{project.entry_point} = {target}\n"
84
+ ).encode("ascii")
85
+ files["cogniflow_tui.py"] = (
86
+ "from pathlib import Path\n"
87
+ "import os\n"
88
+ "import stat\n"
89
+ "import subprocess\n"
90
+ "import sys\n\n"
91
+ "def main():\n"
92
+ " suffix = '.exe' if sys.platform == 'win32' else ''\n"
93
+ " executable = Path(sys.executable).parent / ('cogniflow-tui-native' + suffix)\n"
94
+ " metadata = os.lstat(executable)\n"
95
+ " if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode) or (sys.platform == 'win32' and metadata.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT):\n"
96
+ " raise RuntimeError('native frontend executable is missing or unsafe')\n"
97
+ " if sys.platform == 'win32':\n"
98
+ " return subprocess.run([str(executable), *sys.argv[1:]], stdin=None, stdout=None, stderr=None, shell=False, check=False).returncode\n"
99
+ " os.execv(str(executable), [str(executable), *sys.argv[1:]])\n"
100
+ ).encode("ascii")
101
+ if project.normalized_name in {"cf-service-mcp-server", "cogniflow-tui"}:
102
+ records = [{"normalized_distribution": name, "sha256": digest} for name, digest in sorted(authority.items() if project.normalized_name == "cf-service-mcp-server" else [(project.normalized_name, payload_sha256)])]
103
+ files[f"{dist_info}/NATIVE-AUTHORITY.json"] = json.dumps({"schema": "stonecastle.native-authority.v1", "executables": records}, sort_keys=True, separators=(",", ":")).encode("utf-8")
104
+ files[f"{dist_info}/RECORD"] = _record(files, f"{dist_info}/RECORD")
105
+ filename = f"{wheel_name}-{project.version}-py3-none-{platform_tag}.whl"
106
+ output = staging / "wheels" / filename
107
+ output.parent.mkdir(parents=True, exist_ok=True)
108
+ epoch = (1980, 1, 1, 0, 0, 0)
109
+ with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
110
+ for name in sorted(files):
111
+ info = zipfile.ZipInfo(name, epoch)
112
+ info.compress_type = zipfile.ZIP_DEFLATED
113
+ info.external_attr = (0o100755 if "/scripts/" in name else 0o100644) << 16
114
+ archive.writestr(info, files[name])
115
+ return {"distribution": project.distribution, "normalized_distribution": project.normalized_name, "version": project.version, "filename": filename, "sha256": hashlib.sha256(output.read_bytes()).hexdigest(), "size_bytes": output.stat().st_size, "source_project": project.relative, "artifact_kind": "native", "wheel_tags": [f"py3-none-{platform_tag}"], "executable": executable, "discovery_group": project.discovery_group, "entry_point": project.entry_point}
116
+
117
+
118
+ def _build_native_binary(project: NativeProject, staging: Path) -> Path:
119
+ normalized_platform, _, suffix = _platform()
120
+ if normalized_platform not in project.platforms:
121
+ raise ResolverError("UNSUPPORTED_PLATFORM", "native metadata does not declare this platform")
122
+ target_dir = staging / "cargo-target"
123
+ env = os.environ.copy()
124
+ cargo = env.get("STONECASTLE_CARGO") or shutil.which("cargo")
125
+ if not cargo:
126
+ raise ResolverError("BUILD_TOOL_UNAVAILABLE", "exact Cargo executable is unavailable")
127
+ cargo_candidate = Path(cargo)
128
+ cargo_path = cargo_candidate.resolve()
129
+ if cargo_path.name.lower().startswith("rustup"):
130
+ sibling = cargo_candidate.with_name("cargo.exe" if os.name == "nt" else "cargo")
131
+ cargo_path = sibling.absolute() if sibling.is_file() else cargo_path
132
+ if not cargo_path.is_file():
133
+ raise ResolverError("BUILD_TOOL_UNAVAILABLE", "exact Cargo executable is unavailable")
134
+ cargo_home = Path(env.get("CARGO_HOME", "")) if env.get("CARGO_HOME") else cargo_path.parent.parent
135
+ rustup_home = Path(env.get("RUSTUP_HOME", "")) if env.get("RUSTUP_HOME") else cargo_home.parent / ".rustup"
136
+ env.update({"CARGO_HOME": str(cargo_home), "RUSTUP_HOME": str(rustup_home), "RUSTUP_TOOLCHAIN": "1.96.0", "CARGO_TARGET_DIR": str(target_dir), "CARGO_INCREMENTAL": "0", "SOURCE_DATE_EPOCH": "315532800"})
137
+ env["RUSTFLAGS"] = (env.get("RUSTFLAGS", "") + " --remap-path-prefix=" + str(project.crate_path.resolve()) + "=/stonecastle").strip()
138
+ if suffix:
139
+ env["RUSTFLAGS"] += " -C link-arg=/Brepro"
140
+ env.pop("PYTHONPATH", None)
141
+ try:
142
+ result = subprocess.run([str(cargo_path), "build", "--release", "--locked"], cwd=project.crate_path, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=600, check=False)
143
+ except (FileNotFoundError, subprocess.TimeoutExpired) as error:
144
+ raise ResolverError("BUILD_TOOL_UNAVAILABLE", "locked Cargo build is unavailable") from error
145
+ if result.returncode != 0:
146
+ diagnostic = (result.stderr or result.stdout or "no Cargo diagnostic").strip().replace("\n", " ")[:500]
147
+ raise ResolverError("BUILD_FAILED", f"locked native gateway build failed: {diagnostic}")
148
+ return target_dir / "release" / (project.binary + suffix)
149
+
150
+
151
+ def build_natives(projects: tuple[NativeProject, ...], staging: Path) -> list[dict[str, object]]:
152
+ """Compile the complete native set before emitting authority-bound wheels."""
153
+ binaries = [(project, _build_native_binary(project, staging)) for project in projects]
154
+ authority = {project.normalized_name: hashlib.sha256(binary.read_bytes()).hexdigest() for project, binary in binaries}
155
+ if len(authority) != len(projects):
156
+ raise ResolverError("NATIVE_AUTHORITY_INVALID", "native project authority is ambiguous")
157
+ packaged = [package_native(project, binary, staging, authority=authority) for project, binary in binaries]
158
+ from .artifacts import validate_native_wheel
159
+ return [validate_native_wheel(staging / "wheels" / str(record["filename"]), project) for (project, _binary), record in zip(binaries, packaged, strict=True)]
160
+
161
+
162
+ def build_native(project: NativeProject, staging: Path) -> dict[str, object]:
163
+ return build_natives((project,), staging)[0]
@@ -0,0 +1,129 @@
1
+ """Fail-closed loading and validation of the indexed TriG profile catalog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ import re
9
+
10
+ from rdflib import Dataset, Literal, RDF, SKOS, URIRef
11
+ from packaging.version import Version, InvalidVersion
12
+
13
+ from .model import ResolverError, canonical_json
14
+
15
+ PROFILE_NS = "urn:cf:profile:"
16
+ INSTALLATION_PROFILE = URIRef(PROFILE_NS + "InstallationProfile")
17
+ IDENTIFIER = URIRef(PROFILE_NS + "hasProfileIdentifier")
18
+ IDENTIFIER_TYPE = URIRef(PROFILE_NS + "ProfileIdentifier")
19
+ VERSION = URIRef(PROFILE_NS + "hasProfileVersion")
20
+ VERSION_TYPE = URIRef(PROFILE_NS + "ProfileVersion")
21
+ CAPABILITY = URIRef(PROFILE_NS + "hasCapabilityRequest")
22
+ CAPABILITY_TYPE = URIRef(PROFILE_NS + "CapabilityRequest")
23
+ POST_INSTALL_VERIFICATION = URIRef(PROFILE_NS + "hasPostInstallVerificationCapability")
24
+ POST_INSTALL_VERIFICATION_TYPE = URIRef(PROFILE_NS + "PostInstallVerificationCapability")
25
+ SUPPORTED_VERSION = Version("1.0.0")
26
+
27
+
28
+ def _attribute_values(dataset: Dataset, subject: URIRef, relation: URIRef, attribute_type: URIRef) -> list[object]:
29
+ nodes = sorted({obj for _, _, obj, _ in dataset.quads((subject, relation, None, None))}, key=str)
30
+ values: list[object] = []
31
+ for node in nodes:
32
+ if isinstance(node, Literal):
33
+ raise ResolverError("PROFILE_INVALID", "profile relations must target typed attribute nodes")
34
+ predicates = {predicate for _, predicate, _, _ in dataset.quads((node, None, None, None))}
35
+ types = {obj for _, _, obj, _ in dataset.quads((node, RDF.type, None, None))}
36
+ node_values = {obj for _, _, obj, _ in dataset.quads((node, RDF.value, None, None))}
37
+ if predicates != {RDF.type, RDF.value} or types != {attribute_type} or len(node_values) != 1:
38
+ raise ResolverError("PROFILE_INVALID", "profile attribute structure is outside the canonical model")
39
+ values.append(next(iter(node_values)))
40
+ return values
41
+
42
+
43
+ def _profile(path: Path, requested_iri: str | None = None) -> dict[str, object]:
44
+ dataset = Dataset()
45
+ try:
46
+ dataset.parse(path, format="trig")
47
+ except Exception as error:
48
+ raise ResolverError("PROFILE_INVALID", "profile document is not valid TriG") from error
49
+ subjects = sorted({subject for subject, _, _, _ in dataset.quads((None, RDF.type, INSTALLATION_PROFILE, None)) if isinstance(subject, URIRef)}, key=str)
50
+ if len(subjects) != 1:
51
+ raise ResolverError("PROFILE_INVALID", "profile document must contain exactly one InstallationProfile subject")
52
+ subject = subjects[0]
53
+ if requested_iri is not None and str(subject) != requested_iri:
54
+ raise ResolverError("PROFILE_INVALID", "profile subject does not match requested profile IRI")
55
+ allowed_predicates = {RDF.type, SKOS.prefLabel, SKOS.definition, IDENTIFIER, VERSION, CAPABILITY, POST_INSTALL_VERIFICATION}
56
+ predicates = {predicate for _, predicate, _, _ in dataset.quads((subject, None, None, None))}
57
+ types = {obj for _, _, obj, _ in dataset.quads((subject, RDF.type, None, None))}
58
+ labels = {obj for _, _, obj, _ in dataset.quads((subject, SKOS.prefLabel, None, None))}
59
+ definitions = {obj for _, _, obj, _ in dataset.quads((subject, SKOS.definition, None, None))}
60
+ if predicates - allowed_predicates or types != {INSTALLATION_PROFILE}:
61
+ raise ResolverError("PROFILE_INVALID", "profile structure is outside the canonical model")
62
+ if len(labels) != 1 or len(definitions) != 1 or any(not isinstance(item, Literal) or not item.value for item in labels | definitions):
63
+ raise ResolverError("PROFILE_INVALID", "profile documentation is outside the canonical model")
64
+ identifiers = _attribute_values(dataset, subject, IDENTIFIER, IDENTIFIER_TYPE)
65
+ versions = _attribute_values(dataset, subject, VERSION, VERSION_TYPE)
66
+ capabilities = sorted(_attribute_values(dataset, subject, CAPABILITY, CAPABILITY_TYPE), key=str)
67
+ verification = _attribute_values(dataset, subject, POST_INSTALL_VERIFICATION, POST_INSTALL_VERIFICATION_TYPE)
68
+ attribute_nodes = {obj for relation in (IDENTIFIER, VERSION, CAPABILITY, POST_INSTALL_VERIFICATION) for _, _, obj, _ in dataset.quads((subject, relation, None, None))}
69
+ document_subjects = {item for item, _, _, _ in dataset.quads((None, None, None, None))}
70
+ if document_subjects != {subject, *attribute_nodes}:
71
+ raise ResolverError("PROFILE_INVALID", "profile document contains subjects outside the canonical model")
72
+ if len(identifiers) != 1 or len(versions) != 1 or not capabilities or any(not isinstance(item, URIRef) for item in capabilities):
73
+ raise ResolverError("PROFILE_INVALID", "profile identity, version, or capabilities are invalid")
74
+ identifier = identifiers[0]
75
+ version = versions[0]
76
+ if not isinstance(identifier, Literal) or not isinstance(version, Literal) or not identifier.value or not version.value:
77
+ raise ResolverError("PROFILE_INVALID", "profile identity and version must be literals")
78
+ try:
79
+ parsed_version = Version(str(version))
80
+ except InvalidVersion as error:
81
+ raise ResolverError("PROFILE_INVALID", "profile version is not a supported semantic version") from error
82
+ if parsed_version != SUPPORTED_VERSION:
83
+ raise ResolverError("PROFILE_INVALID", "profile semantic version is unsupported")
84
+ capability_iris = [str(item) for item in capabilities]
85
+ if capability_iris != sorted(capability_iris) or len(capability_iris) != len(set(capability_iris)):
86
+ raise ResolverError("PROFILE_INVALID", "capabilities are not canonical and unique")
87
+ if any(not re.fullmatch(r"urn:cf:service:[A-Za-z][A-Za-z0-9]*", item) for item in capability_iris):
88
+ raise ResolverError("PROFILE_INVALID", "profile capability contains package or path data")
89
+ canonical_bytes = path.read_bytes().replace(b"\r\n", b"\n")
90
+ if len(verification) > 1 or any(not isinstance(item, URIRef) for item in verification):
91
+ raise ResolverError("PROFILE_INVALID", "post-install verification capability is ambiguous")
92
+ if verification and str(verification[0]) not in capability_iris:
93
+ raise ResolverError("PROFILE_INVALID", "post-install verification capability is not requested")
94
+ return {"profile": str(subject), "identifier": str(identifier), "version": str(version), "capabilities": capability_iris, "post_install_verification_capability": str(verification[0]) if verification else None, "sha256": hashlib.sha256(canonical_bytes).hexdigest()}
95
+
96
+
97
+ def load_catalog(checkout: Path, profile_iri: str) -> dict[str, object]:
98
+ checkout = checkout.resolve()
99
+ root = (checkout / "stonecastle" / "profiles").resolve()
100
+ if not root.is_dir() or not re.fullmatch(r"urn:cf:profile:[A-Za-z0-9.-]+", profile_iri):
101
+ raise ResolverError("PROFILE_INVALID", "profile catalog root or profile IRI is invalid")
102
+ catalog_path = root / "profiles.index.json"
103
+ try:
104
+ catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
105
+ except (OSError, json.JSONDecodeError) as error:
106
+ raise ResolverError("PROFILE_INVALID", "profile catalog index is invalid") from error
107
+ if set(catalog) != {"schema", "profiles"} or catalog["schema"] != "cf.installation-profile-catalog.v1" or not isinstance(catalog["profiles"], list):
108
+ raise ResolverError("PROFILE_INVALID", "profile catalog schema is invalid")
109
+ actual_paths = sorted(path.relative_to(checkout).as_posix() for path in root.glob("*.trig"))
110
+ indexed_paths = sorted(item.get("path") for item in catalog["profiles"] if isinstance(item, dict))
111
+ if actual_paths != indexed_paths or any(not isinstance(path, str) or not path.startswith("stonecastle/profiles/") or ".." in Path(path).parts for path in indexed_paths):
112
+ raise ResolverError("PROFILE_INVALID", "profile catalog does not exactly cover indexed profiles")
113
+ entries = []
114
+ for item in catalog["profiles"]:
115
+ if set(item) != {"profile", "path", "sha256"}:
116
+ raise ResolverError("PROFILE_INVALID", "profile catalog entry fields are invalid")
117
+ path = checkout / item["path"]
118
+ if not path.is_file() or path.is_symlink():
119
+ raise ResolverError("PROFILE_INVALID", "catalog profile path is missing or redirected")
120
+ parsed = _profile(path, str(item["profile"]))
121
+ if parsed["sha256"] != item["sha256"]:
122
+ raise ResolverError("PROFILE_INVALID", "profile digest mismatch")
123
+ entries.append({**parsed, "path": item["path"]})
124
+ if len({item["profile"] for item in entries}) != len(entries) or len({item["identifier"] for item in entries}) != len(entries):
125
+ raise ResolverError("PROFILE_AMBIGUOUS", "profile catalog contains duplicate identity")
126
+ matches = [item for item in entries if item["profile"] == profile_iri]
127
+ if not matches:
128
+ raise ResolverError("PROFILE_NOT_FOUND", "requested profile is not in the catalog")
129
+ return {"profile": matches[0], "catalog": catalog, "catalog_digest": "sha256:" + hashlib.sha256(canonical_json(catalog)).hexdigest()}