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.
Files changed (52) hide show
  1. cortex/__init__.py +7 -0
  2. cortex/adapters.py +339 -0
  3. cortex/blocklist.py +51 -0
  4. cortex/challenges.py +210 -0
  5. cortex/cli.py +7 -0
  6. cortex/core.py +601 -0
  7. cortex/core_helpers.py +190 -0
  8. cortex/data/identity_preamble.md +5 -0
  9. cortex/data/layer1_part_a.md +65 -0
  10. cortex/data/layer1_part_b.md +17 -0
  11. cortex/executive.py +295 -0
  12. cortex/foundation.py +185 -0
  13. cortex/genome.py +348 -0
  14. cortex/graveyard.py +226 -0
  15. cortex/hooks/__init__.py +27 -0
  16. cortex/hooks/_shared.py +167 -0
  17. cortex/hooks/post_tool_use.py +13 -0
  18. cortex/hooks/pre_tool_use.py +13 -0
  19. cortex/hooks/session_start.py +13 -0
  20. cortex/hooks/stop.py +13 -0
  21. cortex/invariants.py +258 -0
  22. cortex/packs.py +118 -0
  23. cortex/repomap.py +6 -0
  24. cortex/requirements.py +497 -0
  25. cortex/retry.py +312 -0
  26. cortex/stop_contract.py +217 -0
  27. cortex/stop_payload.py +122 -0
  28. cortex/stop_policy.py +100 -0
  29. cortex/stop_runtime.py +400 -0
  30. cortex/stop_signals.py +75 -0
  31. cortex/store.py +793 -0
  32. cortex/templates/__init__.py +10 -0
  33. cortex/utils.py +58 -0
  34. cortex_loop-0.1.0a1.dist-info/METADATA +121 -0
  35. cortex_loop-0.1.0a1.dist-info/RECORD +52 -0
  36. cortex_loop-0.1.0a1.dist-info/WHEEL +5 -0
  37. cortex_loop-0.1.0a1.dist-info/entry_points.txt +3 -0
  38. cortex_loop-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  39. cortex_loop-0.1.0a1.dist-info/top_level.txt +3 -0
  40. cortex_ops_cli/__init__.py +3 -0
  41. cortex_ops_cli/_adapter_validation.py +119 -0
  42. cortex_ops_cli/_check_report.py +454 -0
  43. cortex_ops_cli/_check_report_output.py +270 -0
  44. cortex_ops_cli/_openai_bridge_probe.py +241 -0
  45. cortex_ops_cli/_openai_bridge_protocol.py +469 -0
  46. cortex_ops_cli/_runtime_profile_templates.py +341 -0
  47. cortex_ops_cli/_runtime_profiles.py +445 -0
  48. cortex_ops_cli/gemini_hooks.py +301 -0
  49. cortex_ops_cli/main.py +911 -0
  50. cortex_ops_cli/openai_app_server_bridge.py +375 -0
  51. cortex_repomap/__init__.py +1 -0
  52. cortex_repomap/engine.py +1201 -0
