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
cortex/__init__.py
ADDED
cortex/adapters.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
from .stop_payload import parse_stop_fields_json
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class NormalizedEvent:
|
|
18
|
+
name: str
|
|
19
|
+
payload: dict[str, Any]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EventAdapter(Protocol):
|
|
23
|
+
def normalize(self, event_name: str, payload: Mapping[str, Any] | None = None) -> NormalizedEvent: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
CANONICAL_EVENT_ALIASES = {
|
|
27
|
+
"session_start": "session_start",
|
|
28
|
+
"sessionstart": "session_start",
|
|
29
|
+
"session_marker": "session_marker",
|
|
30
|
+
"sessionmarker": "session_marker",
|
|
31
|
+
"pre_tool_use": "pre_tool_use",
|
|
32
|
+
"pretooluse": "pre_tool_use",
|
|
33
|
+
"post_tool_use": "post_tool_use",
|
|
34
|
+
"posttooluse": "post_tool_use",
|
|
35
|
+
"stop": "stop",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GenericAdapter:
|
|
40
|
+
EVENT_ALIASES = CANONICAL_EVENT_ALIASES
|
|
41
|
+
|
|
42
|
+
def normalize(self, event_name: str, payload: Mapping[str, Any] | None = None) -> NormalizedEvent:
|
|
43
|
+
name = _normalize_event_name(event_name, self.EVENT_ALIASES)
|
|
44
|
+
data = dict(payload) if isinstance(payload, Mapping) else {}
|
|
45
|
+
return NormalizedEvent(name=name, payload=data)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ClaudeAdapter:
|
|
49
|
+
EVENT_ALIASES = CANONICAL_EVENT_ALIASES
|
|
50
|
+
|
|
51
|
+
def normalize(self, event_name: str, payload: Mapping[str, Any] | None = None) -> NormalizedEvent:
|
|
52
|
+
name = _normalize_event_name(event_name, self.EVENT_ALIASES)
|
|
53
|
+
data = dict(payload) if isinstance(payload, Mapping) else {}
|
|
54
|
+
data = _normalize_claude_payload(data)
|
|
55
|
+
message = data.get("last_assistant_message")
|
|
56
|
+
if isinstance(message, str):
|
|
57
|
+
rewritten = _rewrite_legacy_trailer_markers(message)
|
|
58
|
+
if name == "stop":
|
|
59
|
+
stop_fields, passthrough = _normalize_claude_stop_fields(rewritten)
|
|
60
|
+
if isinstance(stop_fields, dict):
|
|
61
|
+
data["stop_fields"] = stop_fields
|
|
62
|
+
data["last_assistant_message"] = passthrough
|
|
63
|
+
else:
|
|
64
|
+
data["last_assistant_message"] = rewritten
|
|
65
|
+
return NormalizedEvent(name=name, payload=data)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
GEMINI_EVENT_ALIASES = {
|
|
69
|
+
**CANONICAL_EVENT_ALIASES,
|
|
70
|
+
"SessionStart": "session_start",
|
|
71
|
+
"BeforeTool": "pre_tool_use",
|
|
72
|
+
"AfterTool": "post_tool_use",
|
|
73
|
+
"AfterAgent": "stop",
|
|
74
|
+
"SessionEnd": "session_end",
|
|
75
|
+
"BeforeAgent": "before_agent",
|
|
76
|
+
"BeforeModel": "before_model",
|
|
77
|
+
"AfterModel": "after_model",
|
|
78
|
+
"BeforeToolSelection": "before_tool_selection",
|
|
79
|
+
"Notification": "notification",
|
|
80
|
+
"PreCompress": "pre_compress",
|
|
81
|
+
"sessionstart": "session_start",
|
|
82
|
+
"beforetool": "pre_tool_use",
|
|
83
|
+
"aftertool": "post_tool_use",
|
|
84
|
+
"afteragent": "stop",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class GeminiAdapter:
|
|
89
|
+
EVENT_ALIASES = GEMINI_EVENT_ALIASES
|
|
90
|
+
|
|
91
|
+
def normalize(self, event_name: str, payload: Mapping[str, Any] | None = None) -> NormalizedEvent:
|
|
92
|
+
name = _normalize_event_name(event_name, self.EVENT_ALIASES)
|
|
93
|
+
data = dict(payload) if isinstance(payload, Mapping) else {}
|
|
94
|
+
_normalize_session_id(data)
|
|
95
|
+
if name in {"pre_tool_use", "post_tool_use"}:
|
|
96
|
+
_normalize_tool_name(data, candidate_keys=("tool_name",))
|
|
97
|
+
if name == "post_tool_use":
|
|
98
|
+
_normalize_gemini_status(data)
|
|
99
|
+
if name == "stop":
|
|
100
|
+
prompt_response = data.get("prompt_response")
|
|
101
|
+
if not isinstance(prompt_response, str):
|
|
102
|
+
if "prompt_response" not in data:
|
|
103
|
+
logger.warning("Gemini AfterAgent payload missing prompt_response; using empty string fallback.")
|
|
104
|
+
else:
|
|
105
|
+
logger.warning(
|
|
106
|
+
"Gemini AfterAgent prompt_response is not a string (%s); using empty string fallback.",
|
|
107
|
+
type(prompt_response).__name__,
|
|
108
|
+
)
|
|
109
|
+
prompt_response = ""
|
|
110
|
+
stop_fields, passthrough = _normalize_gemini_stop_fields(prompt_response)
|
|
111
|
+
data["stop_fields"] = stop_fields
|
|
112
|
+
data["last_assistant_message"] = passthrough
|
|
113
|
+
return NormalizedEvent(name=name, payload=data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
OPENAI_EVENT_ALIASES = {
|
|
117
|
+
**CANONICAL_EVENT_ALIASES,
|
|
118
|
+
"item/commandexecution/requestapproval": "pre_tool_use",
|
|
119
|
+
"item/commandExecution/requestApproval": "pre_tool_use",
|
|
120
|
+
"command_execution_request_approval": "pre_tool_use",
|
|
121
|
+
"item/commandexecution/completed": "post_tool_use",
|
|
122
|
+
"item/commandExecution/completed": "post_tool_use",
|
|
123
|
+
"command_execution_completed": "post_tool_use",
|
|
124
|
+
"turn/completed": "stop",
|
|
125
|
+
"turn_completed": "stop",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class OpenAIAdapter:
|
|
130
|
+
EVENT_ALIASES = OPENAI_EVENT_ALIASES
|
|
131
|
+
|
|
132
|
+
def normalize(self, event_name: str, payload: Mapping[str, Any] | None = None) -> NormalizedEvent:
|
|
133
|
+
name = _normalize_event_name(event_name, self.EVENT_ALIASES)
|
|
134
|
+
data = dict(payload) if isinstance(payload, Mapping) else {}
|
|
135
|
+
_normalize_session_id(data)
|
|
136
|
+
if name in {"pre_tool_use", "post_tool_use"}:
|
|
137
|
+
_normalize_tool_name(data, candidate_keys=("tool_name", "command", "tool", "action"))
|
|
138
|
+
if name == "stop":
|
|
139
|
+
final_text = data.get("final_text")
|
|
140
|
+
if isinstance(final_text, str) and "last_assistant_message" not in data:
|
|
141
|
+
data["last_assistant_message"] = final_text
|
|
142
|
+
if "stop_fields" in data and isinstance(data.get("stop_fields"), Mapping):
|
|
143
|
+
data["stop_fields"] = dict(data["stop_fields"])
|
|
144
|
+
return NormalizedEvent(name=name, payload=data)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_adapter(adapter_path: str) -> EventAdapter:
|
|
148
|
+
module_name, class_name = _split_adapter_path(adapter_path)
|
|
149
|
+
try:
|
|
150
|
+
module = importlib.import_module(module_name)
|
|
151
|
+
except Exception as exc: # noqa: BLE001
|
|
152
|
+
raise ValueError(f"Failed to import adapter module '{module_name}': {exc}") from exc
|
|
153
|
+
adapter_cls = getattr(module, class_name, None)
|
|
154
|
+
if adapter_cls is None:
|
|
155
|
+
raise ValueError(f"Adapter class '{class_name}' not found in module '{module_name}'.")
|
|
156
|
+
try:
|
|
157
|
+
adapter = adapter_cls()
|
|
158
|
+
except Exception as exc: # noqa: BLE001
|
|
159
|
+
raise ValueError(f"Failed to instantiate adapter '{module_name}:{class_name}': {exc}") from exc
|
|
160
|
+
if not callable(getattr(adapter, "normalize", None)):
|
|
161
|
+
raise TypeError(
|
|
162
|
+
f"Adapter '{module_name}:{class_name}' must define callable normalize(event_name, payload)."
|
|
163
|
+
)
|
|
164
|
+
return adapter
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _split_adapter_path(adapter_path: str) -> tuple[str, str]:
|
|
168
|
+
token = str(adapter_path or "").strip()
|
|
169
|
+
if not token:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"runtime.adapter is required. Set [runtime].adapter = \"module.path:ClassName\" in cortex.toml."
|
|
172
|
+
)
|
|
173
|
+
if ":" not in token:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"Invalid runtime.adapter '{token}'. Expected format 'module.path:ClassName'."
|
|
176
|
+
)
|
|
177
|
+
module_name, class_name = token.split(":", 1)
|
|
178
|
+
module_name = module_name.strip()
|
|
179
|
+
class_name = class_name.strip()
|
|
180
|
+
module_name = _ADAPTER_PATH_ALIASES.get(module_name, module_name)
|
|
181
|
+
if not module_name or not class_name:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"Invalid runtime.adapter '{token}'. Expected format 'module.path:ClassName'."
|
|
184
|
+
)
|
|
185
|
+
return module_name, class_name
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _normalize_event_name(event_name: str, aliases: dict[str, str]) -> str:
|
|
189
|
+
raw = str(event_name or "").strip()
|
|
190
|
+
if raw in aliases:
|
|
191
|
+
return aliases[raw]
|
|
192
|
+
token = raw.lower().replace("-", "_")
|
|
193
|
+
return aliases.get(token) or aliases.get(token.replace("_", "")) or token
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _normalize_claude_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
197
|
+
_normalize_tool_name(payload, candidate_keys=("tool_name", "tool", "toolName", "action"))
|
|
198
|
+
_normalize_session_id(payload)
|
|
199
|
+
if "stop_fields" not in payload and "cortex_stop" in payload:
|
|
200
|
+
payload["stop_fields"] = payload.get("cortex_stop")
|
|
201
|
+
return payload
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _normalize_claude_stop_fields(message: str) -> tuple[dict[str, Any] | None, str]:
|
|
205
|
+
parsed, _, _ = parse_stop_fields_json(message)
|
|
206
|
+
passthrough = _strip_gemini_stop_markers(message)
|
|
207
|
+
if isinstance(parsed, dict):
|
|
208
|
+
stop_fields = {str(k): v for k, v in parsed.items()}
|
|
209
|
+
if passthrough and not stop_fields.get("summary"):
|
|
210
|
+
stop_fields["summary"] = passthrough
|
|
211
|
+
return stop_fields, passthrough
|
|
212
|
+
return None, passthrough or message.strip()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_ADAPTER_PATH_ALIASES = {
|
|
216
|
+
"cortex.adapters.claude": "cortex.adapters",
|
|
217
|
+
"cortex.adapters.generic": "cortex.adapters",
|
|
218
|
+
"cortex.adapters.gemini": "cortex.adapters",
|
|
219
|
+
"cortex.adapters.openai": "cortex.adapters",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _normalize_session_id(payload: dict[str, Any]) -> None:
|
|
224
|
+
raw_session_id = payload.get("session_id")
|
|
225
|
+
if isinstance(raw_session_id, str):
|
|
226
|
+
session_id = raw_session_id.strip()
|
|
227
|
+
if session_id:
|
|
228
|
+
payload["session_id"] = session_id
|
|
229
|
+
return
|
|
230
|
+
payload.pop("session_id", None)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _normalize_tool_name(payload: dict[str, Any], *, candidate_keys: tuple[str, ...]) -> None:
|
|
234
|
+
primary = payload.get("tool_name")
|
|
235
|
+
if isinstance(primary, str) and primary.strip():
|
|
236
|
+
payload["tool_name"] = primary.strip()
|
|
237
|
+
return
|
|
238
|
+
payload.pop("tool_name", None)
|
|
239
|
+
for key in candidate_keys:
|
|
240
|
+
val = payload.get(key)
|
|
241
|
+
if isinstance(val, str) and val.strip():
|
|
242
|
+
payload["tool_name"] = val.strip()
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _normalize_gemini_status(payload: dict[str, Any]) -> None:
|
|
247
|
+
status = payload.get("status")
|
|
248
|
+
if isinstance(status, str) and status.strip():
|
|
249
|
+
payload["status"] = status.strip().lower()
|
|
250
|
+
return
|
|
251
|
+
tool_response = payload.get("tool_response")
|
|
252
|
+
if isinstance(tool_response, Mapping) and tool_response.get("error"):
|
|
253
|
+
payload["status"] = "error"
|
|
254
|
+
return
|
|
255
|
+
payload["status"] = "ok"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _normalize_gemini_stop_fields(prompt_response: str) -> tuple[dict[str, Any], str]:
|
|
259
|
+
parsed, marker_found, error = parse_stop_fields_json(prompt_response)
|
|
260
|
+
passthrough = _strip_gemini_stop_markers(prompt_response)
|
|
261
|
+
|
|
262
|
+
if isinstance(parsed, dict):
|
|
263
|
+
stop_fields = {str(k): v for k, v in parsed.items()}
|
|
264
|
+
if passthrough and not stop_fields.get("summary"):
|
|
265
|
+
stop_fields["summary"] = passthrough
|
|
266
|
+
return stop_fields, passthrough
|
|
267
|
+
|
|
268
|
+
stop_fields = _recover_partial_stop_fields(prompt_response)
|
|
269
|
+
if marker_found and error:
|
|
270
|
+
stop_fields["marker_parse_error"] = error
|
|
271
|
+
if passthrough:
|
|
272
|
+
stop_fields.setdefault("summary", passthrough)
|
|
273
|
+
elif prompt_response.strip():
|
|
274
|
+
stop_fields.setdefault("summary", prompt_response.strip())
|
|
275
|
+
return stop_fields, passthrough
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _strip_gemini_stop_markers(text: str) -> str:
|
|
279
|
+
cleaned = text
|
|
280
|
+
fenced_patterns = (
|
|
281
|
+
r"```(?:stop-fields|stop_fields)\s*\{.*?\}\s*```",
|
|
282
|
+
r"```json\s*\{.*?\"challenge_coverage\".*?\}\s*```",
|
|
283
|
+
)
|
|
284
|
+
for pattern in fenced_patterns:
|
|
285
|
+
cleaned = re.sub(pattern, "", cleaned, flags=re.DOTALL | re.IGNORECASE)
|
|
286
|
+
marker = "STOP_FIELDS_JSON:"
|
|
287
|
+
marker_idx = cleaned.rfind(marker)
|
|
288
|
+
if marker_idx != -1:
|
|
289
|
+
cleaned = cleaned[:marker_idx]
|
|
290
|
+
return cleaned.strip()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _recover_partial_stop_fields(text: str) -> dict[str, Any]:
|
|
294
|
+
recovered: dict[str, Any] = {}
|
|
295
|
+
|
|
296
|
+
coverage: dict[str, bool] = {}
|
|
297
|
+
for key in ("null_inputs", "boundary_values", "error_handling", "graveyard_regression"):
|
|
298
|
+
match = re.search(rf'"{key}"\s*:\s*(true|false)', text, flags=re.IGNORECASE)
|
|
299
|
+
if match:
|
|
300
|
+
coverage[key] = match.group(1).lower() == "true"
|
|
301
|
+
if coverage:
|
|
302
|
+
recovered["challenge_coverage"] = coverage
|
|
303
|
+
|
|
304
|
+
truth_claims: dict[str, list[str]] = {}
|
|
305
|
+
for key in ("modified_files", "tests_ran"):
|
|
306
|
+
values = _recover_string_list(text, key)
|
|
307
|
+
if values:
|
|
308
|
+
truth_claims[key] = values
|
|
309
|
+
if truth_claims:
|
|
310
|
+
recovered["truth_claims"] = truth_claims
|
|
311
|
+
|
|
312
|
+
return recovered
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _recover_string_list(text: str, key: str) -> list[str]:
|
|
316
|
+
match = re.search(rf'"{key}"\s*:\s*\[(.*?)\]', text, flags=re.DOTALL | re.IGNORECASE)
|
|
317
|
+
if not match:
|
|
318
|
+
return []
|
|
319
|
+
values: list[str] = []
|
|
320
|
+
seen: set[str] = set()
|
|
321
|
+
for token in re.findall(r'"((?:\\.|[^"\\])*)"', match.group(1)):
|
|
322
|
+
try:
|
|
323
|
+
value = str(json.loads(f'"{token}"'))
|
|
324
|
+
except json.JSONDecodeError:
|
|
325
|
+
value = token
|
|
326
|
+
cleaned = value.strip()
|
|
327
|
+
if not cleaned or cleaned in seen:
|
|
328
|
+
continue
|
|
329
|
+
seen.add(cleaned)
|
|
330
|
+
values.append(cleaned)
|
|
331
|
+
return values
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _rewrite_legacy_trailer_markers(message: str) -> str:
|
|
335
|
+
return (
|
|
336
|
+
message.replace("CORTEX_STOP_JSON:", "STOP_FIELDS_JSON:")
|
|
337
|
+
.replace("```cortex-stop", "```stop-fields")
|
|
338
|
+
.replace("```cortex_stop", "```stop_fields")
|
|
339
|
+
)
|
cortex/blocklist.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
DEFAULT_BLOCKED_TOOLS: frozenset[str] = frozenset({
|
|
6
|
+
"vim",
|
|
7
|
+
"nvim",
|
|
8
|
+
"nano",
|
|
9
|
+
"emacs",
|
|
10
|
+
"vi",
|
|
11
|
+
"python_repl",
|
|
12
|
+
"ipython",
|
|
13
|
+
"node_repl",
|
|
14
|
+
"irb",
|
|
15
|
+
"gdb",
|
|
16
|
+
"lldb",
|
|
17
|
+
"pdb",
|
|
18
|
+
"less",
|
|
19
|
+
"more",
|
|
20
|
+
"man",
|
|
21
|
+
"ssh",
|
|
22
|
+
"docker_exec_interactive",
|
|
23
|
+
"kubectl_exec",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class BlockVerdict:
|
|
29
|
+
blocked: bool
|
|
30
|
+
reason: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def evaluate_blocklist(
|
|
34
|
+
tool_name: object | None,
|
|
35
|
+
*,
|
|
36
|
+
enabled: bool = True,
|
|
37
|
+
blocked_tools: frozenset[str] = DEFAULT_BLOCKED_TOOLS,
|
|
38
|
+
allowed_tools: frozenset[str] = frozenset(),
|
|
39
|
+
fail_closed: bool = False,
|
|
40
|
+
) -> BlockVerdict:
|
|
41
|
+
if not enabled:
|
|
42
|
+
return BlockVerdict(False, "blocklist_disabled")
|
|
43
|
+
normalized = "" if tool_name is None else str(tool_name).strip().lower()
|
|
44
|
+
normalized = normalized or "unknown"
|
|
45
|
+
if normalized in allowed_tools:
|
|
46
|
+
return BlockVerdict(False, "explicitly_allowed")
|
|
47
|
+
if normalized in blocked_tools:
|
|
48
|
+
return BlockVerdict(True, "tool_denied")
|
|
49
|
+
if fail_closed and normalized not in allowed_tools:
|
|
50
|
+
return BlockVerdict(True, "fail_closed")
|
|
51
|
+
return BlockVerdict(False, "not_in_denylist")
|
cortex/challenges.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .genome import ChallengesConfig
|
|
9
|
+
from .requirements import evaluate_evidence_reference
|
|
10
|
+
from .store import SQLiteStore
|
|
11
|
+
from .templates import BUILTIN_CHALLENGE_TEMPLATES
|
|
12
|
+
from .utils import _as_string_list
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class ChallengeCoverage:
|
|
17
|
+
category: str
|
|
18
|
+
covered: bool
|
|
19
|
+
evidence: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict[str, Any]:
|
|
22
|
+
return {"category": self.category, "covered": self.covered, "evidence": self.evidence}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(slots=True)
|
|
26
|
+
class ChallengeReport:
|
|
27
|
+
active_categories: list[str]
|
|
28
|
+
custom_paths: list[str]
|
|
29
|
+
results: list[ChallengeCoverage]
|
|
30
|
+
missing_categories: list[str]
|
|
31
|
+
unverified_categories: list[str]
|
|
32
|
+
uncheckable_categories: list[str]
|
|
33
|
+
diagnostics: list[dict[str, Any]]
|
|
34
|
+
config_warnings: list[str]
|
|
35
|
+
ok: bool
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"active_categories": self.active_categories,
|
|
40
|
+
"custom_paths": self.custom_paths,
|
|
41
|
+
"results": [r.to_dict() for r in self.results],
|
|
42
|
+
"missing_categories": self.missing_categories,
|
|
43
|
+
"unverified_categories": self.unverified_categories,
|
|
44
|
+
"uncheckable_categories": self.uncheckable_categories,
|
|
45
|
+
"diagnostics": self.diagnostics,
|
|
46
|
+
"config_warnings": self.config_warnings,
|
|
47
|
+
"ok": self.ok,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ChallengeEnforcer:
|
|
52
|
+
def __init__(self, store: SQLiteStore, config: ChallengesConfig) -> None:
|
|
53
|
+
self.store = store
|
|
54
|
+
self.config = config
|
|
55
|
+
|
|
56
|
+
def evaluate(
|
|
57
|
+
self,
|
|
58
|
+
session_id: str,
|
|
59
|
+
coverage_payload: Mapping[str, Any] | None = None,
|
|
60
|
+
*,
|
|
61
|
+
require_verifiable_coverage: bool = False,
|
|
62
|
+
root: Path | None = None,
|
|
63
|
+
witness: Mapping[str, list[str]] | None = None,
|
|
64
|
+
) -> ChallengeReport:
|
|
65
|
+
coverage_payload = coverage_payload or {}
|
|
66
|
+
results: list[ChallengeCoverage] = []
|
|
67
|
+
missing: list[str] = []
|
|
68
|
+
unverified: list[str] = []
|
|
69
|
+
uncheckable: list[str] = []
|
|
70
|
+
diagnostics: list[dict[str, Any]] = []
|
|
71
|
+
config_warnings: list[str] = []
|
|
72
|
+
resolved_root = root.resolve() if root is not None else None
|
|
73
|
+
|
|
74
|
+
missing_builtin = [
|
|
75
|
+
name for name in BUILTIN_CHALLENGE_TEMPLATES if name not in self.config.active_categories
|
|
76
|
+
]
|
|
77
|
+
if missing_builtin:
|
|
78
|
+
config_warnings.append(
|
|
79
|
+
"Built-in challenge categories missing from active set: " + ", ".join(missing_builtin)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
for category in self.config.active_categories:
|
|
83
|
+
raw = coverage_payload.get(category)
|
|
84
|
+
covered, evidence = self._coerce_coverage(raw)
|
|
85
|
+
if require_verifiable_coverage and covered:
|
|
86
|
+
verification = self._verify_covered_category(
|
|
87
|
+
evidence=evidence,
|
|
88
|
+
root=resolved_root,
|
|
89
|
+
witness=witness,
|
|
90
|
+
)
|
|
91
|
+
evidence["verification"] = verification
|
|
92
|
+
verification_status = str(verification.get("status") or "")
|
|
93
|
+
if verification_status != "verified":
|
|
94
|
+
covered = False
|
|
95
|
+
reason = str(verification.get("reason") or "missing verifiable evidence")
|
|
96
|
+
if verification_status == "uncheckable":
|
|
97
|
+
uncheckable.append(category)
|
|
98
|
+
else:
|
|
99
|
+
unverified.append(category)
|
|
100
|
+
config_warnings.append(
|
|
101
|
+
f"Challenge coverage '{category}' marked covered but evidence is {verification_status}: {reason}"
|
|
102
|
+
)
|
|
103
|
+
if not covered:
|
|
104
|
+
missing.append(category)
|
|
105
|
+
diagnostics.append(
|
|
106
|
+
self._coverage_diagnostic(
|
|
107
|
+
category=category,
|
|
108
|
+
raw=raw,
|
|
109
|
+
evidence=evidence,
|
|
110
|
+
require_verifiable_coverage=require_verifiable_coverage,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
result = ChallengeCoverage(category=category, covered=covered, evidence=evidence)
|
|
114
|
+
results.append(result)
|
|
115
|
+
self.store.record_challenge_result(session_id, category, covered, evidence)
|
|
116
|
+
|
|
117
|
+
ok = not missing if self.config.require_coverage else True
|
|
118
|
+
return ChallengeReport(
|
|
119
|
+
active_categories=list(self.config.active_categories),
|
|
120
|
+
custom_paths=list(self.config.custom_paths),
|
|
121
|
+
results=results,
|
|
122
|
+
missing_categories=missing,
|
|
123
|
+
unverified_categories=sorted(set(unverified)),
|
|
124
|
+
uncheckable_categories=sorted(set(uncheckable)),
|
|
125
|
+
diagnostics=diagnostics,
|
|
126
|
+
config_warnings=config_warnings,
|
|
127
|
+
ok=ok,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def missing_coverage_diagnostics(self) -> list[dict[str, Any]]:
|
|
131
|
+
return [
|
|
132
|
+
{"evidence_found": [], "evidence_expected": [f"challenge_coverage for: {', '.join(self.config.active_categories)}"], "gap_description": "No challenge_coverage was provided for the stop attempt.", "gap_characterization": "comprehension_gap", "distance_signal": "far"}
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
def invalid_coverage_diagnostics(self, raw: Any) -> list[dict[str, Any]]:
|
|
136
|
+
return [
|
|
137
|
+
{"evidence_found": [f"challenge_coverage={type(raw).__name__}"], "evidence_expected": ["challenge_coverage object keyed by active challenge categories"], "gap_description": "Challenge coverage used an invalid payload shape.", "gap_characterization": "comprehension_gap", "distance_signal": "far"}
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _coerce_coverage(raw: Any) -> tuple[bool, dict[str, Any]]:
|
|
142
|
+
if isinstance(raw, bool):
|
|
143
|
+
return raw, {}
|
|
144
|
+
if isinstance(raw, Mapping):
|
|
145
|
+
covered = bool(raw.get("covered", False))
|
|
146
|
+
evidence = {str(k): v for k, v in raw.items() if str(k) != "covered"}
|
|
147
|
+
return covered, evidence
|
|
148
|
+
if raw is None:
|
|
149
|
+
return False, {}
|
|
150
|
+
return bool(raw), {"raw": raw}
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _verify_covered_category(
|
|
154
|
+
*,
|
|
155
|
+
evidence: Mapping[str, Any],
|
|
156
|
+
root: Path | None,
|
|
157
|
+
witness: Mapping[str, list[str]] | None,
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
if root is None:
|
|
160
|
+
return {
|
|
161
|
+
"status": "uncheckable",
|
|
162
|
+
"reason": "verification root unavailable",
|
|
163
|
+
"checked_references": [],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
references = _as_string_list(evidence.get("evidence"))
|
|
167
|
+
if not references:
|
|
168
|
+
return {
|
|
169
|
+
"status": "unverified",
|
|
170
|
+
"reason": "covered=true requires non-empty evidence list",
|
|
171
|
+
"checked_references": [],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
checks = [evaluate_evidence_reference(ref, root=root, witness=witness) for ref in references]
|
|
175
|
+
if any(check.get("status") == "verified" for check in checks):
|
|
176
|
+
return {
|
|
177
|
+
"status": "verified",
|
|
178
|
+
"reason": "",
|
|
179
|
+
"checked_references": checks,
|
|
180
|
+
}
|
|
181
|
+
if any(check.get("status") == "uncheckable" for check in checks):
|
|
182
|
+
return {
|
|
183
|
+
"status": "uncheckable",
|
|
184
|
+
"reason": "no verifiable evidence reference was checkable",
|
|
185
|
+
"checked_references": checks,
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
"status": "unverified",
|
|
189
|
+
"reason": "no evidence reference was verified",
|
|
190
|
+
"checked_references": checks,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _coverage_diagnostic(
|
|
195
|
+
*,
|
|
196
|
+
category: str,
|
|
197
|
+
raw: Any,
|
|
198
|
+
evidence: Mapping[str, Any],
|
|
199
|
+
require_verifiable_coverage: bool,
|
|
200
|
+
) -> dict[str, Any]:
|
|
201
|
+
verification = evidence.get("verification") if isinstance(evidence, Mapping) else None
|
|
202
|
+
if raw is None:
|
|
203
|
+
return {"evidence_found": [], "evidence_expected": [f"challenge_coverage.{category}=true"], "gap_description": f"Challenge category '{category}' was not addressed in stop coverage.", "gap_characterization": "comprehension_gap", "distance_signal": "far"}
|
|
204
|
+
|
|
205
|
+
evidence_refs = _as_string_list(evidence.get("evidence")) if isinstance(evidence, Mapping) else []
|
|
206
|
+
if require_verifiable_coverage and isinstance(verification, Mapping):
|
|
207
|
+
status = str(verification.get("status") or "unverified")
|
|
208
|
+
return {"evidence_found": evidence_refs or [f"verification_status={status}"], "evidence_expected": [f"verifiable evidence for challenge '{category}'"], "gap_description": f"Challenge category '{category}' was claimed but not verifiably supported.", "gap_characterization": "execution_gap", "distance_signal": "moderate" if evidence_refs else "far"}
|
|
209
|
+
|
|
210
|
+
return {"evidence_found": ["covered=false"], "evidence_expected": [f"challenge_coverage.{category}=true"], "gap_description": f"Challenge category '{category}' remains uncovered.", "gap_characterization": "comprehension_gap", "distance_signal": "moderate"}
|
cortex/cli.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
from types import ModuleType
|
|
4
|
+
|
|
5
|
+
_IMPL = import_module("cortex_ops_cli.main")
|
|
6
|
+
sys.modules[__name__].__class__ = type("_ShimModule", (ModuleType,), {"__getattr__": lambda self, name: getattr(_IMPL, name), "__setattr__": lambda self, name, value: (setattr(_IMPL, name, value), ModuleType.__setattr__(self, name, value))[1]})
|
|
7
|
+
if __name__ == "__main__": raise SystemExit(_IMPL.main()) # noqa: E701
|