crit 0.10.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.
Files changed (53) hide show
  1. crit/__init__.py +2 -0
  2. crit/cli.py +55 -0
  3. crit/inference.py +153 -0
  4. crit/process/__init__.py +0 -0
  5. crit/process/guardian.py +63 -0
  6. crit/process/ownership.py +270 -0
  7. crit/process/tree.py +283 -0
  8. crit/run/__init__.py +0 -0
  9. crit/run/cli.py +225 -0
  10. crit/run/cost.py +347 -0
  11. crit/run/evaluation.py +174 -0
  12. crit/run/hub.py +144 -0
  13. crit/run/measure.py +365 -0
  14. crit/run/plans.py +1206 -0
  15. crit/run/recon.py +566 -0
  16. crit/run/records.py +1099 -0
  17. crit/run/served.py +184 -0
  18. crit/run/store.py +1106 -0
  19. crit-0.10.0.dist-info/METADATA +508 -0
  20. crit-0.10.0.dist-info/RECORD +53 -0
  21. crit-0.10.0.dist-info/WHEEL +5 -0
  22. crit-0.10.0.dist-info/entry_points.txt +2 -0
  23. crit-0.10.0.dist-info/licenses/LICENSE +208 -0
  24. crit-0.10.0.dist-info/top_level.txt +4 -0
  25. crit_api/__init__.py +1 -0
  26. crit_api/admission.py +153 -0
  27. crit_api/anthropic.py +468 -0
  28. crit_api/catalog.py +87 -0
  29. crit_api/channel.py +214 -0
  30. crit_api/cli.py +220 -0
  31. crit_api/content.py +51 -0
  32. crit_api/delivery.py +114 -0
  33. crit_api/disconnect.py +66 -0
  34. crit_api/engine.py +988 -0
  35. crit_api/host.py +450 -0
  36. crit_api/logprobs.py +140 -0
  37. crit_api/metrics.py +169 -0
  38. crit_api/models.py +320 -0
  39. crit_api/ollama.py +357 -0
  40. crit_api/options.py +344 -0
  41. crit_api/residency.py +159 -0
  42. crit_api/responses.py +410 -0
  43. crit_api/server.py +1866 -0
  44. crit_api/stream.py +100 -0
  45. crit_api/tool_stream.py +97 -0
  46. crit_kernels/__init__.py +351 -0
  47. crit_models/__init__.py +114 -0
  48. crit_models/calibration.py +116 -0
  49. crit_models/codecs.py +156 -0
  50. crit_models/drafter.py +314 -0
  51. crit_models/evaluate.py +71 -0
  52. crit_models/ptq.py +375 -0
  53. crit_models/recipe.py +177 -0