@@ -0,0 +1,469 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import select
5
+ import subprocess
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+
10
+ COMMAND_APPROVAL_METHOD = "item/commandExecution/requestApproval"
11
+ FILE_CHANGE_APPROVAL_METHOD = "item/fileChange/requestApproval"
12
+ NOTIFICATION_ITEM_COMPLETED = "item/completed"
13
+ NOTIFICATION_TURN_COMPLETED = "turn/completed"
14
+ NOTIFICATION_TASK_COMPLETE = "codex/event/task_complete"
15
+ NOTIFICATION_THREAD_STATUS_CHANGED = "thread/status/changed"
16
+ DEFAULT_APPROVAL_POLICY_CANDIDATES = ("untrusted", "unlessTrusted", "on-request")
17
+
18
+
19
+ class BridgeError(RuntimeError):
20
+ pass
21
+
22
+
23
+ class AppServerClient:
24
+ def __init__(self, *, codex_bin: str, cwd: Path, timeout_seconds: float) -> None:
25
+ cmd = [codex_bin, "app-server", "--listen", "stdio://"]
26
+ self.proc = subprocess.Popen(
27
+ cmd,
28
+ cwd=str(cwd),
29
+ stdin=subprocess.PIPE,
30
+ stdout=subprocess.PIPE,
31
+ stderr=subprocess.PIPE,
32
+ text=True,
33
+ bufsize=1,
34
+ )
35
+ self.timeout_seconds = timeout_seconds
36
+ self._next_request_id = 1
37
+ self._pending_messages: list[dict[str, Any]] = []
38
+
39
+ def close(self) -> None:
40
+ if self.proc.poll() is not None:
41
+ return
42
+ try:
43
+ self.proc.terminate()
44
+ self.proc.wait(timeout=1.0)
45
+ except Exception:
46
+ self.proc.kill()
47
+ self.proc.wait(timeout=1.0)
48
+
49
+ def _write(self, payload: dict[str, Any]) -> None:
50
+ if self.proc.stdin is None:
51
+ raise BridgeError("codex app-server stdin unavailable")
52
+ self.proc.stdin.write(json.dumps(payload) + "\n")
53
+ self.proc.stdin.flush()
54
+
55
+ def _read_line(self, timeout_seconds: float) -> str:
56
+ if self.proc.stdout is None:
57
+ raise BridgeError("codex app-server stdout unavailable")
58
+ fd = self.proc.stdout.fileno()
59
+ ready, _, _ = select.select([fd], [], [], timeout_seconds)
60
+ if not ready:
61
+ raise BridgeError("Timed out waiting for codex app-server response")
62
+ line = self.proc.stdout.readline()
63
+ if not line:
64
+ stderr = ""
65
+ if self.proc.stderr is not None:
66
+ try:
67
+ stderr = self.proc.stderr.read().strip()
68
+ except Exception:
69
+ stderr = ""
70
+ raise BridgeError(f"codex app-server closed stdout. stderr={stderr[:4000]}")
71
+ return line
72
+
73
+ def _read_message(self, timeout_seconds: float, *, allow_pending: bool = True) -> dict[str, Any]:
74
+ if allow_pending and self._pending_messages:
75
+ return self._pending_messages.pop(0)
76
+ line = self._read_line(timeout_seconds)
77
+ try:
78
+ decoded = json.loads(line)
79
+ except json.JSONDecodeError as exc:
80
+ raise BridgeError(f"Invalid JSON-RPC payload from app-server: {exc}") from exc
81
+ if not isinstance(decoded, dict):
82
+ raise BridgeError("Invalid JSON-RPC payload from app-server: expected object")
83
+ return decoded
84
+
85
+ def _pop_pending_response(self, request_id: int) -> dict[str, Any] | None:
86
+ for idx, msg in enumerate(self._pending_messages):
87
+ if msg.get("id") == request_id and ("result" in msg or "error" in msg):
88
+ return self._pending_messages.pop(idx)
89
+ return None
90
+
91
+ def request(
92
+ self,
93
+ method: str,
94
+ params: dict[str, Any] | None = None,
95
+ *,
96
+ timeout_seconds: float | None = None,
97
+ ) -> Any:
98
+ request_id = self._next_request_id
99
+ self._next_request_id += 1
100
+ self._write({"id": request_id, "method": method, "params": params or {}})
101
+ deadline = time.time() + (self.timeout_seconds if timeout_seconds is None else timeout_seconds)
102
+ while True:
103
+ pending_response = self._pop_pending_response(request_id)
104
+ if pending_response is not None:
105
+ if "error" in pending_response:
106
+ raise BridgeError(f"JSON-RPC {method} failed: {pending_response['error']}")
107
+ return pending_response.get("result")
108
+
109
+ remaining = deadline - time.time()
110
+ if remaining <= 0:
111
+ raise BridgeError(f"Timed out waiting for JSON-RPC response to {method}")
112
+
113
+ msg = self._read_message(max(0.01, remaining), allow_pending=False)
114
+ if msg.get("id") == request_id and ("result" in msg or "error" in msg):
115
+ if "error" in msg:
116
+ raise BridgeError(f"JSON-RPC {method} failed: {msg['error']}")
117
+ return msg.get("result")
118
+ self._pending_messages.append(msg)
119
+
120
+ def send_notification(self, method: str, params: dict[str, Any] | None = None) -> None:
121
+ self._write({"method": method, "params": params or {}})
122
+
123
+ def send_server_request_result(self, request_id: Any, result: dict[str, Any]) -> None:
124
+ self._write({"id": request_id, "result": result})
125
+
126
+ def next_message(self, *, timeout_seconds: float | None = None) -> dict[str, Any]:
127
+ return self._read_message(self.timeout_seconds if timeout_seconds is None else timeout_seconds)
128
+
129
+
130
+ class BridgeRunResult(dict):
131
+ pass
132
+
133
+
134
+ def classify_command_surface(
135
+ *,
136
+ command_items: list[dict[str, Any]],
137
+ approval_requests: list[dict[str, Any]],
138
+ ) -> dict[str, Any]:
139
+ approved_item_ids = {
140
+ str(item.get("item_id") or "").strip()
141
+ for item in approval_requests
142
+ if isinstance(item, dict) and str(item.get("method")) == COMMAND_APPROVAL_METHOD
143
+ }
144
+ approved_item_ids.discard("")
145
+ command_item_ids = {
146
+ str(item.get("item_id") or "").strip()
147
+ for item in command_items
148
+ if isinstance(item, dict)
149
+ }
150
+ command_item_ids.discard("")
151
+
152
+ with_approval = sorted(command_item_ids & approved_item_ids)
153
+ without_approval = sorted(command_item_ids - approved_item_ids)
154
+ declined_item_ids = {
155
+ str(item.get("item_id") or "").strip()
156
+ for item in approval_requests
157
+ if isinstance(item, dict)
158
+ and str(item.get("method")) == COMMAND_APPROVAL_METHOD
159
+ and str(item.get("decision")) == "decline"
160
+ }
161
+ declined_item_ids.discard("")
162
+
163
+ nonblocking_declines: list[str] = []
164
+ for item in command_items:
165
+ if not isinstance(item, dict):
166
+ continue
167
+ item_id = str(item.get("item_id") or "").strip()
168
+ if item_id not in declined_item_ids:
169
+ continue
170
+ status = str(item.get("status") or "").strip().lower()
171
+ has_exec_payload = item.get("exit_code") is not None or bool(
172
+ str(item.get("aggregated_output") or "").strip()
173
+ )
174
+ if status != "declined" or has_exec_payload:
175
+ nonblocking_declines.append(item_id or "<missing_item_id>")
176
+
177
+ return {
178
+ "command_item_ids": sorted(command_item_ids),
179
+ "approved_item_ids": sorted(approved_item_ids),
180
+ "command_items_with_approval": with_approval,
181
+ "command_items_without_approval": without_approval,
182
+ "declined_item_ids": sorted(declined_item_ids),
183
+ "nonblocking_declines": sorted(set(nonblocking_declines)),
184
+ }
185
+
186
+
187
+ def initialize_app_server(*, client: AppServerClient) -> None:
188
+ init_result = client.request(
189
+ "initialize",
190
+ {
191
+ "clientInfo": {
192
+ "name": "cortex-openai-bridge",
193
+ "title": "Cortex OpenAI Bridge",
194
+ "version": "1.0.0",
195
+ },
196
+ "capabilities": {
197
+ "experimentalApi": False,
198
+ "optOutNotificationMethods": [],
199
+ },
200
+ },
201
+ )
202
+ if not isinstance(init_result, dict):
203
+ raise BridgeError("initialize did not return a JSON object")
204
+ client.send_notification("initialized", {})
205
+
206
+
207
+ def is_policy_compat_error(exc: BridgeError) -> bool:
208
+ message = str(exc).lower()
209
+ return (
210
+ "approvalpolicy" in message
211
+ or "askforapproval" in message
212
+ or "unknown variant" in message
213
+ or "invalid type" in message
214
+ or "invalid value" in message
215
+ or "unlesstrusted" in message
216
+ )
217
+
218
+
219
+ def start_thread_with_policy_fallback(
220
+ *,
221
+ client: AppServerClient,
222
+ cwd: Path,
223
+ model: str | None,
224
+ approval_policy_candidates: tuple[str, ...] = DEFAULT_APPROVAL_POLICY_CANDIDATES,
225
+ ) -> tuple[dict[str, Any], str]:
226
+ if not approval_policy_candidates:
227
+ raise BridgeError("No approval policy candidates configured.")
228
+
229
+ last_error: BridgeError | None = None
230
+ for idx, policy in enumerate(approval_policy_candidates):
231
+ params: dict[str, Any] = {
232
+ "cwd": str(cwd),
233
+ "approvalPolicy": policy,
234
+ "sandbox": "workspace-write",
235
+ }
236
+ if model:
237
+ params["model"] = model
238
+ try:
239
+ result = client.request("thread/start", params)
240
+ if not isinstance(result, dict):
241
+ raise BridgeError("thread/start did not return a JSON object")
242
+ return result, policy
243
+ except BridgeError as exc:
244
+ last_error = exc
245
+ if idx < len(approval_policy_candidates) - 1 and is_policy_compat_error(exc):
246
+ continue
247
+ raise
248
+
249
+ if last_error is not None:
250
+ raise last_error
251
+ raise BridgeError("thread/start failed: no approval policy candidate succeeded")
252
+
253
+
254
+ def _bridge_turn_result(
255
+ *,
256
+ thread_id: str,
257
+ turn_id: str,
258
+ approval_policy_used: str,
259
+ approval_policy_candidates: tuple[str, ...],
260
+ final_text: str,
261
+ approval_requests: list[dict[str, Any]],
262
+ command_completion_items: list[dict[str, Any]],
263
+ coverage_gaps: list[str],
264
+ duplicate_turn_completed_count: int,
265
+ elapsed_seconds: float,
266
+ ) -> BridgeRunResult:
267
+ return BridgeRunResult(
268
+ {
269
+ "ok": True,
270
+ "thread_id": thread_id,
271
+ "turn_id": turn_id,
272
+ "approval_policy_used": approval_policy_used,
273
+ "approval_policy_candidates": list(approval_policy_candidates),
274
+ "text": final_text,
275
+ "response_present": bool(final_text.strip()),
276
+ "approval_requests": approval_requests,
277
+ "command_completion_items": command_completion_items,
278
+ "command_surface": classify_command_surface(
279
+ command_items=command_completion_items,
280
+ approval_requests=approval_requests,
281
+ ),
282
+ "coverage_gaps": sorted(set(coverage_gaps)),
283
+ "duplicate_turn_completed_count": duplicate_turn_completed_count,
284
+ "elapsed_seconds": elapsed_seconds,
285
+ }
286
+ )
287
+
288
+
289
+ def execute_turn(
290
+ *,
291
+ codex_bin: str,
292
+ cwd: Path,
293
+ prompt: str,
294
+ model: str | None,
295
+ timeout_seconds: float,
296
+ approval_policy_candidates: tuple[str, ...] = DEFAULT_APPROVAL_POLICY_CANDIDATES,
297
+ approval_handler: Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]],
298
+ client_cls: type[AppServerClient] = AppServerClient,
299
+ ) -> BridgeRunResult:
300
+ client = client_cls(codex_bin=codex_bin, cwd=cwd, timeout_seconds=timeout_seconds)
301
+ start_time = time.time()
302
+ try:
303
+ initialize_app_server(client=client)
304
+ thread_result, approval_policy_used = start_thread_with_policy_fallback(
305
+ client=client,
306
+ cwd=cwd,
307
+ model=model,
308
+ approval_policy_candidates=approval_policy_candidates,
309
+ )
310
+ thread_id = str(((thread_result or {}).get("thread") or {}).get("id") or "").strip()
311
+ if not thread_id:
312
+ raise BridgeError("thread/start did not return thread.id")
313
+
314
+ turn_result = client.request(
315
+ "turn/start",
316
+ {
317
+ "threadId": thread_id,
318
+ "input": [{"type": "text", "text": prompt, "textElements": []}],
319
+ "cwd": str(cwd),
320
+ },
321
+ )
322
+ turn_id = str(((turn_result or {}).get("turn") or {}).get("id") or "").strip()
323
+ if not turn_id:
324
+ raise BridgeError("turn/start did not return turn.id")
325
+
326
+ final_agent_messages: dict[str, str] = {}
327
+ completed_turns: set[str] = set()
328
+ duplicate_turn_completed_count = 0
329
+ command_completion_items: list[dict[str, Any]] = []
330
+ approval_requests: list[dict[str, Any]] = []
331
+ coverage_gaps: list[str] = []
332
+ task_complete_turns: set[str] = set()
333
+ task_complete_last_messages: dict[str, str] = {}
334
+
335
+ while True:
336
+ msg = client.next_message()
337
+ method = str(msg.get("method") or "")
338
+
339
+ if method and "id" in msg and "result" not in msg and "error" not in msg:
340
+ request_id = msg.get("id")
341
+ params = msg.get("params") if isinstance(msg.get("params"), dict) else {}
342
+ if method in {COMMAND_APPROVAL_METHOD, FILE_CHANGE_APPROVAL_METHOD}:
343
+ decision, metadata = approval_handler(method, params)
344
+ if decision not in {"accept", "decline", "cancel"}:
345
+ decision = "decline"
346
+ approval_requests.append(
347
+ {
348
+ "method": method,
349
+ "item_id": str(params.get("itemId") or ""),
350
+ "command": str(params.get("command") or ""),
351
+ "cwd": str(params.get("cwd") or ""),
352
+ "decision": decision,
353
+ "metadata": metadata,
354
+ }
355
+ )
356
+ client.send_server_request_result(request_id, {"decision": decision})
357
+ continue
358
+ coverage_gaps.append(f"unhandled_server_request:{method}")
359
+ client.send_server_request_result(request_id, {"decision": "decline"})
360
+ continue
361
+
362
+ if not method:
363
+ continue
364
+
365
+ params = msg.get("params") if isinstance(msg.get("params"), dict) else {}
366
+ if method == NOTIFICATION_ITEM_COMPLETED:
367
+ item = params.get("item") if isinstance(params.get("item"), dict) else {}
368
+ item_type = str(item.get("type") or "")
369
+ item_turn_id = str(params.get("turnId") or "")
370
+ if item_type == "agentMessage" and item_turn_id:
371
+ final_agent_messages[item_turn_id] = str(item.get("text") or "")
372
+ if item_type == "commandExecution":
373
+ command_completion_items.append(
374
+ {
375
+ "item_id": str(item.get("id") or ""),
376
+ "turn_id": item_turn_id,
377
+ "status": str(item.get("status") or ""),
378
+ "exit_code": item.get("exitCode"),
379
+ "aggregated_output": item.get("aggregatedOutput"),
380
+ "command": str(item.get("command") or ""),
381
+ "cwd": str(item.get("cwd") or ""),
382
+ }
383
+ )
384
+ continue
385
+
386
+ if method == NOTIFICATION_TURN_COMPLETED:
387
+ notification_turn = params.get("turn") if isinstance(params.get("turn"), dict) else {}
388
+ completed_turn_id = str(notification_turn.get("id") or "")
389
+ if not completed_turn_id:
390
+ coverage_gaps.append("turn_completed_missing_turn_id")
391
+ continue
392
+ if completed_turn_id in completed_turns:
393
+ duplicate_turn_completed_count += 1
394
+ continue
395
+ completed_turns.add(completed_turn_id)
396
+ if completed_turn_id != turn_id:
397
+ continue
398
+
399
+ final_text = str(final_agent_messages.get(turn_id, "") or "")
400
+ elapsed_seconds = round(time.time() - start_time, 6)
401
+ return _bridge_turn_result(
402
+ thread_id=thread_id,
403
+ turn_id=turn_id,
404
+ approval_policy_used=approval_policy_used,
405
+ approval_policy_candidates=approval_policy_candidates,
406
+ final_text=final_text,
407
+ approval_requests=approval_requests,
408
+ command_completion_items=command_completion_items,
409
+ coverage_gaps=coverage_gaps,
410
+ duplicate_turn_completed_count=duplicate_turn_completed_count,
411
+ elapsed_seconds=elapsed_seconds,
412
+ )
413
+
414
+ if method == NOTIFICATION_TASK_COMPLETE:
415
+ task_msg = params.get("msg") if isinstance(params.get("msg"), dict) else {}
416
+ completed_turn_id = str(task_msg.get("turn_id") or params.get("id") or "").strip()
417
+ if completed_turn_id:
418
+ task_complete_turns.add(completed_turn_id)
419
+ last_message = str(task_msg.get("last_agent_message") or "")
420
+ if last_message:
421
+ task_complete_last_messages[completed_turn_id] = last_message
422
+ if completed_turn_id == turn_id and completed_turn_id not in completed_turns:
423
+ coverage_gaps.append("turn_completed_missing_used_task_complete_fallback")
424
+ final_text = str(
425
+ final_agent_messages.get(turn_id, "")
426
+ or task_complete_last_messages.get(turn_id, "")
427
+ or ""
428
+ )
429
+ elapsed_seconds = round(time.time() - start_time, 6)
430
+ return _bridge_turn_result(
431
+ thread_id=thread_id,
432
+ turn_id=turn_id,
433
+ approval_policy_used=approval_policy_used,
434
+ approval_policy_candidates=approval_policy_candidates,
435
+ final_text=final_text,
436
+ approval_requests=approval_requests,
437
+ command_completion_items=command_completion_items,
438
+ coverage_gaps=coverage_gaps,
439
+ duplicate_turn_completed_count=duplicate_turn_completed_count,
440
+ elapsed_seconds=elapsed_seconds,
441
+ )
442
+ continue
443
+
444
+ if method == NOTIFICATION_THREAD_STATUS_CHANGED:
445
+ event_thread_id = str(params.get("threadId") or "")
446
+ status = params.get("status") if isinstance(params.get("status"), dict) else {}
447
+ status_type = str(status.get("type") or "")
448
+ if event_thread_id == thread_id and status_type == "idle" and turn_id in task_complete_turns:
449
+ coverage_gaps.append("turn_completed_missing_used_task_complete_fallback")
450
+ final_text = str(
451
+ final_agent_messages.get(turn_id, "")
452
+ or task_complete_last_messages.get(turn_id, "")
453
+ or ""
454
+ )
455
+ elapsed_seconds = round(time.time() - start_time, 6)
456
+ return _bridge_turn_result(
457
+ thread_id=thread_id,
458
+ turn_id=turn_id,
459
+ approval_policy_used=approval_policy_used,
460
+ approval_policy_candidates=approval_policy_candidates,
461
+ final_text=final_text,
462
+ approval_requests=approval_requests,
463
+ command_completion_items=command_completion_items,
464
+ coverage_gaps=coverage_gaps,
465
+ duplicate_turn_completed_count=duplicate_turn_completed_count,
466
+ elapsed_seconds=elapsed_seconds,
467
+ )
468
+ finally:
469
+ client.close()