dsh-context-guardian 0.1.0-alpha.10
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.
- package/CHANGELOG.md +555 -0
- package/LICENSE +21 -0
- package/README.md +412 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/cg_anchors.js +203 -0
- package/cg_autorecall.js +215 -0
- package/cg_configure.py +205 -0
- package/cg_doctor.py +449 -0
- package/cg_handoff.js +253 -0
- package/cg_memory.js +509 -0
- package/cg_postcheck.js +173 -0
- package/cg_provenance.js +206 -0
- package/cg_recall.js +559 -0
- package/cg_recall_cli.py +124 -0
- package/configure.py +10 -0
- package/context_guardian.py +1991 -0
- package/cordis.patch.yml +42 -0
- package/docs/dsh-integration.md +229 -0
- package/docs/dsh-upstream-append-only.md +37 -0
- package/engine.js +1407 -0
- package/index.js +255 -0
- package/modules/cg_bridge.py +251 -0
- package/package.json +88 -0
- package/requirements.txt +4 -0
- package/setup.mjs +629 -0
- package/update_check.js +184 -0
- package/vendor/LICENSE.dsh-compaction-instant +28 -0
- package/vendor/compiler.js +851 -0
- package/vendor/region.js +455 -0
package/cg_doctor.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cg_doctor.py -- check a context-guardian install and say how to fix what is wrong.
|
|
3
|
+
|
|
4
|
+
One line per check (OK / WARN / FAIL, each non-OK line followed by a `fix:` line), then a
|
|
5
|
+
count line; exit 1 when a check failed. Stdlib only, Python 3.10+, all three OSes. MIT.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
|
|
15
|
+
# subprocess is used for exactly one thing: the bridge probe below, with an argv list.
|
|
16
|
+
import subprocess # nosec B404
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import urllib.request
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
PACKAGE_ROOT = Path(__file__).resolve().parent
|
|
23
|
+
PKG_FALLBACK_NAME = "dsh-context-guardian"
|
|
24
|
+
DEFAULT_PORT = 8786
|
|
25
|
+
PROXY_DEFAULT_NUM_CTX = 32768
|
|
26
|
+
HELLO_IN = '{"id":1,"op":"hello"}\n{"id":2,"op":"shutdown"}\n'
|
|
27
|
+
RESERVE_FIX = "lower GUARDIAN_RESERVE_OUTPUT or raise GUARDIAN_NUM_CTX"
|
|
28
|
+
NUMCTX_RE = re.compile(r"^\s*numCtx\s*:\s*(\d+)\s*(#.*)?$", re.MULTILINE)
|
|
29
|
+
VERSION_RE = re.compile(r'^__version__\s*=\s*"([^"]+)"', re.MULTILINE)
|
|
30
|
+
PYVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
|
31
|
+
# (key, smallest accepted whole number) -- one table, one loop, no copy-paste
|
|
32
|
+
WHOLE_KEYS = (("GUARDIAN_NUM_CTX", 1), ("GUARDIAN_RESERVE_OUTPUT", 0),
|
|
33
|
+
("GUARDIAN_KEEP_RECENT_MESSAGES", 0), ("GUARDIAN_PORT", 1))
|
|
34
|
+
|
|
35
|
+
def _f(level, what, fix=""):
|
|
36
|
+
return {"level": level, "what": what, "fix": fix}
|
|
37
|
+
|
|
38
|
+
def _whole(raw, minimum=0):
|
|
39
|
+
"""int(raw) when raw is digits only and >= minimum, else None."""
|
|
40
|
+
text = (raw or "").strip()
|
|
41
|
+
if not text.isdigit():
|
|
42
|
+
return None
|
|
43
|
+
number = int(text)
|
|
44
|
+
return number if number >= minimum else None
|
|
45
|
+
|
|
46
|
+
def read_env_file(path: Path) -> dict:
|
|
47
|
+
"""KEY=VALUE per line; skips blank lines, `#` comments and lines without `=`, and
|
|
48
|
+
strips one pair of matching surrounding quotes. Missing/unreadable -> {}; never raises."""
|
|
49
|
+
out = {}
|
|
50
|
+
try:
|
|
51
|
+
text = Path(path).read_text(encoding="utf-8", errors="replace")
|
|
52
|
+
except OSError:
|
|
53
|
+
return out
|
|
54
|
+
for line in text.splitlines():
|
|
55
|
+
if "=" not in line:
|
|
56
|
+
continue
|
|
57
|
+
key, _, value = (part.strip() for part in line.strip().partition("="))
|
|
58
|
+
if key.startswith("#"):
|
|
59
|
+
continue
|
|
60
|
+
if len(value) > 1 and value[0] == value[-1] and value[0] in "\"'":
|
|
61
|
+
value = value[1:-1]
|
|
62
|
+
if key:
|
|
63
|
+
out[key] = value
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
def find_preset_num_ctx(preset: Path | None) -> int | None:
|
|
67
|
+
"""The first non-commented `numCtx: <digits>` in a DSH preset, else None."""
|
|
68
|
+
try:
|
|
69
|
+
text = Path(preset).read_text(encoding="utf-8", errors="replace") if preset else ""
|
|
70
|
+
except OSError:
|
|
71
|
+
return None
|
|
72
|
+
match = NUMCTX_RE.search(text)
|
|
73
|
+
return int(match.group(1)) if match else None
|
|
74
|
+
|
|
75
|
+
def resolve_window(env: dict, preset_num_ctx: int | None) -> dict:
|
|
76
|
+
"""A positive whole-number env GUARDIAN_NUM_CTX wins, then the preset, else the host;
|
|
77
|
+
a bad env value falls through here -- check 2 is what reports it."""
|
|
78
|
+
value = _whole(env.get("GUARDIAN_NUM_CTX"), 1)
|
|
79
|
+
if value is not None:
|
|
80
|
+
return {"source": "env", "value": value}
|
|
81
|
+
if preset_num_ctx is not None:
|
|
82
|
+
return {"source": "preset", "value": preset_num_ctx}
|
|
83
|
+
return {"source": "host", "value": None}
|
|
84
|
+
|
|
85
|
+
def find_python(root: Path, env: dict, which, os_name: str = os.name) -> str | None:
|
|
86
|
+
"""Same order as the JS plugin: $GUARDIAN_PYTHON, a .venv beside the package, then
|
|
87
|
+
python / python3 on PATH. The env value is used as given (no existence test) so a
|
|
88
|
+
typo surfaces as a bridge failure that has a fix."""
|
|
89
|
+
given = (env.get("GUARDIAN_PYTHON") or "").strip()
|
|
90
|
+
if given:
|
|
91
|
+
return given
|
|
92
|
+
if os_name == "nt":
|
|
93
|
+
candidate = Path(root) / ".venv" / "Scripts" / "python.exe"
|
|
94
|
+
else:
|
|
95
|
+
candidate = Path(root) / ".venv" / "bin" / "python"
|
|
96
|
+
return str(candidate) if candidate.is_file() else which("python" if os_name == "nt" else "python3")
|
|
97
|
+
|
|
98
|
+
def _http_get(url: str, timeout: float) -> tuple[int, str]:
|
|
99
|
+
"""urllib GET. Raises OSError (URLError is one) on failure -- never a fake status."""
|
|
100
|
+
opener = urllib.request.build_opener()
|
|
101
|
+
with opener.open(url, timeout=timeout) as response: # loopback URL built by the caller
|
|
102
|
+
return int(response.getcode()), response.read().decode("utf-8", "replace")
|
|
103
|
+
|
|
104
|
+
def _run_bridge(argv: list[str], stdin_text: str, timeout: float) -> tuple[int, str]:
|
|
105
|
+
"""Run the bridge with an argv list, never a shell. A timeout or OSError comes back
|
|
106
|
+
as (-1, "<ExceptionName>: <message>") instead of raising."""
|
|
107
|
+
try:
|
|
108
|
+
# argv is a list and shell is False, and the interpreter is the user's own
|
|
109
|
+
# GUARDIAN_PYTHON / .venv / PATH choice -- so B603 does not apply here.
|
|
110
|
+
proc = subprocess.run(argv, input=stdin_text, capture_output=True, # nosec B603
|
|
111
|
+
text=True, timeout=timeout, shell=False, check=False)
|
|
112
|
+
return proc.returncode, proc.stdout
|
|
113
|
+
except (OSError, ValueError) as exc:
|
|
114
|
+
return -1, f"{type(exc).__name__}: {exc}"
|
|
115
|
+
|
|
116
|
+
def _bridge_version(out: str):
|
|
117
|
+
"""(X.Y.Z, (X, Y)) from the bridge's first non-empty line, else (None, None)."""
|
|
118
|
+
first = next((ln for ln in out.splitlines() if ln.strip()), "")
|
|
119
|
+
try:
|
|
120
|
+
reply = json.loads(first)
|
|
121
|
+
except ValueError:
|
|
122
|
+
return None, None
|
|
123
|
+
result = reply.get("result") if isinstance(reply, dict) and reply.get("ok") is True else None
|
|
124
|
+
version = result.get("python") if isinstance(result, dict) else None
|
|
125
|
+
parts = PYVER_RE.match(version) if isinstance(version, str) else None
|
|
126
|
+
return (version, (int(parts.group(1)), int(parts.group(2)))) if parts else (None, None)
|
|
127
|
+
|
|
128
|
+
def _number_findings(work: dict) -> list[dict]:
|
|
129
|
+
"""Check 2's numbers: one loop over WHOLE_KEYS, the threshold, then the pair."""
|
|
130
|
+
found, nums = [], {}
|
|
131
|
+
for key, minimum in WHOLE_KEYS:
|
|
132
|
+
raw = (work.get(key) or "").strip()
|
|
133
|
+
if not raw:
|
|
134
|
+
continue
|
|
135
|
+
number = _whole(raw, minimum)
|
|
136
|
+
if number is None:
|
|
137
|
+
found.append(_f("error", f"{key}={raw} is not a whole number",
|
|
138
|
+
f"set {key} to digits only (e.g. 8192) in your .env or environment"))
|
|
139
|
+
else:
|
|
140
|
+
nums[key] = number
|
|
141
|
+
threshold = (work.get("GUARDIAN_COMPACT_THRESHOLD") or "").strip()
|
|
142
|
+
try:
|
|
143
|
+
share = float(threshold) if threshold else None
|
|
144
|
+
except ValueError:
|
|
145
|
+
share = None
|
|
146
|
+
if threshold and (share is None or not 0 < share < 1):
|
|
147
|
+
found.append(_f("error", f"GUARDIAN_COMPACT_THRESHOLD={threshold} must be between 0 and 1",
|
|
148
|
+
"use a fraction such as 0.85"))
|
|
149
|
+
num, reserve = nums.get("GUARDIAN_NUM_CTX"), nums.get("GUARDIAN_RESERVE_OUTPUT")
|
|
150
|
+
if num is not None and reserve is not None and reserve >= num:
|
|
151
|
+
found.append(_f("error", f"GUARDIAN_RESERVE_OUTPUT ({reserve}) is not below GUARDIAN_NUM_CTX ({num})",
|
|
152
|
+
RESERVE_FIX))
|
|
153
|
+
return found
|
|
154
|
+
|
|
155
|
+
def doctor(root: Path = PACKAGE_ROOT, env: dict | None = None, preset: Path | None = None,
|
|
156
|
+
http_get=None, run=None, which=shutil.which) -> list[dict]:
|
|
157
|
+
"""Run every check and return the findings, in check order."""
|
|
158
|
+
root = Path(root)
|
|
159
|
+
if env is None:
|
|
160
|
+
env = dict(os.environ)
|
|
161
|
+
get, run_it = (_http_get if http_get is None else http_get), (_run_bridge if run is None else run)
|
|
162
|
+
# the process environment WINS over .env, as the proxy's load_dotenv does.
|
|
163
|
+
work = read_env_file(root / ".env")
|
|
164
|
+
work.update(env)
|
|
165
|
+
out = []
|
|
166
|
+
|
|
167
|
+
# 1. package + proxy version
|
|
168
|
+
name, data = PKG_FALLBACK_NAME, None
|
|
169
|
+
try:
|
|
170
|
+
data = json.loads((root / "package.json").read_text(encoding="utf-8"))
|
|
171
|
+
except OSError:
|
|
172
|
+
data = None
|
|
173
|
+
except ValueError:
|
|
174
|
+
data = "bad"
|
|
175
|
+
if isinstance(data, dict) and isinstance(data.get("version"), str) and data["version"]:
|
|
176
|
+
name = str(data.get("name") or PKG_FALLBACK_NAME)
|
|
177
|
+
out.append(_f("ok", "package: {} {}".format(name, data["version"])))
|
|
178
|
+
else:
|
|
179
|
+
bad = "package.json is not valid JSON" if data == "bad" else "package.json not found"
|
|
180
|
+
out.append(_f("error", bad, "run the doctor from the installed package folder (npm ls dsh-context-guardian shows where)"))
|
|
181
|
+
try:
|
|
182
|
+
match = VERSION_RE.search((root / "context_guardian.py").read_text(encoding="utf-8", errors="replace"))
|
|
183
|
+
except OSError:
|
|
184
|
+
match = None
|
|
185
|
+
if match:
|
|
186
|
+
out.append(_f("ok", f"proxy: context_guardian.py {match.group(1)}"))
|
|
187
|
+
else:
|
|
188
|
+
out.append(_f("warn", "proxy version not found", f"reinstall the package: npm i {name}@latest"))
|
|
189
|
+
|
|
190
|
+
# 2. .env and the numbers in it
|
|
191
|
+
envfile = root / ".env"
|
|
192
|
+
out.append(_f("ok", f"using .env: {envfile}" if envfile.is_file() else "no .env (optional)"))
|
|
193
|
+
out.extend(_number_findings(work))
|
|
194
|
+
|
|
195
|
+
# 3. the window both halves will use
|
|
196
|
+
w = resolve_window(work, find_preset_num_ctx(preset))
|
|
197
|
+
if w["source"] == "env":
|
|
198
|
+
out.append(_f("ok", f"window: {w['value']} tokens from GUARDIAN_NUM_CTX (engine and proxy)"))
|
|
199
|
+
elif w["source"] == "preset":
|
|
200
|
+
out.append(_f("warn", f"window: pinned to {w['value']} by numCtx in {preset}",
|
|
201
|
+
"delete the numCtx line so the engine uses the window DSH reports for your model; "
|
|
202
|
+
f"keep it only if {w['value']} really is your model's window"))
|
|
203
|
+
else:
|
|
204
|
+
out.append(_f("ok", "window: the engine uses the window DSH reports for the session; "
|
|
205
|
+
f"the proxy uses its default {PROXY_DEFAULT_NUM_CTX}"))
|
|
206
|
+
|
|
207
|
+
# 4. the proxy
|
|
208
|
+
host = (work.get("GUARDIAN_HOST") or "").strip() or "127.0.0.1"
|
|
209
|
+
port = _whole(work.get("GUARDIAN_PORT"), 1) or DEFAULT_PORT
|
|
210
|
+
url = f"http://{host}:{port}/guardian/stats"
|
|
211
|
+
stats = None
|
|
212
|
+
try:
|
|
213
|
+
status, body = get(url, 2.0)
|
|
214
|
+
candidate = json.loads(body) if status == 200 else None
|
|
215
|
+
stats = candidate if isinstance(candidate, dict) else None
|
|
216
|
+
except Exception: # noqa: BLE001 -- any transport or parse failure means "not reachable"
|
|
217
|
+
stats = None
|
|
218
|
+
if stats is None:
|
|
219
|
+
out.append(_f("warn", f"proxy not reachable at {url}", "optional: the DSH bundle works without it. To run it: python context_guardian.py"))
|
|
220
|
+
else:
|
|
221
|
+
running = stats.get("num_ctx")
|
|
222
|
+
whole = isinstance(running, int) and not isinstance(running, bool)
|
|
223
|
+
shown = f" (num_ctx {running})" if whole else ""
|
|
224
|
+
out.append(_f("ok", f"proxy reachable at {url}{shown}"))
|
|
225
|
+
if w["source"] == "env" and whole and running != w["value"]:
|
|
226
|
+
out.append(_f("warn", f"proxy is running with num_ctx {running} but GUARDIAN_NUM_CTX is {w['value']}",
|
|
227
|
+
"restart the proxy so it re-reads GUARDIAN_NUM_CTX"))
|
|
228
|
+
|
|
229
|
+
# 5. the Python bridge
|
|
230
|
+
bridge = root / "modules" / "cg_bridge.py"
|
|
231
|
+
if not bridge.is_file():
|
|
232
|
+
out.append(_f("error", "modules/cg_bridge.py is missing", f"reinstall the package: npm i {PKG_FALLBACK_NAME}@latest"))
|
|
233
|
+
else:
|
|
234
|
+
py = find_python(root, work, which)
|
|
235
|
+
if not py:
|
|
236
|
+
out.append(_f("error", "no Python found for the bridge", "install Python 3.10+ or set GUARDIAN_PYTHON to its full path"))
|
|
237
|
+
else:
|
|
238
|
+
try:
|
|
239
|
+
code, said = run_it([py, str(bridge)], HELLO_IN, 20.0)
|
|
240
|
+
except Exception as exc: # noqa: BLE001 -- an injected runner may raise
|
|
241
|
+
code, said = -1, f"{type(exc).__name__}: {exc}"
|
|
242
|
+
version, series = _bridge_version(said or "")
|
|
243
|
+
if series is not None and series < (3, 10):
|
|
244
|
+
out.append(_f("error", f"python bridge runs Python {version}; 3.10+ is required",
|
|
245
|
+
"set GUARDIAN_PYTHON to a Python 3.10+ interpreter"))
|
|
246
|
+
elif series is not None:
|
|
247
|
+
out.append(_f("ok", f"python bridge works: {py} (Python {version})"))
|
|
248
|
+
else:
|
|
249
|
+
detail = (said or "").strip()[-200:] or (f"exit code {code}")
|
|
250
|
+
out.append(_f("error", f"python bridge failed: {detail}",
|
|
251
|
+
f"run: {py} modules/cg_bridge.py --selftest and check GUARDIAN_PYTHON"))
|
|
252
|
+
return out
|
|
253
|
+
|
|
254
|
+
def render(findings) -> tuple[str, int]:
|
|
255
|
+
"""The text report and its exit code: 1 when any finding is an error."""
|
|
256
|
+
lines, counts = [], {"ok": 0, "warn": 0, "error": 0}
|
|
257
|
+
for finding in findings:
|
|
258
|
+
level = finding["level"]
|
|
259
|
+
counts[level] = counts.get(level, 0) + 1
|
|
260
|
+
lines.append("{} {}".format({"ok": "OK ", "warn": "WARN", "error": "FAIL"}[level], finding["what"]))
|
|
261
|
+
if level != "ok" and finding["fix"]:
|
|
262
|
+
lines.append(" fix: {}".format(finding["fix"]))
|
|
263
|
+
lines.append(f"doctor: {counts['ok']} ok, {counts['warn']} warnings, {counts['error']} errors")
|
|
264
|
+
return "\n".join(lines), (1 if counts["error"] else 0)
|
|
265
|
+
|
|
266
|
+
def main(argv=None) -> int:
|
|
267
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
268
|
+
if "--selftest" in args:
|
|
269
|
+
return selftest()
|
|
270
|
+
parser = argparse.ArgumentParser(prog="cg_doctor.py", description="Check a context-guardian install and say how to fix it.")
|
|
271
|
+
parser.add_argument("--preset", type=Path, default=None, help="a DSH preset / cordis.patch.yml to check for a pinned numCtx")
|
|
272
|
+
parser.add_argument("--json", action="store_true", help="print the findings as JSON")
|
|
273
|
+
ns = parser.parse_args(args)
|
|
274
|
+
findings = doctor(preset=ns.preset)
|
|
275
|
+
if ns.json:
|
|
276
|
+
print(json.dumps(findings, indent=2))
|
|
277
|
+
return 1 if any(f["level"] == "error" for f in findings) else 0
|
|
278
|
+
text, code = render(findings)
|
|
279
|
+
print(text)
|
|
280
|
+
return code
|
|
281
|
+
|
|
282
|
+
# --- selftest: real comparisons over temp-dir fixtures; http_get/run/which are fakes.
|
|
283
|
+
def _plant(root, with_pkg=True, proxy="9.9.9"):
|
|
284
|
+
root = Path(root)
|
|
285
|
+
if with_pkg:
|
|
286
|
+
(root / "package.json").write_text(f'{{"name": "{PKG_FALLBACK_NAME}", "version": "1.2.3"}}', encoding="utf-8")
|
|
287
|
+
if proxy is not None:
|
|
288
|
+
(root / "context_guardian.py").write_text(f'__version__ = "{proxy}"\n', encoding="utf-8")
|
|
289
|
+
(root / "modules").mkdir(parents=True, exist_ok=True)
|
|
290
|
+
(root / "modules" / "cg_bridge.py").write_text("# bridge\n", encoding="utf-8")
|
|
291
|
+
return root
|
|
292
|
+
|
|
293
|
+
def _up(url, timeout):
|
|
294
|
+
return 200, '{"num_ctx": 65536}'
|
|
295
|
+
|
|
296
|
+
def _good_run(argv, stdin_text, timeout):
|
|
297
|
+
return 0, '{"id":1,"ok":true,"result":{"python":"3.11.9"}}\n'
|
|
298
|
+
|
|
299
|
+
def _dr(root, env=None, preset=None, http_get=None, run=None):
|
|
300
|
+
"""doctor() over a planted root with every door faked."""
|
|
301
|
+
return doctor(root=root, env=env if env is not None else {"GUARDIAN_PYTHON": "py"}, preset=preset,
|
|
302
|
+
http_get=http_get or _up, run=run or _good_run, which=lambda name: None)
|
|
303
|
+
|
|
304
|
+
def _levels(findings, level):
|
|
305
|
+
return [f for f in findings if f["level"] == level]
|
|
306
|
+
|
|
307
|
+
def _check_env_file_parses_quotes_and_comments():
|
|
308
|
+
with tempfile.TemporaryDirectory() as t:
|
|
309
|
+
path = Path(t) / ".env"
|
|
310
|
+
path.write_text('A="x"\n# B=1\nC = 3\njunk\n', encoding="utf-8")
|
|
311
|
+
return read_env_file(path) == {"A": "x", "C": "3"}
|
|
312
|
+
|
|
313
|
+
def _check_env_file_missing_is_empty():
|
|
314
|
+
with tempfile.TemporaryDirectory() as t:
|
|
315
|
+
return read_env_file(Path(t) / "nope.env") == {}
|
|
316
|
+
|
|
317
|
+
def _check_preset_numctx_found_and_comment_ignored():
|
|
318
|
+
with tempfile.TemporaryDirectory() as t:
|
|
319
|
+
path = Path(t) / "cordis.patch.yml"
|
|
320
|
+
path.write_text(" config:\n # numCtx: 1\n numCtx: 65536\n", encoding="utf-8")
|
|
321
|
+
return (find_preset_num_ctx(path) == 65536 and find_preset_num_ctx(Path(t) / "gone.yml") is None
|
|
322
|
+
and find_preset_num_ctx(None) is None)
|
|
323
|
+
|
|
324
|
+
def _check_window_env_beats_preset():
|
|
325
|
+
return resolve_window({"GUARDIAN_NUM_CTX": "65536"}, 4096) == {"source": "env", "value": 65536}
|
|
326
|
+
|
|
327
|
+
def _check_window_preset_when_no_env():
|
|
328
|
+
return resolve_window({}, 4096) == {"source": "preset", "value": 4096}
|
|
329
|
+
|
|
330
|
+
def _check_window_host_when_neither():
|
|
331
|
+
return resolve_window({}, None) == {"source": "host", "value": None}
|
|
332
|
+
|
|
333
|
+
def _check_window_bad_env_value_falls_through():
|
|
334
|
+
return resolve_window({"GUARDIAN_NUM_CTX": "abc"}, 4096) == {"source": "preset", "value": 4096}
|
|
335
|
+
|
|
336
|
+
def _check_find_python_prefers_env_then_venv_then_path():
|
|
337
|
+
with tempfile.TemporaryDirectory() as t:
|
|
338
|
+
root, asked = Path(t), []
|
|
339
|
+
|
|
340
|
+
def which(name):
|
|
341
|
+
asked.append(name)
|
|
342
|
+
return "PATH-" + name
|
|
343
|
+
|
|
344
|
+
venv = root / ".venv" / "Scripts" / "python.exe"
|
|
345
|
+
venv.parent.mkdir(parents=True)
|
|
346
|
+
venv.write_text("x", encoding="utf-8")
|
|
347
|
+
from_venv = find_python(root, {}, which, os_name="nt")
|
|
348
|
+
venv.unlink()
|
|
349
|
+
from_nt, from_path = find_python(root, {}, which, os_name="nt"), find_python(root, {}, which, "posix")
|
|
350
|
+
from_env = find_python(root, {"GUARDIAN_PYTHON": " C:/py/python.exe "}, which, os_name="nt")
|
|
351
|
+
return (from_venv == str(venv) and from_nt == "PATH-python" and from_path == "PATH-python3"
|
|
352
|
+
and from_env == "C:/py/python.exe" and asked == ["python", "python3"])
|
|
353
|
+
|
|
354
|
+
def _check_default_root_is_the_file_folder():
|
|
355
|
+
return (PACKAGE_ROOT == Path(__file__).resolve().parent and PACKAGE_ROOT.name != "modules"
|
|
356
|
+
and PACKAGE_ROOT.is_dir())
|
|
357
|
+
|
|
358
|
+
def _check_all_good_has_no_errors():
|
|
359
|
+
with tempfile.TemporaryDirectory() as t:
|
|
360
|
+
findings = _dr(_plant(t), env={"GUARDIAN_NUM_CTX": "65536", "GUARDIAN_PYTHON": "py"})
|
|
361
|
+
return len(findings) == 6 and all(f["level"] == "ok" for f in findings)
|
|
362
|
+
|
|
363
|
+
def _check_missing_package_json_is_error():
|
|
364
|
+
with tempfile.TemporaryDirectory() as t:
|
|
365
|
+
return any("package.json not found" in f["what"] and f["fix"]
|
|
366
|
+
for f in _levels(_dr(_plant(t, with_pkg=False)), "error"))
|
|
367
|
+
|
|
368
|
+
def _check_bad_numeric_is_error_with_fix():
|
|
369
|
+
with tempfile.TemporaryDirectory() as t:
|
|
370
|
+
errors = _levels(_dr(_plant(t), env={"GUARDIAN_PYTHON": "py", "GUARDIAN_NUM_CTX": "32k"}), "error")
|
|
371
|
+
return len(errors) == 1 and "GUARDIAN_NUM_CTX=32k" in errors[0]["what"] and bool(errors[0]["fix"])
|
|
372
|
+
|
|
373
|
+
def _check_reserve_not_below_window_is_error():
|
|
374
|
+
with tempfile.TemporaryDirectory() as t:
|
|
375
|
+
env = {"GUARDIAN_PYTHON": "py", "GUARDIAN_NUM_CTX": "8192", "GUARDIAN_RESERVE_OUTPUT": "9999"}
|
|
376
|
+
return any("GUARDIAN_RESERVE_OUTPUT (9999) is not below GUARDIAN_NUM_CTX (8192)" in f["what"]
|
|
377
|
+
and RESERVE_FIX in f["fix"] for f in _levels(_dr(_plant(t), env=env), "error"))
|
|
378
|
+
|
|
379
|
+
def _check_preset_pin_is_warn():
|
|
380
|
+
with tempfile.TemporaryDirectory() as t:
|
|
381
|
+
root = _plant(t)
|
|
382
|
+
preset = root / "cordis.patch.yml"
|
|
383
|
+
preset.write_text("config:\n numCtx: 32768\n", encoding="utf-8")
|
|
384
|
+
return any("pinned to 32768" in f["what"] and f["fix"] for f in _levels(_dr(root, preset=preset), "warn"))
|
|
385
|
+
|
|
386
|
+
def _check_proxy_down_is_warn_not_error():
|
|
387
|
+
def down(url, timeout):
|
|
388
|
+
raise OSError("refused")
|
|
389
|
+
|
|
390
|
+
with tempfile.TemporaryDirectory() as t:
|
|
391
|
+
findings = _dr(_plant(t), http_get=down)
|
|
392
|
+
wanted = f"proxy not reachable at http://127.0.0.1:{DEFAULT_PORT}/guardian/stats"
|
|
393
|
+
return any(wanted in f["what"] and f["fix"] for f in _levels(findings, "warn")) \
|
|
394
|
+
and not _levels(findings, "error")
|
|
395
|
+
|
|
396
|
+
def _check_proxy_window_mismatch_is_warn():
|
|
397
|
+
def stale(url, timeout):
|
|
398
|
+
return 200, '{"num_ctx": 32768}'
|
|
399
|
+
|
|
400
|
+
with tempfile.TemporaryDirectory() as t:
|
|
401
|
+
env = {"GUARDIAN_PYTHON": "py", "GUARDIAN_NUM_CTX": "65536"}
|
|
402
|
+
warns = _levels(_dr(_plant(t), env=env, http_get=stale), "warn")
|
|
403
|
+
return any("num_ctx 32768 but GUARDIAN_NUM_CTX is 65536" in f["what"]
|
|
404
|
+
and "restart the proxy" in f["fix"] for f in warns)
|
|
405
|
+
|
|
406
|
+
def _check_bridge_failure_is_error():
|
|
407
|
+
def crashed(argv, stdin_text, timeout):
|
|
408
|
+
return 1, "Traceback (most recent call last): boom\n"
|
|
409
|
+
|
|
410
|
+
with tempfile.TemporaryDirectory() as t:
|
|
411
|
+
return any("python bridge failed" in f["what"] and "boom" in f["what"] and f["fix"]
|
|
412
|
+
for f in _levels(_dr(_plant(t), run=crashed), "error"))
|
|
413
|
+
|
|
414
|
+
def _check_bridge_old_python_is_error():
|
|
415
|
+
def old(argv, stdin_text, timeout):
|
|
416
|
+
return 0, '{"id":1,"ok":true,"result":{"python":"3.8.10"}}\n'
|
|
417
|
+
|
|
418
|
+
with tempfile.TemporaryDirectory() as t:
|
|
419
|
+
return any("3.10+ is required" in f["what"] and "3.10+" in f["fix"]
|
|
420
|
+
for f in _levels(_dr(_plant(t), run=old), "error"))
|
|
421
|
+
|
|
422
|
+
def _check_cli_exit_codes():
|
|
423
|
+
text, code = render([{"level": "error", "what": "x", "fix": "do y"}, {"level": "ok", "what": "z", "fix": ""}])
|
|
424
|
+
ok_text, ok_code = render([{"level": "ok", "what": "z", "fix": ""}])
|
|
425
|
+
last = text.strip().splitlines()[-1]
|
|
426
|
+
return (code == 1 and ok_code == 0 and "FAIL x" in text and "fix: do y" in text
|
|
427
|
+
and re.fullmatch(r"doctor: 1 ok, 0 warnings, 1 errors", last) is not None
|
|
428
|
+
and ok_text.strip().splitlines()[-1] == "doctor: 1 ok, 0 warnings, 0 errors")
|
|
429
|
+
|
|
430
|
+
CHECKS = [("env_file_parses_quotes_and_comments", _check_env_file_parses_quotes_and_comments), ("env_file_missing_is_empty", _check_env_file_missing_is_empty), ("preset_numctx_found_and_comment_ignored", _check_preset_numctx_found_and_comment_ignored), ("window_env_beats_preset", _check_window_env_beats_preset), ("window_preset_when_no_env", _check_window_preset_when_no_env), ("window_host_when_neither", _check_window_host_when_neither), ("window_bad_env_value_falls_through", _check_window_bad_env_value_falls_through), ("find_python_prefers_env_then_venv_then_path", _check_find_python_prefers_env_then_venv_then_path), ("default_root_is_the_file_folder", _check_default_root_is_the_file_folder), ("all_good_has_no_errors", _check_all_good_has_no_errors), ("missing_package_json_is_error", _check_missing_package_json_is_error), ("bad_numeric_is_error_with_fix", _check_bad_numeric_is_error_with_fix), ("reserve_not_below_window_is_error", _check_reserve_not_below_window_is_error), ("preset_pin_is_warn", _check_preset_pin_is_warn), ("proxy_down_is_warn_not_error", _check_proxy_down_is_warn_not_error), ("proxy_window_mismatch_is_warn", _check_proxy_window_mismatch_is_warn), ("bridge_failure_is_error", _check_bridge_failure_is_error), ("bridge_old_python_is_error", _check_bridge_old_python_is_error), ("cli_exit_codes", _check_cli_exit_codes)]
|
|
431
|
+
|
|
432
|
+
def selftest() -> int:
|
|
433
|
+
total, passed = 0, 0
|
|
434
|
+
for name, function in CHECKS:
|
|
435
|
+
detail = ""
|
|
436
|
+
try:
|
|
437
|
+
ok = bool(function())
|
|
438
|
+
except Exception as exc: # noqa: BLE001 -- a crash is a failed check
|
|
439
|
+
ok, detail = False, f" ({type(exc).__name__}: {exc})"
|
|
440
|
+
if ok:
|
|
441
|
+
passed += 1
|
|
442
|
+
print(" {} {}{}".format("ok " if ok else "FAIL", name, detail))
|
|
443
|
+
total += 1
|
|
444
|
+
failed = total - passed
|
|
445
|
+
print(f"cg_doctor selftest: {total} checks, {passed} passed, {failed} failed")
|
|
446
|
+
return 1 if failed or total == 0 else 0
|
|
447
|
+
|
|
448
|
+
if __name__ == "__main__":
|
|
449
|
+
sys.exit(main())
|