crit/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """crit: run records, Kernels, Eval dispatch and the serving API's command line."""
2
+ __version__ = "0.10.0"
crit/cli.py ADDED
@@ -0,0 +1,55 @@
1
+ """Small product CLI; implementation code is never imported by navigation."""
2
+ import argparse
3
+ from pathlib import Path
4
+ import sys
5
+
6
+ ROOT = Path(__file__).resolve().parents[2]
7
+
8
+
9
+ def main(argv=None):
10
+ # Route subcommands with their own parsers before the maintenance parser.
11
+ arguments = list(sys.argv[1:] if argv is None else argv)
12
+ if arguments and arguments[0] == "run":
13
+ from .run.cli import main as run_main
14
+ return run_main(arguments[1:])
15
+ if arguments and arguments[0] == "serve":
16
+ from crit_api.cli import main as serve_main
17
+ return serve_main(arguments[1:])
18
+ if arguments and arguments[0] == "kernels":
19
+ from crit_kernels import main as kernels_main
20
+ return kernels_main(arguments[1:])
21
+ if arguments and arguments[0] == "models":
22
+ from crit_models import main as models_main
23
+ return models_main(arguments[1:])
24
+ if arguments and arguments[0] == "eval":
25
+ try:
26
+ from crit_eval.cli import main as eval_main
27
+ except ImportError:
28
+ raise SystemExit("crit eval needs crit Eval (https://github.com/crit-labs/crit-eval): install it with "
29
+ "python -m pip install -e ../crit-eval beside this checkout") from None
30
+ return eval_main(arguments[1:])
31
+ parser = argparse.ArgumentParser(description="crit Core, Inference, Kernels, Models, Eval, run records and the serving API")
32
+ from . import __version__
33
+ parser.add_argument("--version", action="version", version="crit " + __version__)
34
+ subs = parser.add_subparsers(dest="command", required=True)
35
+ subs.add_parser("contracts", help="Print native core contract locations")
36
+ subs.add_parser("kernels", help="Inspect kernel applicability and retain verified dependency bytes")
37
+ subs.add_parser("models", help="Transform model weights (PTQ) and record how; use crit models --help")
38
+ subs.add_parser("serve", help="Serve a sealed candidate as an OpenAI-compatible API; use crit serve --help")
39
+ subs.add_parser("run", help="Publish retained run/candidate records; use crit run --help")
40
+ subs.add_parser("eval", help="Run the common speed, quality and exactness suite (crit Eval); use crit eval --help")
41
+ sub = subs.add_parser("inference", help="Query or check the inference techniques")
42
+ sub.add_argument("arguments", nargs=argparse.REMAINDER)
43
+ args = parser.parse_args(arguments)
44
+ repository = ROOT if (ROOT / "core/include/crit/core.h").is_file() else Path.cwd()
45
+ if not (repository / "core/include/crit/core.h").is_file():
46
+ parser.error("Run repository maintenance commands from the crit repository")
47
+ if args.command == "inference":
48
+ from . import inference
49
+ return inference.main(args.arguments, repository)
50
+ print("core/include/crit/core.h\ncore/include/crit/cuda.h\ncore/contracts/README.md")
51
+ return 0
52
+
53
+
54
+ if __name__ == "__main__":
55
+ raise SystemExit(main())
crit/inference.py ADDED
@@ -0,0 +1,153 @@
1
+ """Query/check the crit inference techniques; never import implementation code."""
2
+ import argparse
3
+ import json
4
+ from pathlib import Path, PurePosixPath
5
+ import sys
6
+
7
+ ROOT = Path(__file__).resolve().parents[2]
8
+ GUIDANCE_TEXT = ("use_when", "avoid_when")
9
+ GUIDANCE_LISTS = ("provides", "engine_supplies", "preserve", "adapt")
10
+ # Navigation metadata, not folders: every entry names one category; `list` groups by it in this order.
11
+ CATEGORIES = ("attention", "projections", "experts", "speculation", "sampling-and-constraints",
12
+ "execution", "memory-and-loading")
13
+
14
+
15
+ def under(root, relative):
16
+ if not isinstance(relative, str) or not relative or "\\" in relative or ":" in relative:
17
+ raise ValueError("canonical repository-relative path required")
18
+ path = PurePosixPath(relative)
19
+ if path.is_absolute() or any(p in {"", ".", ".."} for p in relative.split("/")):
20
+ raise ValueError("canonical repository-relative path required")
21
+ target = (root / relative).resolve()
22
+ if not target.is_relative_to(root.resolve()):
23
+ raise ValueError("path escapes the repository")
24
+ return target
25
+
26
+
27
+ def load(root=ROOT):
28
+ """Read authored entries directly, without generated files or code imports."""
29
+ entries = [json.loads(p.read_text(encoding="utf-8")) for p in sorted((root / "inference").glob("*/entry.json"))]
30
+ return {"schema_version": 3, "collection_name": "crit Inference", "entries": entries}
31
+
32
+
33
+ def check(root=ROOT):
34
+ errors = []
35
+ store = load(root)
36
+ ids = set()
37
+ if not store["entries"]:
38
+ errors.append("Inference contains no techniques")
39
+ for entry in store["entries"]:
40
+ ident = entry["id"]
41
+ under(root, "inference/" + ident)
42
+ if ident in ids:
43
+ errors.append("duplicate technique ID")
44
+ ids.add(ident)
45
+ if any(field in entry for field in ("donors", "origin_path", "source_bundle_path", "code_locations")):
46
+ errors.append(f"{ident}: external source-bundle metadata is not a crit technique")
47
+ if entry.get("category") not in CATEGORIES:
48
+ errors.append(f"{ident}: category must be one of " + ", ".join(CATEGORIES))
49
+ if not entry["summary"] or not entry["implementation_scope"] or not entry["code_files"]:
50
+ errors.append(f"{ident}: description and actual implementation required")
51
+ if entry.get("implementation_status") not in {"reference", "native_source"}:
52
+ errors.append(f"{ident}: explicit reference/native-source scope required")
53
+ if any(not entry.get("validation", {}).get(field) for field in ("host", "native", "performance")):
54
+ errors.append(f"{ident}: host/native/performance status must be explicit")
55
+ guidance = entry.get("agent_guidance")
56
+ if not isinstance(guidance, dict):
57
+ errors.append(f"{ident}: agent selection, implementation and adaptation guidance required")
58
+ else:
59
+ for field in GUIDANCE_TEXT:
60
+ value = guidance.get(field)
61
+ if not isinstance(value, str) or not value.strip():
62
+ errors.append(f"{ident}: agent_guidance.{field} must be nonempty text")
63
+ for field in GUIDANCE_LISTS:
64
+ value = guidance.get(field)
65
+ if not isinstance(value, list) or not value or any(not isinstance(item, str) or not item.strip() for item in value):
66
+ errors.append(f"{ident}: agent_guidance.{field} must list concrete responsibilities or choices")
67
+ if entry["code_path"] != f"inference/{ident}/code" or entry["path"] != f"inference/{ident}/README.md":
68
+ errors.append(f"{ident}: technique-local description/code required")
69
+ if not under(root, entry["path"]).is_file():
70
+ errors.append(f"{ident}: description missing")
71
+ for field in ("input", "output", "ownership", "failure", "core_boundary"):
72
+ if not entry["contract"].get(field):
73
+ errors.append(f"{ident}: missing {field} contract")
74
+ for field in ("hardware", "model", "workload"):
75
+ if not entry["requires"].get(field):
76
+ errors.append(f"{ident}: missing {field} requirement")
77
+ actual = False
78
+ listed = set()
79
+ for code in entry["code_files"]:
80
+ path = under(root, code["path"])
81
+ if not path.is_relative_to(under(root, entry["code_path"])) or not path.is_file():
82
+ errors.append(f"{ident}: actual code must be inside the feature")
83
+ elif path.suffix in {".py", ".cpp", ".cu", ".h"} and path.stat().st_size:
84
+ actual = True
85
+ listed.add(path)
86
+ if not actual:
87
+ errors.append(f"{ident}: code folder contains no actual implementation")
88
+ for path in under(root, entry["code_path"]).rglob("*"):
89
+ if path.is_file() and path.suffix in {".py", ".cpp", ".cu", ".h"} and path.resolve() not in listed:
90
+ errors.append(f"{ident}: implementation file missing from metadata")
91
+ return errors
92
+
93
+
94
+ def main(argv=None, root=ROOT):
95
+ parser = argparse.ArgumentParser(description=__doc__)
96
+ subs = parser.add_subparsers(dest="command", required=True)
97
+ for name in ("list", "search", "show"):
98
+ sub = subs.add_parser(name)
99
+ if name != "list":
100
+ sub.add_argument("query")
101
+ sub.add_argument("--json", action="store_true")
102
+ subs.add_parser("check")
103
+ args = parser.parse_args(argv)
104
+ try:
105
+ if args.command == "check":
106
+ errors = check(root)
107
+ if errors:
108
+ raise ValueError("\n".join(errors))
109
+ print("crit Inference passed: actual technique code and authored metadata.")
110
+ else:
111
+ entries = load(root)["entries"]
112
+ if args.command == "show":
113
+ entries = [e for e in entries if e["id"] == args.query]
114
+ if not entries:
115
+ raise ValueError("unknown technique")
116
+ elif args.command == "search":
117
+ words = args.query.casefold().split()
118
+ entries = [e for e in entries if all(w in json.dumps(e).casefold() for w in words)]
119
+ if args.json:
120
+ print(json.dumps(entries[0] if args.command == "show" else entries, indent=2))
121
+ elif args.command == "list":
122
+ for category in CATEGORIES + tuple(sorted({e.get("category") or "" for e in entries} - set(CATEGORIES))):
123
+ group = [e for e in entries if (e.get("category") or "") == category]
124
+ if group:
125
+ print((category or "uncategorized") + ":")
126
+ for entry in group:
127
+ print(" " + entry["id"] + ": " + entry["summary"])
128
+ else:
129
+ for entry in entries:
130
+ print(entry["id"] + ": " + entry["summary"])
131
+ if args.command == "show":
132
+ print("Category: " + str(entry.get("category")))
133
+ print("Scope: " + entry["implementation_scope"])
134
+ guidance = entry["agent_guidance"]
135
+ print("Use when: " + guidance["use_when"])
136
+ print("Consider another approach when: " + guidance["avoid_when"])
137
+ for title, field in (("Supplied here", "provides"), ("Engine supplies", "engine_supplies"),
138
+ ("Preserve", "preserve"), ("Adapt", "adapt")):
139
+ print(title + ":")
140
+ for item in guidance[field]:
141
+ print(" - " + item)
142
+ print("Description and worked example: " + entry["path"])
143
+ print("Code:")
144
+ for code in entry["code_files"]:
145
+ print(" " + code["path"])
146
+ return 0
147
+ except (ValueError, OSError, KeyError, TypeError) as exc:
148
+ print("ERROR: " + str(exc), file=sys.stderr)
149
+ return 1
150
+
151
+
152
+ if __name__ == "__main__":
153
+ raise SystemExit(main())
File without changes
@@ -0,0 +1,63 @@
1
+ """Launch gate and parent-death monitor, inside the supervisor's process tree."""
2
+ from pathlib import Path
3
+ import os
4
+ import signal
5
+ import subprocess
6
+ import sys
7
+ import time
8
+
9
+ if __package__ in {None, ""}:
10
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
11
+ from crit.process.ownership import process_identity, read_ownership
12
+ from crit.run.records import read_json, write_json
13
+
14
+
15
+ def main():
16
+ launch = read_json(Path(sys.argv[1]))
17
+ owner_path = Path(launch["owner_path"])
18
+ owner = read_ownership(owner_path)
19
+ identity = process_identity(os.getpid())
20
+ if (identity in {None, "unknown"} or owner["token"] != launch["token"] or
21
+ process_identity(owner["owner_pid"]) != owner["owner_identity"]):
22
+ return 125
23
+ # A Windows virtual-environment launcher is a different process from the
24
+ # interpreter running this file. Report the actual guardian before the
25
+ # supervisor verifies containment and grants permission to start the engine.
26
+ write_json(Path(sys.argv[1]).with_suffix(".ready.json"),
27
+ {"token": launch["token"], "pid": os.getpid(), "identity": identity}, immutable=True)
28
+ end = time.monotonic() + 10
29
+ while True:
30
+ owner = read_ownership(owner_path)
31
+ if owner["token"] != launch["token"] or process_identity(owner["owner_pid"]) != owner["owner_identity"]:
32
+ return 125
33
+ child = owner.get("process")
34
+ if child and child["pid"] == os.getpid() and child["identity"] == process_identity(os.getpid()):
35
+ break
36
+ if time.monotonic() >= end:
37
+ return 125
38
+ time.sleep(.02)
39
+ # Inherit the guardian's POSIX process group / Windows Job Object.
40
+ try:
41
+ engine = subprocess.Popen(launch["command"], cwd=launch["cwd"], stdin=subprocess.DEVNULL)
42
+ except OSError as exc:
43
+ write_json(Path(sys.argv[1]).with_suffix(".error.json"), {"error": str(exc)}, immutable=True)
44
+ return 125
45
+ while engine.poll() is None:
46
+ try:
47
+ owner = read_ownership(owner_path)
48
+ alive = owner["token"] == launch["token"] and process_identity(owner["owner_pid"]) == owner["owner_identity"]
49
+ except (OSError, ValueError, KeyError) as exc:
50
+ print("Launch guardian could not read ownership: " + str(exc), file=sys.stderr, flush=True)
51
+ alive = False
52
+ if not alive:
53
+ if os.name != "nt":
54
+ os.killpg(os.getpgrp(), signal.SIGKILL)
55
+ engine.kill() # Windows job also closes when its owner exits.
56
+ engine.wait(timeout=5)
57
+ return 125
58
+ time.sleep(.05)
59
+ return engine.returncode
60
+
61
+
62
+ if __name__ == "__main__":
63
+ raise SystemExit(main())
@@ -0,0 +1,270 @@
1
+ """Durable per-user runtime ownership; stale/failed cleanup never grants admission."""
2
+ import getpass
3
+ import hashlib
4
+ import os
5
+ from pathlib import Path
6
+ import signal
7
+ import re
8
+ import shutil
9
+ import tempfile
10
+ import time
11
+ import uuid
12
+
13
+ from crit.run.records import keys, now, number, read_json, safe_path, write_json
14
+ from crit.run.store import lease
15
+
16
+
17
+ def process_identity(pid):
18
+ if not isinstance(pid, int) or pid <= 0:
19
+ return None
20
+ if os.name == "nt":
21
+ import ctypes as c
22
+ from ctypes import wintypes as w
23
+ api = c.WinDLL("kernel32", use_last_error=True)
24
+ api.OpenProcess.argtypes, api.OpenProcess.restype = [w.DWORD, w.BOOL, w.DWORD], w.HANDLE
25
+ api.GetProcessTimes.argtypes = [w.HANDLE] + [c.POINTER(w.FILETIME)] * 4
26
+ api.CloseHandle.argtypes = [w.HANDLE]
27
+ api.GetExitCodeProcess.argtypes = [w.HANDLE, c.POINTER(w.DWORD)]
28
+ handle = api.OpenProcess(0x1000, False, pid)
29
+ if not handle:
30
+ return None if c.get_last_error() in {87, 1168} else "unknown"
31
+ try:
32
+ code = w.DWORD()
33
+ if not api.GetExitCodeProcess(handle, c.byref(code)):
34
+ return "unknown"
35
+ if code.value != 259:
36
+ return None
37
+ values = [w.FILETIME() for _ in range(4)]
38
+ if not api.GetProcessTimes(handle, *(c.byref(v) for v in values)):
39
+ return "unknown"
40
+ return str((values[0].dwHighDateTime << 32) | values[0].dwLowDateTime)
41
+ finally:
42
+ api.CloseHandle(handle)
43
+ try:
44
+ fields = Path(f"/proc/{pid}/stat").read_text().rsplit(")", 1)[1].split()
45
+ if fields[0] in {"Z", "X"}:
46
+ return None
47
+ boot = Path("/proc/sys/kernel/random/boot_id").read_text().strip()
48
+ return boot + ":" + fields[19]
49
+ except FileNotFoundError:
50
+ if Path("/proc").is_dir():
51
+ return None
52
+ except (OSError, IndexError):
53
+ return "unknown"
54
+ try:
55
+ os.kill(pid, 0)
56
+ return "unknown"
57
+ except ProcessLookupError:
58
+ return None
59
+ except PermissionError:
60
+ return "unknown"
61
+
62
+
63
+ def group_alive(pgid):
64
+ if Path("/proc/self/stat").exists():
65
+ for path in Path("/proc").glob("[0-9]*/stat"):
66
+ try:
67
+ fields = path.read_text().rsplit(")", 1)[1].split()
68
+ if int(fields[2]) == pgid and fields[0] not in {"Z", "X"}:
69
+ return True
70
+ except FileNotFoundError:
71
+ continue
72
+ except (OSError, ValueError, IndexError):
73
+ return True # An unobservable member cannot prove settlement.
74
+ return False
75
+ try:
76
+ os.killpg(pgid, 0)
77
+ return True
78
+ except ProcessLookupError:
79
+ return False
80
+
81
+
82
+ def windows_job_settled(name):
83
+ import ctypes as c
84
+ from ctypes import wintypes as w
85
+ if not name:
86
+ raise ValueError("No retained Windows job identity; cleanup cannot be proven")
87
+ api = c.WinDLL("kernel32", use_last_error=True)
88
+ api.OpenJobObjectW.argtypes, api.OpenJobObjectW.restype = [w.DWORD, w.BOOL, w.LPCWSTR], w.HANDLE
89
+ api.QueryInformationJobObject.argtypes = [w.HANDLE, c.c_int, c.c_void_p, w.DWORD, c.c_void_p]
90
+ api.CloseHandle.argtypes = [w.HANDLE]
91
+ handle = api.OpenJobObjectW(4, False, name) # JOB_OBJECT_QUERY
92
+ if not handle:
93
+ if c.get_last_error() == 2: # Destroyed only after the owned processes exit.
94
+ return True
95
+ raise c.WinError(c.get_last_error())
96
+ try:
97
+ class Accounting(c.Structure):
98
+ _fields_ = [("times", c.c_int64 * 4), ("faults", w.DWORD), ("total", w.DWORD),
99
+ ("active", w.DWORD), ("terminated", w.DWORD)]
100
+ value = Accounting()
101
+ if not api.QueryInformationJobObject(handle, 1, c.byref(value), c.sizeof(value), None):
102
+ raise c.WinError(c.get_last_error())
103
+ return value.active == 0
104
+ finally:
105
+ api.CloseHandle(handle)
106
+
107
+
108
+ # How long a killed process tree may take to exit before cleanup counts as failed. Exit is not immediate: a process
109
+ # that pinned tens of GB of host memory or holds a GPU context releases it after SIGKILL (several seconds here).
110
+ RETIREMENT_SECONDS = 60
111
+
112
+
113
+ def read_ownership(path):
114
+ """Read fresh ownership after bounded sharing-denial retries; never infer ownership from cached data.
115
+
116
+ Both supervisor and guardian need this: Windows atomic replacement can briefly deny an open even while
117
+ the owner remains alive. Missing/malformed records and an expired retry still fail closed.
118
+ """
119
+ deadline = time.monotonic() + .25
120
+ while True:
121
+ try:
122
+ return read_json(path)
123
+ except PermissionError:
124
+ if time.monotonic() >= deadline:
125
+ raise
126
+ time.sleep(.005)
127
+
128
+
129
+ def default_directory():
130
+ user = str(os.getuid()) if hasattr(os, "getuid") else hashlib.sha256(getpass.getuser().encode()).hexdigest()[:16]
131
+ return Path(os.environ.get("CRIT_RUNTIME_DIRECTORY", str(Path(tempfile.gettempdir()) / ("crit-runtime-" + user))))
132
+
133
+
134
+ def clean_snapshots(directory, runtime_id):
135
+ """Remove a settled owner's private model snapshots."""
136
+ if runtime_id is None:
137
+ return
138
+ if not isinstance(runtime_id, str) or not re.fullmatch(r"[0-9a-f]{32}", runtime_id):
139
+ raise ValueError("Invalid private runtime identity")
140
+ root = safe_path(directory, "runtimes/" + runtime_id + "/models")
141
+ if root.exists():
142
+ for path in root.rglob("*"):
143
+ safe_path(root, path.relative_to(root).as_posix())
144
+ if path.is_file():
145
+ path.chmod(0o600)
146
+ # Job retirement can precede release of a mapped file's Windows handle.
147
+ # Retry only transient sharing/permission errors on this checked private
148
+ # directory; persistent failure must retain runtime ownership.
149
+ deadline = time.monotonic() + .25
150
+ while True:
151
+ try:
152
+ shutil.rmtree(root)
153
+ break
154
+ except PermissionError as exc:
155
+ if getattr(exc, "winerror", None) not in {5, 32, 33} or time.monotonic() >= deadline:
156
+ raise
157
+ time.sleep(.005)
158
+
159
+
160
+ class RuntimeOwnership:
161
+ def __init__(self, directory=None):
162
+ self.directory = (Path(directory) if directory is not None else default_directory()).expanduser().absolute()
163
+ self.path = safe_path(self.directory, "owner.json")
164
+ self.token = None
165
+ self.failed_tree = None
166
+ self.launch_settled = True # In-process proof for failed guarded launches.
167
+ self.blocked = ""
168
+
169
+ def acquire(self):
170
+ if self.token:
171
+ value = self.read_owned()
172
+ if self.blocked or value["state"] == "blocked":
173
+ raise ValueError("Runtime cleanup is unresolved. Unload/recover it before another request.")
174
+ return
175
+ self.directory.mkdir(parents=True, exist_ok=True, mode=0o700)
176
+ token = uuid.uuid4().hex
177
+ value = {"schema_version": 1, "token": token, "owner_pid": os.getpid(),
178
+ "owner_identity": process_identity(os.getpid()), "process": None,
179
+ "state": "reserved", "updated_at": now(), "error": None}
180
+ try:
181
+ write_json(self.path, value, immutable=True)
182
+ except FileExistsError:
183
+ raise ValueError("Another CRIT runtime owns execution or left unresolved cleanup. Unload it or run crit serve --recover-runtime.") from None
184
+ self.token = token
185
+
186
+ def read_owned(self):
187
+ value = read_ownership(self.path)
188
+ if not self.token or value.get("token") != self.token:
189
+ raise ValueError("Runtime ownership changed")
190
+ return value
191
+
192
+ def update(self, **changes):
193
+ value = self.read_owned()
194
+ write_json(self.path, value | changes | {"updated_at": now()})
195
+
196
+ def bind(self, tree):
197
+ if not tree.guardian or process_identity(tree.guardian["pid"]) != tree.guardian["identity"]:
198
+ raise ValueError("Launch guardian identity must be verified before binding")
199
+ self.update(process=tree.guardian | {"job_name": tree.job_name}, state="running")
200
+
201
+ def block(self, error):
202
+ if self.token:
203
+ self.blocked = str(error) or "Unresolved runtime cleanup"
204
+ try:
205
+ self.update(state="blocked", error=self.blocked)
206
+ except (ValueError, OSError) as exc:
207
+ # The existing exclusive guard still blocks other clients. Keep
208
+ # local quarantine even if the diagnostic update cannot persist.
209
+ self.blocked += "; could not record cleanup failure: " + str(exc)
210
+
211
+ def release(self, settled=False):
212
+ if self.token:
213
+ value = self.read_owned()
214
+ if not settled and (self.blocked or value["state"] == "blocked"):
215
+ raise ValueError("Prove runtime cleanup before releasing ownership")
216
+ self.path.unlink()
217
+ self.token = None
218
+ self.blocked = ""
219
+
220
+ def recover(self):
221
+ """Explicit dead-owner recovery; live owners and PID reuse block it."""
222
+ self.directory.mkdir(parents=True, exist_ok=True, mode=0o700)
223
+ with lease(self.directory):
224
+ if not self.path.exists():
225
+ return {"status": "idle"}
226
+ value = read_json(self.path)
227
+ keys(value, {"schema_version", "token", "owner_pid", "owner_identity", "process", "state", "updated_at", "error"}, {"runtime_id"})
228
+ if value["schema_version"] != 1 or value["state"] not in {"reserved", "running", "blocked"}:
229
+ raise ValueError("Invalid runtime ownership record")
230
+ number(value["owner_pid"], "runtime owner PID", 1, 2 ** 32, integer=True)
231
+ if not isinstance(value["token"], str) or not re.fullmatch(r"[0-9a-f]{32}", value["token"]):
232
+ raise ValueError("Invalid runtime ownership token")
233
+ if value["process"] is not None:
234
+ keys(value["process"], {"pid", "identity"}, {"job_name"})
235
+ number(value["process"]["pid"], "runtime process PID", 1, 2 ** 32, integer=True)
236
+ owner = process_identity(value["owner_pid"])
237
+ if owner is not None and (owner == "unknown" or owner == value["owner_identity"]):
238
+ raise ValueError("The runtime owner is still alive; use its Unload action")
239
+ child = value.get("process")
240
+ if child:
241
+ current = process_identity(child["pid"])
242
+ if current is not None and current != child["identity"]:
243
+ raise ValueError("Process identity changed; refusing ambiguous cleanup")
244
+ if os.name == "nt":
245
+ # Windows job ownership closes on owner death. Do not kill an
246
+ # arbitrary process outside that job or infer that it settled.
247
+ if current is not None or not windows_job_settled(child.get("job_name")):
248
+ raise ValueError("Owned Windows job has not finished closing; retry recovery after settlement")
249
+ else:
250
+ if child["identity"] in {None, "unknown"}:
251
+ raise ValueError("Process birth identity is unavailable; refusing ambiguous recovery")
252
+ boot_path = Path("/proc/sys/kernel/random/boot_id")
253
+ if boot_path.exists() and not child["identity"].startswith(boot_path.read_text().strip() + ":"):
254
+ raise ValueError("Runtime belongs to a different boot; verify and archive its stale guard explicitly")
255
+ if current is None and group_alive(child["pid"]):
256
+ raise ValueError("Live group has no matching birth-identified guardian; refusing ambiguous recovery")
257
+ if group_alive(child["pid"]):
258
+ os.killpg(child["pid"], signal.SIGKILL)
259
+ end = time.monotonic() + RETIREMENT_SECONDS
260
+ while group_alive(child["pid"]) and time.monotonic() < end:
261
+ time.sleep(.02)
262
+ if group_alive(child["pid"]):
263
+ raise ValueError("Owned process group has not settled")
264
+ clean_snapshots(self.directory, value.get("runtime_id"))
265
+ # Before bind, the guardian cannot launch engine code. After bind,
266
+ # recovery has checked the recorded private process group/job.
267
+ recovery = value | {"recovered_at": now(), "recovery": "dead owner; owned process settlement verified"}
268
+ write_json(self.directory / ("recovery-" + uuid.uuid4().hex + ".json"), recovery, immutable=True)
269
+ self.path.unlink()
270
+ return {"status": "recovered"}