cortex-loop 0.1.0a1__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.
- cortex/__init__.py +7 -0
- cortex/adapters.py +339 -0
- cortex/blocklist.py +51 -0
- cortex/challenges.py +210 -0
- cortex/cli.py +7 -0
- cortex/core.py +601 -0
- cortex/core_helpers.py +190 -0
- cortex/data/identity_preamble.md +5 -0
- cortex/data/layer1_part_a.md +65 -0
- cortex/data/layer1_part_b.md +17 -0
- cortex/executive.py +295 -0
- cortex/foundation.py +185 -0
- cortex/genome.py +348 -0
- cortex/graveyard.py +226 -0
- cortex/hooks/__init__.py +27 -0
- cortex/hooks/_shared.py +167 -0
- cortex/hooks/post_tool_use.py +13 -0
- cortex/hooks/pre_tool_use.py +13 -0
- cortex/hooks/session_start.py +13 -0
- cortex/hooks/stop.py +13 -0
- cortex/invariants.py +258 -0
- cortex/packs.py +118 -0
- cortex/repomap.py +6 -0
- cortex/requirements.py +497 -0
- cortex/retry.py +312 -0
- cortex/stop_contract.py +217 -0
- cortex/stop_payload.py +122 -0
- cortex/stop_policy.py +100 -0
- cortex/stop_runtime.py +400 -0
- cortex/stop_signals.py +75 -0
- cortex/store.py +793 -0
- cortex/templates/__init__.py +10 -0
- cortex/utils.py +58 -0
- cortex_loop-0.1.0a1.dist-info/METADATA +121 -0
- cortex_loop-0.1.0a1.dist-info/RECORD +52 -0
- cortex_loop-0.1.0a1.dist-info/WHEEL +5 -0
- cortex_loop-0.1.0a1.dist-info/entry_points.txt +3 -0
- cortex_loop-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- cortex_loop-0.1.0a1.dist-info/top_level.txt +3 -0
- cortex_ops_cli/__init__.py +3 -0
- cortex_ops_cli/_adapter_validation.py +119 -0
- cortex_ops_cli/_check_report.py +454 -0
- cortex_ops_cli/_check_report_output.py +270 -0
- cortex_ops_cli/_openai_bridge_probe.py +241 -0
- cortex_ops_cli/_openai_bridge_protocol.py +469 -0
- cortex_ops_cli/_runtime_profile_templates.py +341 -0
- cortex_ops_cli/_runtime_profiles.py +445 -0
- cortex_ops_cli/gemini_hooks.py +301 -0
- cortex_ops_cli/main.py +911 -0
- cortex_ops_cli/openai_app_server_bridge.py +375 -0
- cortex_repomap/__init__.py +1 -0
- cortex_repomap/engine.py +1201 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import shutil
|
|
5
|
+
import sqlite3
|
|
6
|
+
import subprocess
|
|
7
|
+
import tomllib
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from cortex import __version__
|
|
14
|
+
from cortex.adapters import load_adapter
|
|
15
|
+
from cortex.genome import GENOME_SCHEMA_VERSION, load_genome
|
|
16
|
+
from cortex.store import DB_SCHEMA_VERSION
|
|
17
|
+
from cortex_ops_cli._runtime_profiles import (
|
|
18
|
+
CLAUDE_ADAPTER_ALIASES,
|
|
19
|
+
CLAUDE_DIRNAME,
|
|
20
|
+
GEMINI_ADAPTER_ALIASES,
|
|
21
|
+
OPENAI_ADAPTER_ALIASES,
|
|
22
|
+
resolve_claude_settings_path,
|
|
23
|
+
resolve_gemini_settings_path,
|
|
24
|
+
resolve_openai_bridge_profile_path,
|
|
25
|
+
validate_claude_settings,
|
|
26
|
+
validate_gemini_settings,
|
|
27
|
+
validate_openai_bridge_profile,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
REQUIRED_DB_TABLES = {"sessions", "graveyard", "invariants", "challenge_results", "events"}
|
|
31
|
+
DEFAULT_REPOMAP_SESSION_START_TIMEOUT_MS = 2500
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def collect_check_report(
|
|
35
|
+
root: Path,
|
|
36
|
+
*,
|
|
37
|
+
repomap_dependency_status: Callable[[], list[str]] | None = None,
|
|
38
|
+
repomap_parser_dependency_status: Callable[[], list[str]] | None = None,
|
|
39
|
+
which: Callable[[str], str | None] = shutil.which,
|
|
40
|
+
find_spec: Callable[[str], object | None] = importlib.util.find_spec,
|
|
41
|
+
run_exec: Callable[..., Any] = subprocess.run,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
ok: list[str] = []
|
|
44
|
+
warn: list[str] = []
|
|
45
|
+
err: list[str] = []
|
|
46
|
+
config_path = root / "cortex.toml"
|
|
47
|
+
db_path = root / ".cortex" / "cortex.db"
|
|
48
|
+
check_data: dict[str, Any] = {
|
|
49
|
+
"root": str(root),
|
|
50
|
+
"generated_at": _now_iso(),
|
|
51
|
+
"cortex_version": __version__,
|
|
52
|
+
"config_schema_version": GENOME_SCHEMA_VERSION,
|
|
53
|
+
"config": {"path": str(config_path), "exists": config_path.exists(), "parse_error": None},
|
|
54
|
+
"db": {
|
|
55
|
+
"path": str(db_path),
|
|
56
|
+
"exists": db_path.exists(),
|
|
57
|
+
"tables": [],
|
|
58
|
+
"missing_tables": [],
|
|
59
|
+
"schema_version": 0,
|
|
60
|
+
"expected_schema_version": DB_SCHEMA_VERSION,
|
|
61
|
+
},
|
|
62
|
+
"hooks": {"settings_path": None, "legacy_settings_path": None, "valid": False},
|
|
63
|
+
"runtime": {
|
|
64
|
+
"adapter_path": None,
|
|
65
|
+
"adapter_valid": False,
|
|
66
|
+
"profile": None,
|
|
67
|
+
},
|
|
68
|
+
"invariants": {"paths": [], "missing_paths": []},
|
|
69
|
+
"repomap": {
|
|
70
|
+
"enabled": None,
|
|
71
|
+
"prefer_ast_graph": None,
|
|
72
|
+
"parity_profile": None,
|
|
73
|
+
"deps_missing": [],
|
|
74
|
+
"parser_deps_missing": [],
|
|
75
|
+
},
|
|
76
|
+
"packs": {"temporal_protocol": {"phase_gate_patterns": []}},
|
|
77
|
+
"ok": ok,
|
|
78
|
+
"warnings": warn,
|
|
79
|
+
"errors": err,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
repomap_dependency_status = repomap_dependency_status or _repomap_dependency_status
|
|
83
|
+
repomap_parser_dependency_status = (
|
|
84
|
+
repomap_parser_dependency_status or _repomap_parser_dependency_status
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
genome = None
|
|
88
|
+
if not config_path.exists():
|
|
89
|
+
err.append(f"Missing config file: {config_path}")
|
|
90
|
+
else:
|
|
91
|
+
genome = load_genome(config_path)
|
|
92
|
+
check_data["config"]["parse_error"] = genome.parse_error
|
|
93
|
+
if genome.parse_error:
|
|
94
|
+
err.append(f"Config parse error in {config_path}: {genome.parse_error}")
|
|
95
|
+
else:
|
|
96
|
+
ok.append(f"Config parsed: {config_path}")
|
|
97
|
+
adapter_path = str(genome.runtime.adapter).strip()
|
|
98
|
+
check_data["runtime"]["adapter_path"] = adapter_path or None
|
|
99
|
+
check_data["packs"]["temporal_protocol"] = pack_temporal_protocol_summary(
|
|
100
|
+
root=root,
|
|
101
|
+
pack_paths=list(genome.packs.paths),
|
|
102
|
+
)
|
|
103
|
+
if not adapter_path:
|
|
104
|
+
err.append(
|
|
105
|
+
"Missing runtime adapter config: set [runtime].adapter = "
|
|
106
|
+
'"module.path:ClassName" (for example cortex.adapters.claude:ClaudeAdapter '
|
|
107
|
+
"or cortex.adapters.gemini:GeminiAdapter)."
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
try:
|
|
111
|
+
load_adapter(adapter_path)
|
|
112
|
+
except Exception as exc: # noqa: BLE001
|
|
113
|
+
err.append(f"Runtime adapter load failed ({adapter_path}): {exc}")
|
|
114
|
+
else:
|
|
115
|
+
ok.append(f"Runtime adapter loaded: {adapter_path}")
|
|
116
|
+
check_data["runtime"]["adapter_valid"] = True
|
|
117
|
+
|
|
118
|
+
if not db_path.exists():
|
|
119
|
+
err.append(f"Missing database file: {db_path}")
|
|
120
|
+
else:
|
|
121
|
+
db_tables, db_schema_version, db_error = _inspect_db(db_path)
|
|
122
|
+
check_data["db"]["tables"] = sorted(db_tables)
|
|
123
|
+
check_data["db"]["schema_version"] = db_schema_version
|
|
124
|
+
if db_error:
|
|
125
|
+
err.append(f"Database check failed: {db_error}")
|
|
126
|
+
else:
|
|
127
|
+
missing_tables = sorted(REQUIRED_DB_TABLES - db_tables)
|
|
128
|
+
check_data["db"]["missing_tables"] = missing_tables
|
|
129
|
+
if missing_tables:
|
|
130
|
+
err.append("Database missing required tables: " + ", ".join(missing_tables))
|
|
131
|
+
else:
|
|
132
|
+
ok.append(f"Database ready: {db_path} (required tables present)")
|
|
133
|
+
if db_schema_version != DB_SCHEMA_VERSION:
|
|
134
|
+
warn.append(
|
|
135
|
+
"Database schema version mismatch: "
|
|
136
|
+
f"found {db_schema_version}, expected {DB_SCHEMA_VERSION} "
|
|
137
|
+
"(run `cortex init-db --root <project>` to refresh metadata)"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if genome and not genome.parse_error:
|
|
141
|
+
check_data["invariants"]["paths"] = list(genome.invariants.suite_paths)
|
|
142
|
+
check_data["repomap"]["enabled"] = genome.repomap.enabled
|
|
143
|
+
check_data["repomap"]["prefer_ast_graph"] = genome.repomap.prefer_ast_graph
|
|
144
|
+
check_data["repomap"]["parity_profile"] = genome.repomap.parity_profile
|
|
145
|
+
_append_check_policy_warnings(genome=genome, warn=warn)
|
|
146
|
+
|
|
147
|
+
if genome.invariants.suite_paths:
|
|
148
|
+
for suite_path in genome.invariants.suite_paths:
|
|
149
|
+
full_path = root / suite_path
|
|
150
|
+
if not full_path.exists():
|
|
151
|
+
check_data["invariants"]["missing_paths"].append(suite_path)
|
|
152
|
+
warn.append(f"Invariant path missing (warning only): {suite_path}")
|
|
153
|
+
if not check_data["invariants"]["missing_paths"]:
|
|
154
|
+
ok.append(f"Invariant paths present: {len(genome.invariants.suite_paths)} configured")
|
|
155
|
+
else:
|
|
156
|
+
warn.append("No invariant suite paths configured in cortex.toml")
|
|
157
|
+
_check_invariant_execution_mode(
|
|
158
|
+
genome=genome,
|
|
159
|
+
root=root,
|
|
160
|
+
ok=ok,
|
|
161
|
+
warn=warn,
|
|
162
|
+
err=err,
|
|
163
|
+
which=which,
|
|
164
|
+
find_spec=find_spec,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if genome.repomap.enabled:
|
|
168
|
+
missing = repomap_dependency_status() if genome.repomap.prefer_ast_graph else []
|
|
169
|
+
parser_missing = (
|
|
170
|
+
repomap_parser_dependency_status() if genome.repomap.prefer_ast_graph else []
|
|
171
|
+
)
|
|
172
|
+
check_data["repomap"]["deps_missing"] = list(missing)
|
|
173
|
+
check_data["repomap"]["parser_deps_missing"] = list(parser_missing)
|
|
174
|
+
if genome.repomap.prefer_ast_graph and missing:
|
|
175
|
+
warn.append(
|
|
176
|
+
"Repo-map enabled; optional ranking dependencies missing "
|
|
177
|
+
"(AST graph + lightweight fallback remain available): "
|
|
178
|
+
+ ", ".join(missing)
|
|
179
|
+
+ " (install with: pip install -e '.[repomap]')"
|
|
180
|
+
)
|
|
181
|
+
elif genome.repomap.prefer_ast_graph:
|
|
182
|
+
ok.append("Repo-map AST parser/ranking dependencies available")
|
|
183
|
+
else:
|
|
184
|
+
ok.append("Repo-map configured for heuristic-only mode (prefer_ast_graph=false)")
|
|
185
|
+
if genome.repomap.prefer_ast_graph and parser_missing:
|
|
186
|
+
warn.append(
|
|
187
|
+
"Repo-map tree-sitter parser dependencies missing: "
|
|
188
|
+
+ ", ".join(parser_missing)
|
|
189
|
+
+ " (install with: pip install -e '.[repomap]')"
|
|
190
|
+
)
|
|
191
|
+
elif genome.repomap.prefer_ast_graph:
|
|
192
|
+
ok.append("Repo-map tree-sitter parser dependencies available")
|
|
193
|
+
if genome.repomap.parity_profile and parser_missing:
|
|
194
|
+
warn.append(
|
|
195
|
+
"Repo-map parity profile is enabled but parser dependencies are missing; "
|
|
196
|
+
"parity runs will fail until dependencies are installed"
|
|
197
|
+
)
|
|
198
|
+
repomap_artifact = root / genome.repomap.artifact_path
|
|
199
|
+
check_data["repomap"]["artifact_path"] = str(repomap_artifact)
|
|
200
|
+
check_data["repomap"]["artifact_exists"] = repomap_artifact.exists()
|
|
201
|
+
if repomap_artifact.exists():
|
|
202
|
+
ok.append(f"Repo-map artifact present: {repomap_artifact}")
|
|
203
|
+
else:
|
|
204
|
+
warn.append(
|
|
205
|
+
"Repo-map artifact missing (warning only): "
|
|
206
|
+
f"{genome.repomap.artifact_path} (run `cortex repomap --root {root}`)"
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
warn.append(
|
|
210
|
+
"Repo-map is disabled in cortex.toml (set [repomap].enabled = true to test it)"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
adapter_path = str(check_data["runtime"]["adapter_path"] or "")
|
|
214
|
+
if adapter_path in CLAUDE_ADAPTER_ALIASES and bool(check_data["runtime"]["adapter_valid"]):
|
|
215
|
+
check_data["runtime"]["profile"] = "claude"
|
|
216
|
+
settings_path, legacy_settings_path = resolve_claude_settings_path(root)
|
|
217
|
+
check_data["hooks"]["settings_path"] = str(settings_path) if settings_path else None
|
|
218
|
+
check_data["hooks"]["legacy_settings_path"] = (
|
|
219
|
+
str(legacy_settings_path) if legacy_settings_path and legacy_settings_path.exists() else None
|
|
220
|
+
)
|
|
221
|
+
if settings_path is None:
|
|
222
|
+
warn.append(
|
|
223
|
+
f"Claude runtime profile settings not found at {root / CLAUDE_DIRNAME / 'settings.json'}; "
|
|
224
|
+
f"run `cortex runtime install --profile claude --root {root}` for hook wiring"
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
settings_errors, settings_warnings = validate_claude_settings(settings_path)
|
|
228
|
+
err.extend(settings_errors)
|
|
229
|
+
warn.extend(settings_warnings)
|
|
230
|
+
if not settings_errors:
|
|
231
|
+
check_data["hooks"]["valid"] = True
|
|
232
|
+
ok.append(f"Runtime profile wiring found in {settings_path}")
|
|
233
|
+
if legacy_settings_path and legacy_settings_path.exists():
|
|
234
|
+
warn.append(
|
|
235
|
+
"Legacy runtime settings also present at "
|
|
236
|
+
f"{legacy_settings_path}; prefer {root / CLAUDE_DIRNAME / 'settings.json'}"
|
|
237
|
+
)
|
|
238
|
+
elif adapter_path in GEMINI_ADAPTER_ALIASES and bool(check_data["runtime"]["adapter_valid"]):
|
|
239
|
+
check_data["runtime"]["profile"] = "gemini"
|
|
240
|
+
settings_path = resolve_gemini_settings_path(root)
|
|
241
|
+
check_data["hooks"]["settings_path"] = str(settings_path) if settings_path else None
|
|
242
|
+
check_data["hooks"]["legacy_settings_path"] = None
|
|
243
|
+
if settings_path is None:
|
|
244
|
+
err.append(
|
|
245
|
+
"No Gemini settings file found. Run cortex runtime install --profile gemini to create one."
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
settings_errors, settings_warnings = validate_gemini_settings(settings_path)
|
|
249
|
+
err.extend(settings_errors)
|
|
250
|
+
warn.extend(settings_warnings)
|
|
251
|
+
if not settings_errors:
|
|
252
|
+
check_data["hooks"]["valid"] = True
|
|
253
|
+
ok.append(f"Runtime profile wiring found in {settings_path}")
|
|
254
|
+
elif adapter_path in OPENAI_ADAPTER_ALIASES and bool(check_data["runtime"]["adapter_valid"]):
|
|
255
|
+
check_data["runtime"]["profile"] = "openai"
|
|
256
|
+
settings_path = resolve_openai_bridge_profile_path(root)
|
|
257
|
+
check_data["hooks"]["settings_path"] = str(settings_path) if settings_path else None
|
|
258
|
+
check_data["hooks"]["legacy_settings_path"] = None
|
|
259
|
+
if settings_path is None:
|
|
260
|
+
err.append(
|
|
261
|
+
"No OpenAI bridge profile found. Run cortex runtime install --profile openai to create one."
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
settings_errors, settings_warnings = validate_openai_bridge_profile(
|
|
265
|
+
settings_path,
|
|
266
|
+
which=which,
|
|
267
|
+
run_exec=run_exec,
|
|
268
|
+
)
|
|
269
|
+
err.extend(settings_errors)
|
|
270
|
+
warn.extend(settings_warnings)
|
|
271
|
+
if not settings_errors:
|
|
272
|
+
check_data["hooks"]["valid"] = True
|
|
273
|
+
ok.append(f"Runtime profile wiring found in {settings_path}")
|
|
274
|
+
elif adapter_path:
|
|
275
|
+
warn.append(
|
|
276
|
+
f"No runtime profile validator implemented for adapter '{adapter_path}' "
|
|
277
|
+
"(kernel checks still active)."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
check_data["summary"] = {"ok": len(ok), "warnings": len(warn), "errors": len(err)}
|
|
281
|
+
check_data["status"] = "ok" if not err else "error"
|
|
282
|
+
return check_data
|
|
283
|
+
def pack_temporal_protocol_summary(*, root: Path, pack_paths: list[str]) -> dict[str, list[str]]:
|
|
284
|
+
patterns: list[str] = []
|
|
285
|
+
for raw_path in pack_paths:
|
|
286
|
+
path_token = str(raw_path).strip()
|
|
287
|
+
if not path_token:
|
|
288
|
+
continue
|
|
289
|
+
manifest = (
|
|
290
|
+
Path(path_token) if Path(path_token).is_absolute() else (root / path_token)
|
|
291
|
+
).expanduser().resolve()
|
|
292
|
+
if not manifest.exists():
|
|
293
|
+
continue
|
|
294
|
+
try:
|
|
295
|
+
with manifest.open("rb") as fh:
|
|
296
|
+
payload = tomllib.load(fh)
|
|
297
|
+
except Exception: # noqa: BLE001
|
|
298
|
+
continue
|
|
299
|
+
if not isinstance(payload, dict):
|
|
300
|
+
continue
|
|
301
|
+
temporal = payload.get("temporal_protocol")
|
|
302
|
+
if not isinstance(temporal, dict):
|
|
303
|
+
continue
|
|
304
|
+
pattern = str(temporal.get("phase_gate_pattern") or "").strip()
|
|
305
|
+
if pattern:
|
|
306
|
+
patterns.append(pattern)
|
|
307
|
+
seen: set[str] = set()
|
|
308
|
+
deduped: list[str] = []
|
|
309
|
+
for pattern in patterns:
|
|
310
|
+
if pattern in seen:
|
|
311
|
+
continue
|
|
312
|
+
seen.add(pattern)
|
|
313
|
+
deduped.append(pattern)
|
|
314
|
+
return {"phase_gate_patterns": deduped}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _append_check_policy_warnings(*, genome: Any, warn: list[str]) -> None:
|
|
318
|
+
warning_rules = (
|
|
319
|
+
(
|
|
320
|
+
bool(genome.repomap.run_on_session_start),
|
|
321
|
+
"repomap.run_on_session_start=true is deprecated/no-op under magic-kernel policy; "
|
|
322
|
+
"use `cortex repomap` for explicit artifact generation",
|
|
323
|
+
),
|
|
324
|
+
(
|
|
325
|
+
genome.repomap.session_start_timeout_ms != DEFAULT_REPOMAP_SESSION_START_TIMEOUT_MS,
|
|
326
|
+
"repomap.session_start_timeout_ms is ignored because SessionStart repo-map execution "
|
|
327
|
+
"is deprecated/no-op; use `cortex repomap --timeout-ms ...` when needed",
|
|
328
|
+
),
|
|
329
|
+
(
|
|
330
|
+
genome.hooks.mode != "strict"
|
|
331
|
+
and genome.hooks.fail_on_missing_challenge_coverage
|
|
332
|
+
and genome.challenges.require_coverage,
|
|
333
|
+
"hooks.fail_on_missing_challenge_coverage=true has no blocking effect while "
|
|
334
|
+
"hooks.mode='advisory'; set [hooks].mode='strict' to enforce missing challenge "
|
|
335
|
+
"coverage as a real gate",
|
|
336
|
+
),
|
|
337
|
+
(
|
|
338
|
+
genome.hooks.require_structured_stop_payload
|
|
339
|
+
and genome.hooks.allow_message_stop_fallback,
|
|
340
|
+
"hooks.require_structured_stop_payload=true while "
|
|
341
|
+
"hooks.allow_message_stop_fallback=true; set allow_message_stop_fallback=false "
|
|
342
|
+
"for fully deterministic structured stop gating",
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
for enabled, message in warning_rules:
|
|
346
|
+
if enabled:
|
|
347
|
+
warn.append(message)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _inspect_db(db_path: Path) -> tuple[set[str], int, str | None]:
|
|
351
|
+
try:
|
|
352
|
+
conn = sqlite3.connect(db_path)
|
|
353
|
+
try:
|
|
354
|
+
rows = conn.execute("SELECT name FROM sqlite_master WHERE type = 'table'").fetchall()
|
|
355
|
+
user_version_row = conn.execute("PRAGMA user_version").fetchone()
|
|
356
|
+
finally:
|
|
357
|
+
conn.close()
|
|
358
|
+
except sqlite3.Error as exc:
|
|
359
|
+
return set(), 0, str(exc)
|
|
360
|
+
user_version = int(user_version_row[0]) if user_version_row else 0
|
|
361
|
+
return {str(row[0]) for row in rows}, user_version, None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _check_invariant_execution_mode(
|
|
365
|
+
*,
|
|
366
|
+
genome: Any,
|
|
367
|
+
root: Path,
|
|
368
|
+
ok: list[str],
|
|
369
|
+
warn: list[str],
|
|
370
|
+
err: list[str],
|
|
371
|
+
which: Callable[[str], str | None],
|
|
372
|
+
find_spec: Callable[[str], object | None],
|
|
373
|
+
) -> None:
|
|
374
|
+
trust_profile = str(
|
|
375
|
+
getattr(getattr(genome, "project", object()), "trust_profile", "trusted")
|
|
376
|
+
).strip().lower()
|
|
377
|
+
if trust_profile not in {"trusted", "untrusted"}:
|
|
378
|
+
err.append(
|
|
379
|
+
"project.trust_profile must be 'trusted' or 'untrusted'; "
|
|
380
|
+
f"got {trust_profile or '<empty>'!r}"
|
|
381
|
+
)
|
|
382
|
+
trust_profile = "untrusted"
|
|
383
|
+
if genome.invariants.execution_mode == "host":
|
|
384
|
+
if trust_profile == "untrusted":
|
|
385
|
+
err.append(
|
|
386
|
+
"invariants.execution_mode='host' is blocked when "
|
|
387
|
+
"project.trust_profile='untrusted'; set execution_mode='container' "
|
|
388
|
+
"or explicitly set trust_profile='trusted'"
|
|
389
|
+
)
|
|
390
|
+
return
|
|
391
|
+
pytest_bin = str(genome.invariants.pytest_bin).strip() or "pytest"
|
|
392
|
+
pytest_path = Path(pytest_bin).expanduser()
|
|
393
|
+
has_configured_bin = False
|
|
394
|
+
if pytest_path.is_absolute():
|
|
395
|
+
has_configured_bin = pytest_path.exists()
|
|
396
|
+
elif any(sep in pytest_bin for sep in ("/", "\\")):
|
|
397
|
+
has_configured_bin = (root / pytest_path).exists() or pytest_path.exists()
|
|
398
|
+
else:
|
|
399
|
+
has_configured_bin = which(pytest_bin) is not None
|
|
400
|
+
|
|
401
|
+
if has_configured_bin:
|
|
402
|
+
ok.append(f"Invariant host pytest runner available: {pytest_bin}")
|
|
403
|
+
elif find_spec("pytest") is not None:
|
|
404
|
+
warn.append(
|
|
405
|
+
f"invariants.pytest_bin='{pytest_bin}' is unavailable; host mode will fallback "
|
|
406
|
+
"to 'python -m pytest' when invariants run"
|
|
407
|
+
)
|
|
408
|
+
else:
|
|
409
|
+
err.append(
|
|
410
|
+
f"invariants.execution_mode='host' but pytest is unavailable "
|
|
411
|
+
f"(configured pytest_bin='{pytest_bin}' not found and python -m pytest not installed)"
|
|
412
|
+
)
|
|
413
|
+
warn.append(
|
|
414
|
+
"invariants.execution_mode='host' runs tests on the host machine; use only for "
|
|
415
|
+
"trusted repositories (switch to execution_mode='container' for stronger isolation)"
|
|
416
|
+
)
|
|
417
|
+
return
|
|
418
|
+
engine = genome.invariants.container_engine.strip()
|
|
419
|
+
if not engine:
|
|
420
|
+
message = (
|
|
421
|
+
"invariants.execution_mode='container' but container_engine is empty; "
|
|
422
|
+
"set [invariants].container_engine (for example 'docker') or switch "
|
|
423
|
+
"to execution_mode='host'"
|
|
424
|
+
)
|
|
425
|
+
if trust_profile == "untrusted":
|
|
426
|
+
err.append(message)
|
|
427
|
+
else:
|
|
428
|
+
warn.append(message)
|
|
429
|
+
return
|
|
430
|
+
if which(engine): # pragma: no branch
|
|
431
|
+
ok.append(f"Invariant container engine available: {engine}")
|
|
432
|
+
return
|
|
433
|
+
message = (
|
|
434
|
+
f"invariants.execution_mode='container' but container engine '{engine}' is not on PATH; "
|
|
435
|
+
f"install '{engine}' or switch to execution_mode='host'"
|
|
436
|
+
)
|
|
437
|
+
if trust_profile == "untrusted":
|
|
438
|
+
err.append(message)
|
|
439
|
+
else:
|
|
440
|
+
warn.append(message)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _repomap_dependency_status() -> list[str]:
|
|
444
|
+
from cortex.repomap import repomap_missing_dependencies
|
|
445
|
+
|
|
446
|
+
return repomap_missing_dependencies()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _repomap_parser_dependency_status() -> list[str]:
|
|
450
|
+
from cortex.repomap import repomap_missing_parser_dependencies
|
|
451
|
+
|
|
452
|
+
return repomap_missing_parser_dependencies()
|
|
453
|
+
def _now_iso() -> str:
|
|
454
|
+
return datetime.now(timezone.utc).isoformat()
|