cf-runtime 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_runtime/manifest.py ADDED
@@ -0,0 +1,464 @@
1
+ """Strict runtime manifest model and deterministic, atomic TOML representation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import hashlib
7
+ import ipaddress
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import stat
12
+ import re
13
+ from types import MappingProxyType
14
+ from typing import Mapping
15
+ import tomllib
16
+
17
+ from .domain import EnvironmentKind, McpTransport, RuntimeId, SourceKind
18
+ from .paths import CogniflowHome
19
+
20
+ SCHEMA_VERSION = 2
21
+ DEFAULT_DEVELOPMENT_FUSEKI_PORT = 3030
22
+ DEFAULT_PRODUCTION_FUSEKI_PORT = 3130
23
+ DEFAULT_RUNTIME_VERSION = "0.1.0"
24
+ DEFAULT_TOOL_VERSIONS = MappingProxyType({"fuseki": "6.0.0", "java": "21.0.10+7"})
25
+ DEFAULT_DEVELOPMENT_MCP_COMMAND = (
26
+ "cargo", "run", "--quiet", "--manifest-path",
27
+ "stonecastle/cf_service_mcp_server/Cargo.toml", "--bin", "cf-service-mcp-server",
28
+ )
29
+ _BASE_TOP_KEYS = {"schema", "runtime", "paths", "source", "tools", "mcp", "fuseki", "public_gateway", "lifecycle", "versions"}
30
+ _TABLE_KEYS = {
31
+ "schema": {"version"}, "runtime": {"id", "kind"},
32
+ "paths": {"home", "runtime", "data"}, "source": {"kind", "root"},
33
+ "mcp": {"transport", "command", "executable"},
34
+ "fuseki": {"bind", "port", "dataset", "config"},
35
+ "public_gateway": {"bind"}, "lifecycle": {"owner"},
36
+ "versions": {"semantic", "runtime", "release"},
37
+ "installation": {"root", "environment", "python"},
38
+ "instance": {"marker", "state", "plan_digest", "artifact_set_digest", "native_mcp_distribution", "native_mcp_version"},
39
+ }
40
+ _REQUIRED = {
41
+ "schema": {"version"}, "runtime": {"id", "kind"},
42
+ "paths": {"home", "runtime", "data"}, "source": {"kind", "root"},
43
+ "fuseki": {"bind", "port", "dataset", "config"},
44
+ "lifecycle": {"owner"},
45
+ }
46
+ _DATASET_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]*\Z")
47
+
48
+
49
+ def _text(value: object, label: str) -> None:
50
+ if not isinstance(value, str) or not value.strip():
51
+ raise ValueError(f"{label} cannot be empty")
52
+
53
+
54
+ def _loopback(value: str, label: str) -> None:
55
+ try:
56
+ address = ipaddress.ip_address(value)
57
+ except ValueError as error:
58
+ raise ValueError(f"{label} must be a loopback IP address") from error
59
+ if not address.is_loopback:
60
+ raise ValueError(f"{label} must be a loopback IP address")
61
+
62
+
63
+ def _atomic_text(path: Path, content: str) -> None:
64
+ temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
65
+ try:
66
+ with temporary.open("w", encoding="utf-8", newline="\n") as stream:
67
+ stream.write(content)
68
+ stream.flush()
69
+ os.fsync(stream.fileno())
70
+ os.replace(temporary, path)
71
+ finally:
72
+ temporary.unlink(missing_ok=True)
73
+
74
+
75
+ def _same_path(left: Path, right: Path) -> bool:
76
+ left_value = os.path.normcase(str(left.resolve(strict=False)))
77
+ right_value = os.path.normcase(str(right.resolve(strict=False)))
78
+ return left_value == right_value
79
+
80
+
81
+ def _strict_path(value: str | Path, label: str) -> Path:
82
+ path = Path(value).expanduser()
83
+ if not path.is_absolute() or path == Path(path.anchor) or any(part in {".", ".."} for part in path.parts):
84
+ raise ValueError(f"{label} must be an absolute normalized path")
85
+ current = Path(path.anchor)
86
+ for part in path.parts[1:]:
87
+ current /= part
88
+ try:
89
+ info = current.lstat()
90
+ except FileNotFoundError:
91
+ continue
92
+ if current.is_symlink() or (os.name == "nt" and getattr(info, "st_file_attributes", 0) & 0x400):
93
+ raise ValueError(f"{label} contains a redirecting link or reparse point")
94
+ return path
95
+
96
+
97
+ def _real_file(path: Path, label: str) -> None:
98
+ try:
99
+ info = path.lstat()
100
+ except OSError as error:
101
+ raise ValueError(f"{label} is missing") from error
102
+ if not stat.S_ISREG(info.st_mode) or path.is_symlink() or (os.name == "nt" and getattr(info, "st_file_attributes", 0) & 0x400):
103
+ raise ValueError(f"{label} must be a regular non-link file")
104
+ if not _same_path(path, path.resolve(strict=True)):
105
+ raise ValueError(f"{label} is not canonical")
106
+
107
+
108
+ def _below(path: Path, parent: Path, label: str) -> None:
109
+ if not path.is_absolute() or path == path.anchor:
110
+ raise ValueError(f"{label} must be an absolute path")
111
+ try:
112
+ path.relative_to(parent)
113
+ except ValueError as error:
114
+ raise ValueError(f"{label} must be below its declared root") from error
115
+
116
+
117
+ def _validate_installed_paths(manifest: RuntimeManifest) -> None:
118
+ assert manifest.installation_root is not None
119
+ assert manifest.environment_root is not None
120
+ assert manifest.python_executable is not None
121
+ assert manifest.mcp_executable is not None
122
+ assert manifest.instance_marker is not None
123
+ install = manifest.installation_root
124
+ environment = manifest.environment_root
125
+ scripts = environment / ("Scripts" if os.name == "nt" else "bin")
126
+ _real_file(manifest.python_executable, "target Python executable")
127
+ _real_file(manifest.mcp_executable, "installed MCP executable")
128
+ if not install.is_dir() or install.is_symlink() or not environment.is_dir() or environment.is_symlink():
129
+ raise ValueError("installed roots must be real directories")
130
+ _below(environment, install, "target environment")
131
+ _below(manifest.python_executable, environment, "target Python executable")
132
+ _below(manifest.mcp_executable, scripts, "installed MCP executable")
133
+ if not _same_path(manifest.source_root, install):
134
+ raise ValueError("production source root must equal the external installation root")
135
+ if (_same_path(install, manifest.home_root) or _same_path(environment, manifest.home_root)
136
+ or _below_or_equal(install, manifest.home_root) or _below_or_equal(environment, manifest.home_root)):
137
+ raise ValueError("installation root and target environment must be outside Cogniflow home")
138
+ if not _same_path(manifest.instance_marker.parent, install):
139
+ raise ValueError("instance marker must be directly below installation root")
140
+ _real_file(manifest.instance_marker, "SCB-05 instance marker")
141
+ try:
142
+ marker = json.loads(manifest.instance_marker.read_text(encoding="utf-8"))
143
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
144
+ raise ValueError("SCB-05 instance marker is invalid") from error
145
+ schema = marker.get("schema") if isinstance(marker, dict) else None
146
+ if schema == "cf.bootstrap.instance-marker.v1":
147
+ fields = {"schema", "instance_id", "installation_root", "environment_root", "python_executable",
148
+ "plan_digest", "artifact_set_digest", "installed_distributions", "state"}
149
+ elif schema == "cf.bootstrap.instance-marker.v2":
150
+ fields = {"schema", "instance_id", "installation_root", "environment_root", "python_executable",
151
+ "profile", "plan_digest", "profile_digest", "catalog_digest", "selection_digest",
152
+ "artifact_set_digest", "installed_distributions", "recovery_manifest_path",
153
+ "recovery_manifest_digest"}
154
+ else:
155
+ raise ValueError("SCB-05 instance marker schema is unsupported")
156
+ if set(marker) != fields:
157
+ raise ValueError("SCB-05 instance marker fields are invalid")
158
+ expected = {"plan_digest": manifest.plan_digest, "artifact_set_digest": manifest.artifact_set_digest}
159
+ bound_paths = ("installation_root", install), ("environment_root", environment), ("python_executable", manifest.python_executable)
160
+ if (not isinstance(marker.get("instance_id"), str) or not _valid_marker_identifier(marker["instance_id"])
161
+ or (schema == "cf.bootstrap.instance-marker.v1" and marker["state"] != "ready")
162
+ or (schema == "cf.bootstrap.instance-marker.v2" and (
163
+ not isinstance(marker["profile"], str) or not _valid_profile(marker["profile"])
164
+ or not _valid_digest(marker["profile_digest"])
165
+ or not _valid_digest(marker["catalog_digest"])
166
+ or not _valid_digest(marker["selection_digest"])))
167
+ or any(marker.get(key) != value for key, value in expected.items())
168
+ or not _valid_digest(manifest.plan_digest) or not _valid_digest(manifest.artifact_set_digest)
169
+ or any(not isinstance(marker.get(key), str) or not _bound_marker_path(marker[key], value) for key, value in bound_paths)):
170
+ raise ValueError("SCB-05 instance marker is not bound to this production composition")
171
+ if schema == "cf.bootstrap.instance-marker.v2":
172
+ recovery_manifest = install / "state" / "minimum-service-plane" / "recovery-manifest.json"
173
+ if (not _bound_marker_path(marker["recovery_manifest_path"], recovery_manifest)
174
+ or not _valid_digest(marker["recovery_manifest_digest"])):
175
+ raise ValueError("SCB-05 instance marker recovery authority is invalid")
176
+ _real_file(recovery_manifest, "SCB-13 recovery manifest")
177
+ if marker["recovery_manifest_digest"] != "sha256:" + hashlib.sha256(recovery_manifest.read_bytes()).hexdigest():
178
+ raise ValueError("SCB-05 instance marker recovery authority is not bound to retained state")
179
+ installed = marker.get("installed_distributions")
180
+ if (not isinstance(installed, list) or any(
181
+ not isinstance(item, dict) or set(item) != {"name", "version"}
182
+ or not isinstance(item["name"], str) or not _valid_marker_identifier(item["name"])
183
+ or not isinstance(item["version"], str) or not _valid_marker_version(item["version"])
184
+ for item in installed
185
+ ) or [item["name"].lower() for item in installed] != sorted({item["name"].lower() for item in installed})
186
+ or len({item["name"].lower() for item in installed}) != len(installed)
187
+ or not any(item["name"] == manifest.native_mcp_distribution and item["version"] == manifest.native_mcp_version for item in installed)):
188
+ raise ValueError("SCB-05 marker does not contain the installed native MCP distribution")
189
+
190
+
191
+ def _bound_marker_path(value: object, expected: Path) -> bool:
192
+ if not isinstance(value, str):
193
+ return False
194
+ path = Path(value)
195
+ return path.is_absolute() and not any(part in {".", ".."} for part in path.parts) and _same_path(path, expected)
196
+
197
+
198
+ def _valid_marker_identifier(value: str) -> bool:
199
+ return bool(value) and value[0].isalnum() and value[-1].isalnum() and all(character.isascii() and (character.isalnum() or character in ".-_") for character in value)
200
+
201
+
202
+ def _valid_marker_version(value: str) -> bool:
203
+ return bool(value) and value[0].isdigit() and all(character.isascii() and (character.isalnum() or character in ".!+_-") for character in value)
204
+
205
+
206
+ def _valid_digest(value: object) -> bool:
207
+ return isinstance(value, str) and len(value) == 71 and value.startswith("sha256:") and all(character in "0123456789abcdef" for character in value[7:])
208
+
209
+
210
+ def _valid_profile(value: str) -> bool:
211
+ scheme, separator, rest = value.partition(":")
212
+ return bool(separator and rest and scheme[0].isalpha() and all(character.isascii() and (character.isalnum() or character in "+.-") for character in scheme) and not any(character.isspace() for character in rest))
213
+
214
+
215
+ def _below_or_equal(path: Path, parent: Path) -> bool:
216
+ return _same_path(path, parent) or parent in path.parents
217
+
218
+
219
+ @dataclass(frozen=True, slots=True)
220
+ class RuntimeManifest:
221
+ runtime_id: RuntimeId
222
+ environment_kind: EnvironmentKind
223
+ home_root: Path
224
+ runtime_root: Path
225
+ data_root: Path
226
+ source_kind: SourceKind
227
+ source_root: Path
228
+ tool_versions: Mapping[str, str]
229
+ mcp_transport: McpTransport
230
+ mcp_command: tuple[str, ...] | None
231
+ fuseki_bind: str
232
+ fuseki_port: int
233
+ fuseki_dataset: str
234
+ fuseki_config: Path
235
+ public_gateway_bind: str | None
236
+ lifecycle_owner: str
237
+ runtime_version: str
238
+ release_version: str | None
239
+ semantic_revision: str
240
+ schema_version: int = SCHEMA_VERSION
241
+ installation_root: Path | None = None
242
+ environment_root: Path | None = None
243
+ python_executable: Path | None = None
244
+ mcp_executable: Path | None = None
245
+ instance_marker: Path | None = None
246
+ instance_state: str | None = None
247
+ plan_digest: str | None = None
248
+ artifact_set_digest: str | None = None
249
+ native_mcp_distribution: str | None = None
250
+ native_mcp_version: str | None = None
251
+
252
+ def __post_init__(self) -> None:
253
+ object.__setattr__(self, "runtime_id", RuntimeId(self.runtime_id))
254
+ object.__setattr__(self, "environment_kind", EnvironmentKind(self.environment_kind))
255
+ object.__setattr__(self, "source_kind", SourceKind(self.source_kind))
256
+ object.__setattr__(self, "mcp_transport", McpTransport(self.mcp_transport))
257
+ object.__setattr__(self, "mcp_command", None if self.mcp_command is None else tuple(self.mcp_command))
258
+ object.__setattr__(self, "tool_versions", MappingProxyType(dict(self.tool_versions)))
259
+ home = CogniflowHome.at(self.home_root)
260
+ object.__setattr__(self, "home_root", home.root)
261
+ for field in ("runtime_root", "data_root", "fuseki_config"):
262
+ strict = _strict_path(getattr(self, field), field)
263
+ object.__setattr__(self, field, home.validate_path(strict, label=field))
264
+ object.__setattr__(self, "source_root", _strict_path(self.source_root, "source root").resolve(strict=False))
265
+ for field in ("installation_root", "environment_root", "python_executable", "mcp_executable", "instance_marker"):
266
+ value = getattr(self, field)
267
+ if value is not None:
268
+ object.__setattr__(self, field, _strict_path(value, field).resolve(strict=False))
269
+ self.validate()
270
+
271
+ def validate(self) -> None:
272
+ if isinstance(self.schema_version, bool) or self.schema_version != SCHEMA_VERSION:
273
+ raise ValueError(f"manifest schema version must be {SCHEMA_VERSION}")
274
+ paths = CogniflowHome.at(self.home_root).runtime(self.runtime_id)
275
+ if self.runtime_root != paths.runtime_root or self.data_root != paths.data or self.fuseki_config != paths.fuseki_config:
276
+ raise ValueError("manifest paths do not match the canonical runtime layout")
277
+ _loopback(self.fuseki_bind, "Fuseki bind")
278
+ if isinstance(self.fuseki_port, bool) or not isinstance(self.fuseki_port, int) or not 1 <= self.fuseki_port <= 65535:
279
+ raise ValueError("Fuseki port must be an integer from 1 to 65535")
280
+ for value, label in ((self.fuseki_dataset, "Fuseki dataset"), (self.lifecycle_owner, "lifecycle owner"),
281
+ (self.runtime_version, "runtime version"), (self.semantic_revision, "semantic revision")):
282
+ _text(value, label)
283
+ if not _DATASET_NAME.fullmatch(self.fuseki_dataset):
284
+ raise ValueError("Fuseki dataset must be a safe path identifier")
285
+ if set(self.tool_versions) != {"fuseki", "java"} or any(not isinstance(v, str) or not v for v in self.tool_versions.values()):
286
+ raise ValueError("tool versions must contain exactly non-empty fuseki and java strings")
287
+ if self.public_gateway_bind is not None:
288
+ _loopback(self.public_gateway_bind, "public gateway bind")
289
+ if self.runtime_id.value != self.environment_kind.value:
290
+ raise ValueError("runtime id and environment kind must agree")
291
+ if self.runtime_id is RuntimeId.DEVELOPMENT:
292
+ if (self.source_kind is not SourceKind.WORKING_TREE or self.lifecycle_owner != "cf-runtime"
293
+ or self.release_version is not None or self.mcp_transport is not McpTransport.STDIO
294
+ or not self.mcp_command or any(not isinstance(v, str) or not v for v in self.mcp_command)):
295
+ raise ValueError("development requires a cf-runtime working tree stdio command and no release")
296
+ if not self.source_root.is_dir():
297
+ raise ValueError("development source must be an existing working tree")
298
+ if self.mcp_executable is not None:
299
+ raise ValueError("development cannot contain an installed MCP executable")
300
+ else:
301
+ if (self.source_kind is not SourceKind.RELEASE or self.lifecycle_owner != "cf-runtime"
302
+ or not self.release_version or self.mcp_transport is not McpTransport.STDIO
303
+ or self.mcp_command is None):
304
+ raise ValueError("production requires an explicitly provisioned installed stdio composition")
305
+ required = (self.installation_root, self.environment_root, self.python_executable,
306
+ self.mcp_executable, self.instance_marker, self.plan_digest,
307
+ self.artifact_set_digest, self.native_mcp_distribution, self.native_mcp_version)
308
+ if any(value is None for value in required) or self.instance_state != "ready":
309
+ raise ValueError("production instance binding is incomplete")
310
+ if len(self.mcp_command) != 1 or not _same_path(Path(self.mcp_command[0]), self.mcp_executable):
311
+ raise ValueError("production MCP command must name the installed executable")
312
+ _validate_installed_paths(self)
313
+
314
+ def to_toml(self) -> str:
315
+ def q(value: str) -> str:
316
+ return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"'
317
+ lines = ["[schema]", f"version = {self.schema_version}", "", "[runtime]", f"id = {q(self.runtime_id.value)}",
318
+ f"kind = {q(self.environment_kind.value)}", "", "[paths]", f"home = {q(str(self.home_root))}",
319
+ f"runtime = {q(str(self.runtime_root))}", f"data = {q(str(self.data_root))}", "", "[source]",
320
+ f"kind = {q(self.source_kind.value)}", f"root = {q(str(self.source_root))}", "", "[tools]"]
321
+ lines.extend(f"{q(key)} = {q(self.tool_versions[key])}" for key in sorted(self.tool_versions))
322
+ lines.extend(["", "[mcp]", f"transport = {q(self.mcp_transport.value)}"])
323
+ if self.mcp_command is not None:
324
+ lines.append("command = [" + ", ".join(q(item) for item in self.mcp_command) + "]")
325
+ if self.mcp_executable is not None:
326
+ lines.append(f"executable = {q(str(self.mcp_executable))}")
327
+ lines.extend(["", "[fuseki]", f"bind = {q(self.fuseki_bind)}", f"port = {self.fuseki_port}",
328
+ f"dataset = {q(self.fuseki_dataset)}", f"config = {q(str(self.fuseki_config))}", "", "[public_gateway]"])
329
+ if self.public_gateway_bind is not None:
330
+ lines.append(f"bind = {q(self.public_gateway_bind)}")
331
+ lines.extend(["", "[lifecycle]", f"owner = {q(self.lifecycle_owner)}", "", "[versions]",
332
+ f"semantic = {q(self.semantic_revision)}", f"runtime = {q(self.runtime_version)}"])
333
+ if self.release_version is not None:
334
+ lines.append(f"release = {q(self.release_version)}")
335
+ if self.runtime_id is RuntimeId.PRODUCTION:
336
+ lines.extend(["", "[installation]", f"root = {q(str(self.installation_root))}",
337
+ f"environment = {q(str(self.environment_root))}", f"python = {q(str(self.python_executable))}",
338
+ "", "[instance]", f"marker = {q(str(self.instance_marker))}",
339
+ f"state = {q(self.instance_state or '')}", f"plan_digest = {q(self.plan_digest or '')}",
340
+ f"artifact_set_digest = {q(self.artifact_set_digest or '')}",
341
+ f"native_mcp_distribution = {q(self.native_mcp_distribution or '')}",
342
+ f"native_mcp_version = {q(self.native_mcp_version or '')}"])
343
+ return "\n".join(lines) + "\n"
344
+
345
+ @classmethod
346
+ def from_toml(cls, content: str | bytes) -> RuntimeManifest:
347
+ try:
348
+ raw = tomllib.loads(content.decode() if isinstance(content, bytes) else content)
349
+ except (tomllib.TOMLDecodeError, UnicodeDecodeError) as error:
350
+ raise ValueError(f"invalid runtime manifest TOML: {error}") from error
351
+ if not isinstance(raw, dict) or not _BASE_TOP_KEYS <= set(raw) or set(raw) - _BASE_TOP_KEYS - {"installation", "instance"}:
352
+ raise ValueError("manifest contains unknown top-level tables")
353
+ runtime_id = raw.get("runtime", {}).get("id") if isinstance(raw.get("runtime"), dict) else None
354
+ expected_top = _BASE_TOP_KEYS if runtime_id == "development" else _BASE_TOP_KEYS | {"installation", "instance"}
355
+ if set(raw) != expected_top:
356
+ raise ValueError(f"manifest tables must be exactly {sorted(expected_top)}")
357
+ for table, keys in _TABLE_KEYS.items():
358
+ if table not in raw:
359
+ continue
360
+ if not isinstance(raw[table], dict) or not set(raw[table]).issubset(keys):
361
+ raise ValueError(f"unexpected fields in [{table}]")
362
+ for table, keys in _REQUIRED.items():
363
+ if set(raw[table]) != keys:
364
+ raise ValueError(f"missing fields in [{table}]")
365
+ if set(raw["versions"]) not in ({"semantic", "runtime"}, {"semantic", "runtime", "release"}):
366
+ raise ValueError("invalid fields in [versions]")
367
+ transport = raw["mcp"].get("transport")
368
+ expected_mcp = {"transport", "command"} if transport == "stdio" else {"transport"}
369
+ if transport == "stdio" and "executable" in raw["mcp"]:
370
+ expected_mcp.add("executable")
371
+ if set(raw["mcp"]) != expected_mcp or not isinstance(raw["tools"], dict):
372
+ raise ValueError("invalid fields in [mcp] or [tools]")
373
+ if transport == "stdio" and (not isinstance(raw["mcp"].get("command"), list)
374
+ or not all(isinstance(item, str) and item for item in raw["mcp"]["command"])):
375
+ raise ValueError("MCP command must be a nonempty string array")
376
+ if raw["runtime"].get("id") == "development":
377
+ installation, instance = {}, {}
378
+ else:
379
+ installation, instance = raw["installation"], raw["instance"]
380
+ if set(installation) != {"root", "environment", "python"} or set(instance) != _TABLE_KEYS["instance"]:
381
+ raise ValueError("installed production fields are incomplete")
382
+ try:
383
+ return cls(raw["runtime"]["id"], raw["runtime"]["kind"], Path(raw["paths"]["home"]),
384
+ Path(raw["paths"]["runtime"]), Path(raw["paths"]["data"]), raw["source"]["kind"],
385
+ Path(raw["source"]["root"]), raw["tools"], transport,
386
+ tuple(raw["mcp"]["command"]) if "command" in raw["mcp"] else None,
387
+ raw["fuseki"]["bind"], raw["fuseki"]["port"], raw["fuseki"]["dataset"],
388
+ Path(raw["fuseki"]["config"]), raw["public_gateway"].get("bind"), raw["lifecycle"]["owner"],
389
+ raw["versions"]["runtime"], raw["versions"].get("release"), raw["versions"]["semantic"],
390
+ raw["schema"]["version"],
391
+ Path(installation["root"]) if installation else None,
392
+ Path(installation["environment"]) if installation else None,
393
+ Path(installation["python"]) if installation else None,
394
+ Path(raw["mcp"]["executable"]) if "executable" in raw["mcp"] else None,
395
+ Path(instance["marker"]) if instance else None,
396
+ instance.get("state"), instance.get("plan_digest"),
397
+ instance.get("artifact_set_digest"), instance.get("native_mcp_distribution"),
398
+ instance.get("native_mcp_version"))
399
+ except (KeyError, TypeError, ValueError) as error:
400
+ raise ValueError(f"invalid runtime manifest: {error}") from error
401
+
402
+ @classmethod
403
+ def read(cls, path: str | Path) -> RuntimeManifest:
404
+ return cls.from_toml(Path(path).read_bytes())
405
+
406
+ def write(self, path: str | Path) -> None:
407
+ home = CogniflowHome.at(self.home_root)
408
+ destination = home.validate_path(path, label="manifest path")
409
+ if destination != home.runtime(self.runtime_id).manifest or not destination.parent.is_dir():
410
+ raise ValueError("manifest must use a prepared canonical runtime path")
411
+ if destination.is_symlink():
412
+ raise ValueError("manifest path must not be a symlink")
413
+ other_id = RuntimeId.PRODUCTION if self.runtime_id is RuntimeId.DEVELOPMENT else RuntimeId.DEVELOPMENT
414
+ other_path = home.runtime(other_id).manifest
415
+ if other_path.is_file():
416
+ other = RuntimeManifest.read(other_path)
417
+ development, production = ((self, other) if self.runtime_id is RuntimeId.DEVELOPMENT else (other, self))
418
+ validate_manifest_pair(development, production)
419
+ if destination.is_file() and self.runtime_id is RuntimeId.PRODUCTION:
420
+ if destination.read_bytes() != self.to_toml().encode("utf-8"):
421
+ raise ValueError("production manifest already contains a conflicting composition")
422
+ return
423
+ _atomic_text(destination, self.to_toml())
424
+
425
+
426
+ def development_manifest(home: CogniflowHome, *, source_root: str | Path, runtime_version: str,
427
+ semantic_revision: str, tool_versions: Mapping[str, str],
428
+ mcp_command: tuple[str, ...] = DEFAULT_DEVELOPMENT_MCP_COMMAND,
429
+ fuseki_dataset: str = "cogniflow", public_gateway_bind: str | None = "127.0.0.1") -> RuntimeManifest:
430
+ paths = home.runtime(RuntimeId.DEVELOPMENT)
431
+ return RuntimeManifest(RuntimeId.DEVELOPMENT, EnvironmentKind.DEVELOPMENT, home.root, paths.runtime_root, paths.data,
432
+ SourceKind.WORKING_TREE, Path(source_root), tool_versions, McpTransport.STDIO, mcp_command,
433
+ "127.0.0.1", DEFAULT_DEVELOPMENT_FUSEKI_PORT, fuseki_dataset, paths.fuseki_config,
434
+ public_gateway_bind, "cf-runtime", runtime_version, None, semantic_revision)
435
+
436
+
437
+ def production_manifest(home: CogniflowHome, *, release_version: str, runtime_version: str,
438
+ semantic_revision: str, tool_versions: Mapping[str, str], installation_root: str | Path,
439
+ environment_root: str | Path, python_executable: str | Path,
440
+ mcp_executable: str | Path, instance_marker: str | Path,
441
+ plan_digest: str, artifact_set_digest: str, native_mcp_distribution: str,
442
+ native_mcp_version: str, lifecycle_owner: str = "cf-runtime",
443
+ fuseki_dataset: str = "cogniflow", public_gateway_bind: str | None = "127.0.0.1") -> RuntimeManifest:
444
+ paths = home.runtime(RuntimeId.PRODUCTION)
445
+ return RuntimeManifest(RuntimeId.PRODUCTION, EnvironmentKind.PRODUCTION, home.root, paths.runtime_root, paths.data,
446
+ SourceKind.RELEASE, Path(installation_root), tool_versions, McpTransport.STDIO,
447
+ (str(mcp_executable),),
448
+ "127.0.0.1", DEFAULT_PRODUCTION_FUSEKI_PORT, fuseki_dataset, paths.fuseki_config,
449
+ public_gateway_bind, lifecycle_owner, runtime_version, release_version, semantic_revision,
450
+ installation_root=installation_root, environment_root=environment_root,
451
+ python_executable=python_executable, mcp_executable=mcp_executable,
452
+ instance_marker=instance_marker, instance_state="ready", plan_digest=plan_digest,
453
+ artifact_set_digest=artifact_set_digest, native_mcp_distribution=native_mcp_distribution,
454
+ native_mcp_version=native_mcp_version)
455
+
456
+
457
+ def validate_manifest_pair(development: RuntimeManifest, production: RuntimeManifest) -> None:
458
+ development.validate()
459
+ production.validate()
460
+ by_id = {development.runtime_id: development, production.runtime_id: production}
461
+ if set(by_id) != {RuntimeId.DEVELOPMENT, RuntimeId.PRODUCTION}:
462
+ raise ValueError("manifest pair must contain development and production")
463
+ if development.fuseki_port == production.fuseki_port:
464
+ raise ValueError("development and production cannot use duplicate ports")