cortex-loop 0.1.0a1__tar.gz

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 (103) hide show
  1. cortex_loop-0.1.0a1/LICENSE +21 -0
  2. cortex_loop-0.1.0a1/MANIFEST.in +1 -0
  3. cortex_loop-0.1.0a1/PKG-INFO +121 -0
  4. cortex_loop-0.1.0a1/README.md +89 -0
  5. cortex_loop-0.1.0a1/cortex/__init__.py +7 -0
  6. cortex_loop-0.1.0a1/cortex/adapters.py +339 -0
  7. cortex_loop-0.1.0a1/cortex/blocklist.py +51 -0
  8. cortex_loop-0.1.0a1/cortex/challenges.py +210 -0
  9. cortex_loop-0.1.0a1/cortex/cli.py +7 -0
  10. cortex_loop-0.1.0a1/cortex/core.py +601 -0
  11. cortex_loop-0.1.0a1/cortex/core_helpers.py +190 -0
  12. cortex_loop-0.1.0a1/cortex/data/identity_preamble.md +5 -0
  13. cortex_loop-0.1.0a1/cortex/data/layer1_part_a.md +65 -0
  14. cortex_loop-0.1.0a1/cortex/data/layer1_part_b.md +17 -0
  15. cortex_loop-0.1.0a1/cortex/executive.py +295 -0
  16. cortex_loop-0.1.0a1/cortex/foundation.py +185 -0
  17. cortex_loop-0.1.0a1/cortex/genome.py +348 -0
  18. cortex_loop-0.1.0a1/cortex/graveyard.py +226 -0
  19. cortex_loop-0.1.0a1/cortex/hooks/__init__.py +27 -0
  20. cortex_loop-0.1.0a1/cortex/hooks/_shared.py +167 -0
  21. cortex_loop-0.1.0a1/cortex/hooks/post_tool_use.py +13 -0
  22. cortex_loop-0.1.0a1/cortex/hooks/pre_tool_use.py +13 -0
  23. cortex_loop-0.1.0a1/cortex/hooks/session_start.py +13 -0
  24. cortex_loop-0.1.0a1/cortex/hooks/stop.py +13 -0
  25. cortex_loop-0.1.0a1/cortex/invariants.py +258 -0
  26. cortex_loop-0.1.0a1/cortex/packs.py +118 -0
  27. cortex_loop-0.1.0a1/cortex/repomap.py +6 -0
  28. cortex_loop-0.1.0a1/cortex/requirements.py +497 -0
  29. cortex_loop-0.1.0a1/cortex/retry.py +312 -0
  30. cortex_loop-0.1.0a1/cortex/stop_contract.py +217 -0
  31. cortex_loop-0.1.0a1/cortex/stop_payload.py +122 -0
  32. cortex_loop-0.1.0a1/cortex/stop_policy.py +100 -0
  33. cortex_loop-0.1.0a1/cortex/stop_runtime.py +400 -0
  34. cortex_loop-0.1.0a1/cortex/stop_signals.py +75 -0
  35. cortex_loop-0.1.0a1/cortex/store.py +793 -0
  36. cortex_loop-0.1.0a1/cortex/templates/__init__.py +10 -0
  37. cortex_loop-0.1.0a1/cortex/utils.py +58 -0
  38. cortex_loop-0.1.0a1/cortex_loop.egg-info/PKG-INFO +121 -0
  39. cortex_loop-0.1.0a1/cortex_loop.egg-info/SOURCES.txt +101 -0
  40. cortex_loop-0.1.0a1/cortex_loop.egg-info/dependency_links.txt +1 -0
  41. cortex_loop-0.1.0a1/cortex_loop.egg-info/entry_points.txt +3 -0
  42. cortex_loop-0.1.0a1/cortex_loop.egg-info/requires.txt +13 -0
  43. cortex_loop-0.1.0a1/cortex_loop.egg-info/top_level.txt +3 -0
  44. cortex_loop-0.1.0a1/cortex_ops_cli/__init__.py +3 -0
  45. cortex_loop-0.1.0a1/cortex_ops_cli/_adapter_validation.py +119 -0
  46. cortex_loop-0.1.0a1/cortex_ops_cli/_check_report.py +454 -0
  47. cortex_loop-0.1.0a1/cortex_ops_cli/_check_report_output.py +270 -0
  48. cortex_loop-0.1.0a1/cortex_ops_cli/_openai_bridge_probe.py +241 -0
  49. cortex_loop-0.1.0a1/cortex_ops_cli/_openai_bridge_protocol.py +469 -0
  50. cortex_loop-0.1.0a1/cortex_ops_cli/_runtime_profile_templates.py +341 -0
  51. cortex_loop-0.1.0a1/cortex_ops_cli/_runtime_profiles.py +445 -0
  52. cortex_loop-0.1.0a1/cortex_ops_cli/gemini_hooks.py +301 -0
  53. cortex_loop-0.1.0a1/cortex_ops_cli/main.py +911 -0
  54. cortex_loop-0.1.0a1/cortex_ops_cli/openai_app_server_bridge.py +375 -0
  55. cortex_loop-0.1.0a1/cortex_repomap/__init__.py +1 -0
  56. cortex_loop-0.1.0a1/cortex_repomap/engine.py +1201 -0
  57. cortex_loop-0.1.0a1/pyproject.toml +58 -0
  58. cortex_loop-0.1.0a1/setup.cfg +4 -0
  59. cortex_loop-0.1.0a1/tests/test_adapter_validation_contract.py +127 -0
  60. cortex_loop-0.1.0a1/tests/test_adapter_validation_fixture_hygiene.py +44 -0
  61. cortex_loop-0.1.0a1/tests/test_adapter_validation_fixture_provenance.py +37 -0
  62. cortex_loop-0.1.0a1/tests/test_adapters.py +383 -0
  63. cortex_loop-0.1.0a1/tests/test_blocklist.py +320 -0
  64. cortex_loop-0.1.0a1/tests/test_challenges.py +145 -0
  65. cortex_loop-0.1.0a1/tests/test_cli.py +1768 -0
  66. cortex_loop-0.1.0a1/tests/test_context_budget_regression.py +185 -0
  67. cortex_loop-0.1.0a1/tests/test_core.py +2036 -0
  68. cortex_loop-0.1.0a1/tests/test_core_helpers.py +57 -0
  69. cortex_loop-0.1.0a1/tests/test_docs_hygiene.py +443 -0
  70. cortex_loop-0.1.0a1/tests/test_executive.py +47 -0
  71. cortex_loop-0.1.0a1/tests/test_executive_layer2.py +200 -0
  72. cortex_loop-0.1.0a1/tests/test_foundation.py +64 -0
  73. cortex_loop-0.1.0a1/tests/test_gemini_fixture_hygiene.py +44 -0
  74. cortex_loop-0.1.0a1/tests/test_gemini_fixture_provenance.py +41 -0
  75. cortex_loop-0.1.0a1/tests/test_gemini_hooks_bridge.py +570 -0
  76. cortex_loop-0.1.0a1/tests/test_genome.py +358 -0
  77. cortex_loop-0.1.0a1/tests/test_graveyard.py +283 -0
  78. cortex_loop-0.1.0a1/tests/test_hooks.py +481 -0
  79. cortex_loop-0.1.0a1/tests/test_install_surfaces.py +157 -0
  80. cortex_loop-0.1.0a1/tests/test_integration.py +139 -0
  81. cortex_loop-0.1.0a1/tests/test_invariants.py +326 -0
  82. cortex_loop-0.1.0a1/tests/test_kernel_density_targets.py +59 -0
  83. cortex_loop-0.1.0a1/tests/test_kernel_runtime_agnostic_guard.py +30 -0
  84. cortex_loop-0.1.0a1/tests/test_mini_openai_ab.py +327 -0
  85. cortex_loop-0.1.0a1/tests/test_openai_app_server_bridge.py +850 -0
  86. cortex_loop-0.1.0a1/tests/test_packs.py +127 -0
  87. cortex_loop-0.1.0a1/tests/test_preflight_openai_probe_v121.py +161 -0
  88. cortex_loop-0.1.0a1/tests/test_repo_hygiene.py +98 -0
  89. cortex_loop-0.1.0a1/tests/test_repo_hygiene_sessions.py +174 -0
  90. cortex_loop-0.1.0a1/tests/test_repomap.py +990 -0
  91. cortex_loop-0.1.0a1/tests/test_requirements.py +304 -0
  92. cortex_loop-0.1.0a1/tests/test_retry.py +488 -0
  93. cortex_loop-0.1.0a1/tests/test_runtime_latency_regression.py +320 -0
  94. cortex_loop-0.1.0a1/tests/test_shims.py +63 -0
  95. cortex_loop-0.1.0a1/tests/test_stop_contract.py +271 -0
  96. cortex_loop-0.1.0a1/tests/test_stop_payload.py +75 -0
  97. cortex_loop-0.1.0a1/tests/test_stop_policy.py +125 -0
  98. cortex_loop-0.1.0a1/tests/test_stop_runtime.py +145 -0
  99. cortex_loop-0.1.0a1/tests/test_stop_signals.py +44 -0
  100. cortex_loop-0.1.0a1/tests/test_store.py +759 -0
  101. cortex_loop-0.1.0a1/tests/test_structure_boundaries.py +110 -0
  102. cortex_loop-0.1.0a1/tests/test_study_scripts.py +704 -0
  103. cortex_loop-0.1.0a1/tests/test_workflow_contract.py +168 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cortex contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ prune _governance_archive
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: cortex-loop
3
+ Version: 0.1.0a1
4
+ Summary: Runtime-agnostic enforcement layer for AI coding agents
5
+ Author: Cortex contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/cortex-loop/cortex-loop
8
+ Project-URL: Repository, https://github.com/cortex-loop/cortex-loop
9
+ Project-URL: Issues, https://github.com/cortex-loop/cortex-loop/issues
10
+ Keywords: ai,coding-agent,enforcement,verification,runtime-agnostic
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Intended Audience :: Developers
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: repomap
21
+ Requires-Dist: grep-ast==0.9.0; extra == "repomap"
22
+ Requires-Dist: numpy>=2.0; extra == "repomap"
23
+ Requires-Dist: networkx==3.4.2; extra == "repomap"
24
+ Requires-Dist: tree-sitter-language-pack==0.13.0; extra == "repomap"
25
+ Requires-Dist: tree-sitter==0.25.2; extra == "repomap"
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.2; extra == "dev"
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.3; extra == "dev"
30
+ Requires-Dist: twine>=6.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ ![cortex-loop banner](docs/assets/cortex-loop-banner.webp)
34
+
35
+ # Cortex
36
+
37
+ Cortex is a runtime-agnostic enforcement layer for AI coding agents. It verifies completion claims through a deterministic stop path instead of trusting summaries, so "done" only lands when the evidence is real. Coding agents can sound finished before reality has caught up; Cortex turns that completion claim into something the system can prove or deny.
38
+
39
+ With Cortex, the path of least resistance becomes the path of best practice: the easiest way to finish is to do the work, satisfy the evidence boundary, and let the stop path close honestly.
40
+
41
+ The stop path is the product. Cortex is not a planning framework, generic memory layer, or workflow wrapper. It exists to make completion claims legible, checkable, and hard to bluff.
42
+
43
+ ## Runtime Status
44
+
45
+ | Runtime | Install profile | Status | Notes |
46
+ | --- | --- | --- | --- |
47
+ | Claude Code | `cortex runtime install --profile claude` | Shipped | Strongest current runtime. The truthful boundary is live-proven; remaining caveats are minor. |
48
+ | Gemini CLI | `cortex runtime install --profile gemini` | Shipped with watchlist | The truthful boundary is live-proven. One operational watchlist remains: blocked malformed stops can leave Gemini CLI resident until operator termination. |
49
+ | OpenAI Codex App Server | `cortex runtime install --profile openai` | Experimental | Several critical paths are proven on the latest stable surface, but positive strict close is still not dependable enough for broader support. |
50
+
51
+ Status labels in this table:
52
+ - `Shipped`: live-proven on the shared harness; remaining caveats are non-critical.
53
+ - `Shipped with watchlist`: live-proven, but one named runtime quirk still creates real operator risk.
54
+ - `Experimental`: several important paths are proven, but one boundary-critical guarantee remains unproven or unstable.
55
+
56
+ Detailed runtime release evidence lives in [docs/ADAPTER_VALIDATION.md](docs/ADAPTER_VALIDATION.md) plus committed provenance under [tests/fixtures/adapter_validation/claude/PROVENANCE.json](tests/fixtures/adapter_validation/claude/PROVENANCE.json), [tests/fixtures/adapter_validation/gemini/PROVENANCE.json](tests/fixtures/adapter_validation/gemini/PROVENANCE.json), and [tests/fixtures/adapter_validation/openai/PROVENANCE.json](tests/fixtures/adapter_validation/openai/PROVENANCE.json).
57
+
58
+ ## Quickstart
59
+
60
+ As of 2026-03-07, `cortex-loop` is not yet published on PyPI. Until live
61
+ package publication is proven, the truthful first-run path is from a repo
62
+ checkout:
63
+
64
+ ```bash
65
+ python3 -m venv .venv
66
+ . .venv/bin/activate
67
+ python -m pip install . pytest
68
+ cortex init
69
+ cortex runtime install --profile claude
70
+ ```
71
+
72
+ Before `cortex check`, choose the baseline that matches the repository:
73
+
74
+ - trusted local repo: switch `cortex.toml` to the trusted-host baseline in
75
+ [docs/SECURE_DEFAULTS.md](docs/SECURE_DEFAULTS.md)
76
+ - untrusted or container baseline: keep the defaults and ensure Docker is on
77
+ `PATH`
78
+
79
+ Then run:
80
+
81
+ ```bash
82
+ cortex check
83
+ ```
84
+
85
+ The staged public install posture, update and uninstall commands, and the PyPI
86
+ publication gate live in [docs/INSTALL.md](docs/INSTALL.md).
87
+
88
+ For a first evaluation, keep `claude`. Use `gemini` when you want the shipped
89
+ watchlist surface. Use `openai` only when you are explicitly evaluating the
90
+ experimental surface.
91
+
92
+ ## What Cortex Enforces
93
+
94
+ - Stop-claim normalization and strict payload handling ([tests/test_stop_contract.py](tests/test_stop_contract.py))
95
+ - Challenge coverage across active categories ([tests/test_challenges.py](tests/test_challenges.py))
96
+ - Invariant execution outside model output ([tests/test_invariants.py](tests/test_invariants.py))
97
+ - Deterministic verdict policy ([tests/test_stop_policy.py](tests/test_stop_policy.py))
98
+ - Session-scoped failure memory and retry controls ([tests/test_retry.py](tests/test_retry.py), [tests/test_graveyard.py](tests/test_graveyard.py))
99
+
100
+ ## What Cortex Is Not
101
+
102
+ - not a generic multi-agent framework
103
+ - not prompt theater that trusts summaries
104
+ - not a workflow layer whose main job is planning or project management
105
+
106
+ ## Read Next
107
+
108
+ 1. [START_HERE.md](START_HERE.md)
109
+ 2. [ARCHITECTURE.md](ARCHITECTURE.md)
110
+ 3. [MISSION.md](MISSION.md)
111
+ 4. [docs/README.md](docs/README.md)
112
+ 5. [CONTRIBUTING.md](CONTRIBUTING.md)
113
+
114
+ ## Repository Boundaries
115
+
116
+ - Product-critical code lives in `cortex/`, `cortex_ops_cli/`, `cortex_repomap/`, runtime profile templates, and core tests.
117
+ - Extension manifests live in `packs/`; this repo's pack validation suites live in `tests/packs/`.
118
+ - `eval/` is internal and historical validation only. It is not a downstream product interface or product design authority.
119
+ - `docs/archive/` and `_governance_archive/` are historical context, not active authority.
120
+
121
+ v0.1 remains kernel-first: one hardened kernel, one reference pack, and runtime adapters with capture-backed tests.
@@ -0,0 +1,89 @@
1
+ ![cortex-loop banner](docs/assets/cortex-loop-banner.webp)
2
+
3
+ # Cortex
4
+
5
+ Cortex is a runtime-agnostic enforcement layer for AI coding agents. It verifies completion claims through a deterministic stop path instead of trusting summaries, so "done" only lands when the evidence is real. Coding agents can sound finished before reality has caught up; Cortex turns that completion claim into something the system can prove or deny.
6
+
7
+ With Cortex, the path of least resistance becomes the path of best practice: the easiest way to finish is to do the work, satisfy the evidence boundary, and let the stop path close honestly.
8
+
9
+ The stop path is the product. Cortex is not a planning framework, generic memory layer, or workflow wrapper. It exists to make completion claims legible, checkable, and hard to bluff.
10
+
11
+ ## Runtime Status
12
+
13
+ | Runtime | Install profile | Status | Notes |
14
+ | --- | --- | --- | --- |
15
+ | Claude Code | `cortex runtime install --profile claude` | Shipped | Strongest current runtime. The truthful boundary is live-proven; remaining caveats are minor. |
16
+ | Gemini CLI | `cortex runtime install --profile gemini` | Shipped with watchlist | The truthful boundary is live-proven. One operational watchlist remains: blocked malformed stops can leave Gemini CLI resident until operator termination. |
17
+ | OpenAI Codex App Server | `cortex runtime install --profile openai` | Experimental | Several critical paths are proven on the latest stable surface, but positive strict close is still not dependable enough for broader support. |
18
+
19
+ Status labels in this table:
20
+ - `Shipped`: live-proven on the shared harness; remaining caveats are non-critical.
21
+ - `Shipped with watchlist`: live-proven, but one named runtime quirk still creates real operator risk.
22
+ - `Experimental`: several important paths are proven, but one boundary-critical guarantee remains unproven or unstable.
23
+
24
+ Detailed runtime release evidence lives in [docs/ADAPTER_VALIDATION.md](docs/ADAPTER_VALIDATION.md) plus committed provenance under [tests/fixtures/adapter_validation/claude/PROVENANCE.json](tests/fixtures/adapter_validation/claude/PROVENANCE.json), [tests/fixtures/adapter_validation/gemini/PROVENANCE.json](tests/fixtures/adapter_validation/gemini/PROVENANCE.json), and [tests/fixtures/adapter_validation/openai/PROVENANCE.json](tests/fixtures/adapter_validation/openai/PROVENANCE.json).
25
+
26
+ ## Quickstart
27
+
28
+ As of 2026-03-07, `cortex-loop` is not yet published on PyPI. Until live
29
+ package publication is proven, the truthful first-run path is from a repo
30
+ checkout:
31
+
32
+ ```bash
33
+ python3 -m venv .venv
34
+ . .venv/bin/activate
35
+ python -m pip install . pytest
36
+ cortex init
37
+ cortex runtime install --profile claude
38
+ ```
39
+
40
+ Before `cortex check`, choose the baseline that matches the repository:
41
+
42
+ - trusted local repo: switch `cortex.toml` to the trusted-host baseline in
43
+ [docs/SECURE_DEFAULTS.md](docs/SECURE_DEFAULTS.md)
44
+ - untrusted or container baseline: keep the defaults and ensure Docker is on
45
+ `PATH`
46
+
47
+ Then run:
48
+
49
+ ```bash
50
+ cortex check
51
+ ```
52
+
53
+ The staged public install posture, update and uninstall commands, and the PyPI
54
+ publication gate live in [docs/INSTALL.md](docs/INSTALL.md).
55
+
56
+ For a first evaluation, keep `claude`. Use `gemini` when you want the shipped
57
+ watchlist surface. Use `openai` only when you are explicitly evaluating the
58
+ experimental surface.
59
+
60
+ ## What Cortex Enforces
61
+
62
+ - Stop-claim normalization and strict payload handling ([tests/test_stop_contract.py](tests/test_stop_contract.py))
63
+ - Challenge coverage across active categories ([tests/test_challenges.py](tests/test_challenges.py))
64
+ - Invariant execution outside model output ([tests/test_invariants.py](tests/test_invariants.py))
65
+ - Deterministic verdict policy ([tests/test_stop_policy.py](tests/test_stop_policy.py))
66
+ - Session-scoped failure memory and retry controls ([tests/test_retry.py](tests/test_retry.py), [tests/test_graveyard.py](tests/test_graveyard.py))
67
+
68
+ ## What Cortex Is Not
69
+
70
+ - not a generic multi-agent framework
71
+ - not prompt theater that trusts summaries
72
+ - not a workflow layer whose main job is planning or project management
73
+
74
+ ## Read Next
75
+
76
+ 1. [START_HERE.md](START_HERE.md)
77
+ 2. [ARCHITECTURE.md](ARCHITECTURE.md)
78
+ 3. [MISSION.md](MISSION.md)
79
+ 4. [docs/README.md](docs/README.md)
80
+ 5. [CONTRIBUTING.md](CONTRIBUTING.md)
81
+
82
+ ## Repository Boundaries
83
+
84
+ - Product-critical code lives in `cortex/`, `cortex_ops_cli/`, `cortex_repomap/`, runtime profile templates, and core tests.
85
+ - Extension manifests live in `packs/`; this repo's pack validation suites live in `tests/packs/`.
86
+ - `eval/` is internal and historical validation only. It is not a downstream product interface or product design authority.
87
+ - `docs/archive/` and `_governance_archive/` are historical context, not active authority.
88
+
89
+ v0.1 remains kernel-first: one hardened kernel, one reference pack, and runtime adapters with capture-backed tests.
@@ -0,0 +1,7 @@
1
+ """Cortex Loop package."""
2
+
3
+ from .core import CortexKernel
4
+ from .hooks import marker
5
+
6
+ __all__ = ["CortexKernel", "marker"]
7
+ __version__ = "0.1.0a1"
@@ -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
+ )
@@ -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")