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/teardown.py ADDED
@@ -0,0 +1,237 @@
1
+ """Runtime-owned production teardown planning and transaction state."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import shutil
10
+ import tempfile
11
+ from typing import Mapping
12
+
13
+ from .domain import RuntimeId
14
+ from .lifecycle import ProductionRuntime
15
+ from .paths import CogniflowHome
16
+ from .semantics import recover_stopped, restore, status as semantic_status, uninstall as semantic_uninstall
17
+
18
+ _DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
19
+ _TOKEN = re.compile(r"[0-9a-f]{64}\Z")
20
+ def _canonical(value: object) -> bytes:
21
+ return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
22
+
23
+
24
+ def _digest(value: object) -> str:
25
+ return "sha256:" + hashlib.sha256(_canonical(value)).hexdigest()
26
+
27
+
28
+ def _atomic(path: Path, value: dict[str, object]) -> None:
29
+ path.parent.mkdir(parents=True, exist_ok=True)
30
+ descriptor, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
31
+ try:
32
+ with os.fdopen(descriptor, "wb") as stream:
33
+ stream.write(_canonical(value))
34
+ stream.flush()
35
+ os.fsync(stream.fileno())
36
+ os.replace(name, path)
37
+ finally:
38
+ Path(name).unlink(missing_ok=True)
39
+
40
+
41
+ def plan(home: CogniflowHome) -> dict[str, object]:
42
+ paths = home.runtime(RuntimeId.PRODUCTION)
43
+ value = {
44
+ "schema": "cf.runtime.production-teardown-plan.v1",
45
+ "remove": [str(item) for item in (paths.state, paths.locks, paths.logs, paths.temp, paths.fuseki_config, paths.manifest)],
46
+ "retain": [str(home.tools / "java"), str(home.tools / "fuseki"), str(home.root / "data"), str(paths.fuseki_database), str(home.root)],
47
+ }
48
+ value["plan_digest"] = _digest(value)
49
+ return value
50
+
51
+
52
+ def _path(home: CogniflowHome, transaction_id: str) -> Path:
53
+ if not _TOKEN.fullmatch(transaction_id):
54
+ raise ValueError("runtime teardown transaction id is invalid")
55
+ return home.root / "state" / "runtime-uninstall" / f"{transaction_id}.json"
56
+
57
+
58
+ def _read(home: CogniflowHome, transaction_id: str) -> dict[str, object]:
59
+ path = _path(home, transaction_id)
60
+ raw = path.read_bytes()
61
+ value = json.loads(raw)
62
+ if not isinstance(value, dict) or raw != _canonical(value) or value.get("record_digest") != _digest({key: item for key, item in value.items() if key != "record_digest"}):
63
+ raise ValueError("runtime teardown transaction is invalid")
64
+ return value
65
+
66
+
67
+ def _write(home: CogniflowHome, value: dict[str, object], phase: str) -> dict[str, object]:
68
+ updated = {**value, "phase": phase}
69
+ updated.pop("record_digest", None)
70
+ updated["record_digest"] = _digest(updated)
71
+ _atomic(_path(home, str(updated["transaction_id"])), updated)
72
+ return updated
73
+
74
+
75
+ def _attestation(value: Mapping[str, object]) -> dict[str, object]:
76
+ receipt = value.get("semantic_receipt")
77
+ result = {
78
+ "schema": "cf.runtime.production-teardown-attestation.v1",
79
+ "phase": value["phase"],
80
+ "transaction_id": value["transaction_id"],
81
+ "nonce": value["nonce"],
82
+ "authority": value["authority"],
83
+ "preview_digest": value["preview_digest"],
84
+ "plan": value["plan"],
85
+ "semantic_graphs": value["semantic_graphs"],
86
+ "semantic_receipt": receipt,
87
+ "semantic_receipt_digest": _digest(receipt),
88
+ "transaction_digest": value["record_digest"],
89
+ }
90
+ return result
91
+
92
+
93
+ def _verify_broad_stage(root: Path, target: Path) -> None:
94
+ preserved = {"environment", ".cogniflow-instance.json", ".cogniflow-profile-handoff.json", ".cogniflow-profile.json", ".cogniflow-profile-transition.json", "state"}
95
+ if not root.is_dir() or not target.is_file() or any(item.name not in preserved for item in root.iterdir()):
96
+ raise ValueError("installation broad cleanup is not complete")
97
+
98
+
99
+ def _validate_binding(transaction_id: object, nonce: object, authority: object, preview_digest: object) -> tuple[str, str, dict[str, object], str]:
100
+ if not isinstance(transaction_id, str) or not _TOKEN.fullmatch(transaction_id) or not isinstance(nonce, str) or not _TOKEN.fullmatch(nonce):
101
+ raise ValueError("runtime teardown transaction binding is invalid")
102
+ if not isinstance(authority, dict) or authority.get("schema") != "cf.bootstrap.uninstall-authority.v1":
103
+ raise ValueError("runtime teardown authority is invalid")
104
+ if not isinstance(preview_digest, str) or not _DIGEST.fullmatch(preview_digest):
105
+ raise ValueError("runtime teardown preview binding is invalid")
106
+ return transaction_id, nonce, authority, preview_digest
107
+
108
+
109
+ def prepare(home: CogniflowHome, *, transaction_id: object, nonce: object, authority: object, preview_digest: object, documents: object) -> dict[str, object]:
110
+ transaction_id, nonce, authority, preview_digest = _validate_binding(transaction_id, nonce, authority, preview_digest)
111
+ path = _path(home, transaction_id)
112
+ if path.exists():
113
+ raise ValueError("runtime teardown transaction already exists")
114
+ result = semantic_uninstall(home, documents) if documents else {"receipt": None, "graphs": [], "unrelated_graphs": []}
115
+ receipt = result.get("receipt")
116
+ try:
117
+ value = {
118
+ "schema": "cf.runtime.production-teardown-transaction.v1",
119
+ "phase": "prepared",
120
+ "transaction_id": transaction_id,
121
+ "nonce": nonce,
122
+ "authority": authority,
123
+ "preview_digest": preview_digest,
124
+ "plan": plan(home),
125
+ "documents": documents,
126
+ "semantic_graphs": sorted(str(item["graph"]) for item in result.get("graphs", [])),
127
+ "semantic_receipt": receipt,
128
+ }
129
+ value["record_digest"] = _digest(value)
130
+ _atomic(path, value)
131
+ except Exception:
132
+ if isinstance(receipt, Mapping):
133
+ restore(home, dict(receipt))
134
+ raise
135
+ return _attestation(value)
136
+
137
+
138
+ def _verify_effects(home: CogniflowHome, value: Mapping[str, object]) -> None:
139
+ if value.get("plan") != plan(home):
140
+ raise ValueError("runtime teardown plan changed")
141
+ phase = value.get("phase")
142
+ if phase == "prepared":
143
+ documents = value.get("documents")
144
+ if documents:
145
+ semantic = semantic_status(home, documents)
146
+ if any(item.get("state") != "missing" for item in semantic.get("graphs", [])):
147
+ raise ValueError("runtime teardown semantic effects are not present")
148
+ elif phase == "stopped":
149
+ observed = ProductionRuntime(home).status()
150
+ if observed.get("running") is not False or observed.get("ready") is not False:
151
+ raise ValueError("runtime teardown stop effect is not present")
152
+ elif phase in {"cleaned", "finalized"}:
153
+ if any(Path(item).exists() for item in value["plan"]["remove"]):
154
+ raise ValueError("runtime teardown cleanup effect is not present")
155
+ if phase == "finalized":
156
+ authority = value["authority"]
157
+ root = Path(str(authority["installation_root"]))
158
+ target = Path(str(authority["target_python"]))
159
+ _verify_broad_stage(root, target)
160
+ else:
161
+ raise ValueError("runtime teardown phase is invalid")
162
+
163
+
164
+ def attest(home: CogniflowHome, expected: object) -> dict[str, object]:
165
+ if not isinstance(expected, dict) or not isinstance(expected.get("transaction_id"), str):
166
+ raise ValueError("runtime teardown attestation request is invalid")
167
+ value = _read(home, expected["transaction_id"])
168
+ actual = _attestation(value)
169
+ if actual != expected:
170
+ raise ValueError("runtime teardown transaction does not match")
171
+ _verify_effects(home, value)
172
+ return actual
173
+
174
+
175
+ def stop(home: CogniflowHome, expected: object, *, timeout: float) -> dict[str, object]:
176
+ attest(home, expected)
177
+ value = _read(home, expected["transaction_id"])
178
+ if value["phase"] != "prepared":
179
+ raise ValueError("runtime teardown transaction is not prepared")
180
+ ProductionRuntime(home).stop(timeout=timeout)
181
+ updated = _write(home, value, "stopped")
182
+ _verify_effects(home, updated)
183
+ return _attestation(updated)
184
+
185
+
186
+ def cleanup(home: CogniflowHome, expected: object) -> dict[str, object]:
187
+ attest(home, expected)
188
+ value = _read(home, expected["transaction_id"])
189
+ if value["phase"] != "stopped":
190
+ raise ValueError("runtime teardown transaction is not stopped")
191
+ for item in value["plan"]["remove"]:
192
+ path = Path(item)
193
+ if path.is_symlink():
194
+ raise ValueError("runtime teardown path is unsafe")
195
+ if path.is_dir():
196
+ shutil.rmtree(path)
197
+ elif path.exists():
198
+ path.unlink()
199
+ updated = _write(home, value, "cleaned")
200
+ _verify_effects(home, updated)
201
+ return _attestation(updated)
202
+
203
+
204
+ def finalize(home: CogniflowHome, expected: object) -> dict[str, object]:
205
+ if not isinstance(expected, dict) or not isinstance(expected.get("transaction_id"), str):
206
+ raise ValueError("runtime teardown finalization request is invalid")
207
+ value = _read(home, expected["transaction_id"])
208
+ if value["phase"] == "finalized":
209
+ actual = _attestation(value)
210
+ stable = {"transaction_id", "nonce", "authority", "preview_digest", "plan", "semantic_graphs", "semantic_receipt", "semantic_receipt_digest"}
211
+ if expected.get("phase") != "cleaned" or any(expected.get(key) != actual.get(key) for key in stable):
212
+ raise ValueError("runtime final cleanup retry does not match")
213
+ _verify_effects(home, value)
214
+ return actual
215
+ attest(home, expected)
216
+ if value["phase"] != "cleaned":
217
+ raise ValueError("runtime teardown transaction is not cleaned")
218
+ authority = value["authority"]
219
+ _verify_broad_stage(Path(str(authority["installation_root"])), Path(str(authority["target_python"])))
220
+ finalized = _write(home, value, "finalized")
221
+ _verify_effects(home, finalized)
222
+ return _attestation(finalized)
223
+
224
+
225
+ def recover(home: CogniflowHome, expected: object, *, timeout: float) -> dict[str, object]:
226
+ if not isinstance(expected, dict) or not isinstance(expected.get("transaction_id"), str):
227
+ raise ValueError("runtime teardown recovery request is invalid")
228
+ value = _read(home, expected["transaction_id"])
229
+ if _attestation(value) != expected or value["phase"] not in {"prepared", "stopped"}:
230
+ raise ValueError("runtime teardown recovery transaction does not match")
231
+ receipt = value.get("semantic_receipt")
232
+ if isinstance(receipt, Mapping):
233
+ restored = (recover_stopped(home, dict(receipt), timeout=timeout) if value["phase"] == "stopped" else restore(home, dict(receipt)))["restored"]
234
+ else:
235
+ restored = []
236
+ _path(home, str(value["transaction_id"])).unlink()
237
+ return {"schema": "cf.runtime.production-teardown-recovery.v1", "status": "recovered", "transaction_id": value["transaction_id"], "restored": restored}
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: cf-runtime
3
+ Version: 0.1.8
4
+ Summary: Local Cogniflow runtime bootstrap and lifecycle provider.
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
+ Requires-Dist: rdflib>=7; extra == 'test'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # cf-runtime
14
+
15
+ `cf_runtime` defines Cogniflow runtime identity, filesystem layout, managed tools, manifests, connection targets, and process lifecycle. Other implementation packages cross its installed process boundary with validated JSON rather than importing runtime internals.
16
+
17
+ ## Runtime model
18
+
19
+ The closed runtime identities are `development` and `production`. Development uses an explicit source tree and semantic seed with managed Java and Fuseki on loopback port 3030. Production uses an explicitly installed instance outside `.cogniflow`, isolated port 3130, the instance's canonical Python executable, and its installed native MCP service executable.
20
+
21
+ `CogniflowHome` and `RuntimePaths` own all `.cogniflow` paths. `RuntimeManifest` schema version 2 is strict, deterministic, pair-validated, and atomically written. Unknown fields and obsolete schemas are rejected. Development lifecycle operations use an exclusive canonical lock and publish process and readiness identity atomically.
22
+
23
+ ## Process protocol
24
+
25
+ `cf-runtime-bootstrap` reads one `cf.runtime.bootstrap.v1` JSON request per stdin line and writes one response per stdout line. Supported methods include target resolution, production preflight, managed-tool provisioning, and development prepare/start/status/stop. The principal outputs are canonical runtime targets, verified managed-tool paths, production composition evidence, and development lifecycle state.
26
+
27
+ Production preflight validates an installed instance and publishes runtime composition but never starts a process. Development preparation receives its source root and semantic seed explicitly. A resolved development target uses stdio and exposes only `COGNIFLOW_RUNTIME_MANIFEST` to the child process.
28
+
29
+ ## Managed tools
30
+
31
+ `managed-tools/provision` accepts `cf.runtime.managed-tools.request.v1` with exact Java and Fuseki versions and returns `cf.runtime.managed-tools.result.v1`. Acquisition is HTTPS-only for production URLs, cache-backed, locked, exact-size and SHA-256 verified, archive-safe, version-checked, and idempotent. Provisioning does not disable TLS verification and does not start Fuseki.
32
+
33
+ ## Boundaries
34
+
35
+ The runtime package does not choose installation profiles, resolve Python artifacts, create the target instance, or own service-level install, repair, transition, and uninstall policy. Bootstrap components may invoke managed-tool provisioning before Fuseki exists; local profile and capability discovery therefore remains outside the runtime and does not depend on SPARQL. Package semantics are under `src/cf_runtime/semantics/`.
@@ -0,0 +1,17 @@
1
+ cf_runtime/__init__.py,sha256=Ou6XYgiE4drrE3_lsEQWGa4RhpF_E7FdjVAXFiM-i7o,1923
2
+ cf_runtime/bootstrap.py,sha256=OrJlTaPV0EI5-a6rt_W0FO0AzyQKK_3RBe7RQRgWojc,20726
3
+ cf_runtime/domain.py,sha256=jst5Dgj2OU7unCWkbYf0bxTuh4YIhJtoPZK7zoF6T9A,538
4
+ cf_runtime/installed_seed.py,sha256=qdubfXabUKZP7sR0prrKgZJ1rkpFS5LejwXU4n9jUoA,2316
5
+ cf_runtime/lifecycle.py,sha256=cHkBYFMTY0cgwSQ4QlSOOwkyMSeicSsOZqFNzIGO9jI,26310
6
+ cf_runtime/locking.py,sha256=qYPiaQaWjAdqKfE4hcdp0uqss6lVKtAvryBaTJOFleo,2153
7
+ cf_runtime/managed_tools.py,sha256=kCDSqa_u4S2jt-NEudTdUelxCMexgJeucldQ5SnMHlk,18776
8
+ cf_runtime/manifest.py,sha256=VebMK7dNiBXxH8ANJEiUmeDYl8Qk5Vlm__HNO4BNAs0,28543
9
+ cf_runtime/paths.py,sha256=yvo1OxMF3d2DhxqjKhl38gQ_CXeBuJVcFEIsUcXNimA,8105
10
+ cf_runtime/semantics.py,sha256=ttDiDmDEhYc-Bpi_VapD2DLm5z6tGk38ahvplyvQmc8,22920
11
+ cf_runtime/target.py,sha256=_JIDBQ-gTkGlhBg9Rhsw0DoyWm5HU8u6FwEc4_oJ7SY,7212
12
+ cf_runtime/teardown.py,sha256=O2uCr7HJ3TFJOB816Q92-T0jsaKlai5-V0mt_MJo-Yo,11087
13
+ cf_runtime/semantics/package.trig,sha256=te-b3sDlVxVjwxCiUfbVmvfOj7ITG7_ORWscYKQ8GtE,1499
14
+ cf_runtime-0.1.8.dist-info/METADATA,sha256=NZltoPiVPRlk2zpFxVc3BKF76WJPW9R08wFRmzmrA9U,2915
15
+ cf_runtime-0.1.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
16
+ cf_runtime-0.1.8.dist-info/entry_points.txt,sha256=lIxZHOM0y9zBbG3t7kQ-jm6AU60PhynIJt0_AEjxxkw,136
17
+ cf_runtime-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,5 @@
1
+ [console_scripts]
2
+ cf-runtime-bootstrap = cf_runtime.bootstrap:main
3
+
4
+ [cogniflow.semantic_sources]
5
+ cf-runtime = cf_runtime:semantic_files