correctover-test 0.3.0__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.
@@ -0,0 +1,2 @@
1
+ """Correctover Self-Healing Test Agent v0.1.0"""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,222 @@
1
+ """
2
+ correctover-test CLI — self-healing test agent entry point.
3
+
4
+ Usage:
5
+ correctover-test --quick # 5-second smoke test
6
+ correctover-test --framework crewai # Single framework
7
+ correctover-test --all --html # Full audit, HTML report
8
+ correctover-test --quick --cloud # Upload results to Correctover Cloud
9
+ """
10
+
11
+ import json
12
+ import sys
13
+ import urllib.request
14
+ from typing import Optional
15
+
16
+ import click
17
+
18
+ from .core.engine import TestEngine
19
+ from .core.reporter import Reporter
20
+ from .core.license import get_validator, LicenseExceededError
21
+ from .core.adapters import CrewAIAdapter, SmolagentsAdapter, LlamaIndexAdapter
22
+ from .core.scenarios import MCPSelfHealScenario, EnvProtectionScenario, ContractBreakScenario
23
+
24
+
25
+ ADAPTER_REGISTRY = {
26
+ "crewai": CrewAIAdapter,
27
+ "smolagents": SmolagentsAdapter,
28
+ "llamaindex": LlamaIndexAdapter,
29
+ }
30
+
31
+ SCENARIO_REGISTRY = {
32
+ "mcp_self_heal": MCPSelfHealScenario,
33
+ "env_protection": EnvProtectionScenario,
34
+ "contract_break": ContractBreakScenario,
35
+ }
36
+
37
+ VERSION = "0.2.0"
38
+
39
+
40
+ @click.command()
41
+ @click.option(
42
+ "--framework", "-f",
43
+ default="all",
44
+ help="Target framework: crewai, smolagents, llamaindex, or all",
45
+ )
46
+ @click.option(
47
+ "--scenario", "-s",
48
+ default="all",
49
+ help="Test scenario: mcp_self_heal, env_protection, contract_break, or all",
50
+ )
51
+ @click.option(
52
+ "--output", "-o",
53
+ default=None,
54
+ help="Output file path (.json / .md / .html based on extension)",
55
+ )
56
+ @click.option(
57
+ "--quick", "-q",
58
+ is_flag=True,
59
+ help="5-second smoke test: env_protection on all frameworks",
60
+ )
61
+ @click.option(
62
+ "--all", "run_all",
63
+ is_flag=True,
64
+ help="Full audit: all scenarios on all frameworks",
65
+ )
66
+ @click.option(
67
+ "--format", "output_format",
68
+ type=click.Choice(["terminal", "json", "markdown", "html"]),
69
+ default="terminal",
70
+ help="Output format (default: terminal)",
71
+ )
72
+ @click.option(
73
+ "--cloud",
74
+ default=None,
75
+ help="Correctover Cloud endpoint URL (e.g. https://cloud.correctover.com/api/v1/audit)",
76
+ )
77
+ @click.option(
78
+ "--no-cta",
79
+ is_flag=True,
80
+ help="Suppress the 'Fix this' CTA in output",
81
+ )
82
+ @click.version_option(version=VERSION, prog_name="correctover-test")
83
+ def main(
84
+ framework: str,
85
+ scenario: str,
86
+ output: Optional[str],
87
+ quick: bool,
88
+ run_all: bool,
89
+ output_format: str,
90
+ cloud: Optional[str],
91
+ no_cta: bool,
92
+ ):
93
+ """🔒 Correctover Agent Security Audit
94
+
95
+ Tests your AI agents for guardrail gaps, command injection,
96
+ environment variable leaks, and contract violations.
97
+
98
+ \b
99
+ Quick start:
100
+ correctover-test --quick # 5-second audit
101
+ correctover-test --all --html # Full report
102
+
103
+ \b
104
+ Correctover Cloud:
105
+ correctover-test --quick --cloud https://cloud.correctover.com/api/v1/audit
106
+ """
107
+
108
+ # Resolve frameworks
109
+ if quick:
110
+ framework = "all"
111
+ scenario = "env_protection"
112
+ elif run_all:
113
+ framework = "all"
114
+ scenario = "all"
115
+
116
+ if framework == "all":
117
+ adapters = [cls() for cls in ADAPTER_REGISTRY.values()]
118
+ else:
119
+ fws = [f.strip() for f in framework.split(",")]
120
+ adapters = []
121
+ for fw in fws:
122
+ if fw not in ADAPTER_REGISTRY:
123
+ click.echo(
124
+ f"❌ Unknown framework: {fw}. Available: {list(ADAPTER_REGISTRY.keys())}",
125
+ err=True,
126
+ )
127
+ sys.exit(1)
128
+ adapters.append(ADAPTER_REGISTRY[fw]())
129
+
130
+ # Resolve scenarios
131
+ if scenario == "all":
132
+ scenarios = [cls() for cls in SCENARIO_REGISTRY.values()]
133
+ else:
134
+ scs = [s.strip() for s in scenario.split(",")]
135
+ scenarios = []
136
+ for sc in scs:
137
+ if sc not in SCENARIO_REGISTRY:
138
+ click.echo(
139
+ f"❌ Unknown scenario: {sc}. Available: {list(SCENARIO_REGISTRY.keys())}",
140
+ err=True,
141
+ )
142
+ sys.exit(1)
143
+ scenarios.append(SCENARIO_REGISTRY[sc]())
144
+
145
+ # License check — download-to-pay enforcement
146
+ try:
147
+ from .core.license import LicenseValidator
148
+ license_key = LicenseValidator.get_license_from_env()
149
+ validator = get_validator()
150
+ if license_key:
151
+ validator.set_license_key(license_key)
152
+ status = validator.record_call()
153
+ if status["tier"] == "free":
154
+ click.echo(
155
+ f"📊 Free tier: {status['calls_remaining']} audits remaining today "
156
+ f"({status['calls_today']}/{status['limit']} used)",
157
+ err=True,
158
+ )
159
+ except LicenseExceededError as e:
160
+ click.echo(str(e), err=True)
161
+ sys.exit(1)
162
+
163
+ # Run
164
+ click.echo(
165
+ f"🔍 Auditing {len(adapters)} framework(s) × {len(scenarios)} scenario(s)...",
166
+ err=True,
167
+ )
168
+ engine = TestEngine(adapters)
169
+ results = engine.run_all(scenarios=scenarios)
170
+
171
+ # Upload to cloud
172
+ if cloud:
173
+ try:
174
+ payload = json.dumps({
175
+ "source": "correctover-test",
176
+ "version": VERSION,
177
+ "results": [r.to_dict() for r in results],
178
+ }).encode("utf-8")
179
+ req = urllib.request.Request(
180
+ cloud,
181
+ data=payload,
182
+ headers={"Content-Type": "application/json"},
183
+ method="POST",
184
+ )
185
+ with urllib.request.urlopen(req, timeout=10) as resp:
186
+ cloud_resp = json.loads(resp.read())
187
+ click.echo(
188
+ f"☁️ Uploaded to Correctover Cloud: {cloud_resp.get('dashboard_url', cloud)}",
189
+ err=True,
190
+ )
191
+ except Exception as e:
192
+ click.echo(f"⚠️ Cloud upload failed: {e}", err=True)
193
+
194
+ # Output
195
+ if output:
196
+ if output.endswith(".md"):
197
+ content = Reporter.to_markdown(results)
198
+ elif output.endswith(".html"):
199
+ content = Reporter.to_html(results)
200
+ else:
201
+ content = Reporter.to_json(results)
202
+ with open(output, "w") as f:
203
+ f.write(content)
204
+ click.echo(f"📄 Report written to {output}", err=True)
205
+ else:
206
+ if output_format == "json":
207
+ click.echo(Reporter.to_json(results))
208
+ elif output_format == "markdown":
209
+ click.echo(Reporter.to_markdown(results))
210
+ elif output_format == "html":
211
+ click.echo(Reporter.to_html(results))
212
+ else:
213
+ Reporter.to_terminal(results, show_cta=not no_cta)
214
+
215
+ # Exit code
216
+ total = len(results)
217
+ failed = sum(1 for r in results if r.verdict.value == "FAIL")
218
+ sys.exit(0 if failed == 0 else 1)
219
+
220
+
221
+ if __name__ == "__main__":
222
+ main()
File without changes
@@ -0,0 +1,6 @@
1
+ from .base import FrameworkAdapter
2
+ from .crewai_adapter import CrewAIAdapter
3
+ from .smolagents_adapter import SmolagentsAdapter
4
+ from .llamaindex_adapter import LlamaIndexAdapter
5
+
6
+ __all__ = ["FrameworkAdapter", "CrewAIAdapter", "SmolagentsAdapter", "LlamaIndexAdapter"]
@@ -0,0 +1,206 @@
1
+ """
2
+ FrameworkAdapter ABC and shared data models.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional
9
+ import time
10
+
11
+
12
+ class FaultType(str, Enum):
13
+ COMMAND_INJECTION = "command_injection"
14
+ ENV_EXPOSURE = "env_exposure"
15
+ CONTRACT_BREAK = "contract_break"
16
+
17
+
18
+ class Verdict(str, Enum):
19
+ PASS = "PASS"
20
+ FAIL = "FAIL"
21
+ SKIP = "SKIP"
22
+
23
+
24
+ @dataclass
25
+ class MetricSnapshot:
26
+ """6-dimensional verification metrics per test run."""
27
+ dimension: str # integrity | authorization | auditability | latency | coverage | recovery
28
+ value: float
29
+ threshold: float # pass threshold
30
+ passed: bool
31
+ detail: str = ""
32
+
33
+ def to_dict(self) -> Dict[str, Any]:
34
+ return {
35
+ "dimension": self.dimension,
36
+ "value": self.value,
37
+ "threshold": self.threshold,
38
+ "passed": self.passed,
39
+ "detail": self.detail,
40
+ }
41
+
42
+
43
+ @dataclass
44
+ class TestResult:
45
+ """Single scenario run result."""
46
+ framework: str
47
+ scenario: str
48
+ verdict: Verdict
49
+ metrics: List[MetricSnapshot] = field(default_factory=list)
50
+ decisions_count: int = 0
51
+ envelopes_count: int = 0
52
+ integrity_verified: bool = False
53
+ duration_ms: float = 0.0
54
+ errors: List[str] = field(default_factory=list)
55
+ details: Dict[str, Any] = field(default_factory=dict)
56
+
57
+ @property
58
+ def self_heal_rate(self) -> float:
59
+ """Fraction of metrics that passed."""
60
+ if not self.metrics:
61
+ return 0.0
62
+ return sum(1 for m in self.metrics if m.passed) / len(self.metrics)
63
+
64
+ def to_dict(self) -> Dict[str, Any]:
65
+ return {
66
+ "framework": self.framework,
67
+ "scenario": self.scenario,
68
+ "verdict": self.verdict.value,
69
+ "self_heal_rate": round(self.self_heal_rate, 4),
70
+ "metrics": [m.to_dict() for m in self.metrics],
71
+ "decisions_count": self.decisions_count,
72
+ "envelopes_count": self.envelopes_count,
73
+ "integrity_verified": self.integrity_verified,
74
+ "duration_ms": round(self.duration_ms, 2),
75
+ "errors": self.errors,
76
+ "details": self.details,
77
+ }
78
+
79
+
80
+ class FrameworkAdapter(ABC):
81
+ """Abstract adapter for hooking correctover into an Agent framework."""
82
+
83
+ name: str
84
+
85
+ @abstractmethod
86
+ def create_agent(self, tools: List[Any], config: Optional[Dict] = None) -> Any:
87
+ """Create a native agent instance for this framework."""
88
+ ...
89
+
90
+ @abstractmethod
91
+ def register_guardrail(self, agent: Any, provider: Any) -> Any:
92
+ """
93
+ Inject a correctover GuardrailProvider into the agent.
94
+ Returns the modified agent or a wrapper.
95
+ """
96
+ ...
97
+
98
+ @abstractmethod
99
+ def run_task(self, agent: Any, task: str) -> Dict[str, Any]:
100
+ """
101
+ Execute a task through the agent.
102
+ Returns {"output": str, "duration_ms": float, "error": Optional[str]}.
103
+ """
104
+ ...
105
+
106
+ @abstractmethod
107
+ def detect_missing_guardrail(self, agent: Any) -> List[Dict]:
108
+ """
109
+ Scan the agent for missing guardrail hooks.
110
+ Returns list of findings, empty list = no issues.
111
+ """
112
+ ...
113
+
114
+ @abstractmethod
115
+ def inject_fault(self, agent: Any, fault_type: FaultType) -> Any:
116
+ """
117
+ Inject a fault into the agent's tool call pipeline.
118
+ Returns the modified agent/context.
119
+ """
120
+ ...
121
+
122
+
123
+ class TestScenario(ABC):
124
+ """Abstract test scenario that runs against an adapter+agent pair."""
125
+
126
+ name: str
127
+ description: str
128
+ fault_type: FaultType
129
+
130
+ @abstractmethod
131
+ def setup(self, adapter: FrameworkAdapter, agent: Any) -> Any:
132
+ """Prepare the agent for this scenario. Return modified agent/context."""
133
+ ...
134
+
135
+ @abstractmethod
136
+ def execute(self, adapter: FrameworkAdapter, agent: Any) -> Dict[str, Any]:
137
+ """Run the scenario. Return raw results dict."""
138
+ ...
139
+
140
+ @abstractmethod
141
+ def verify(
142
+ self,
143
+ adapter: FrameworkAdapter,
144
+ agent: Any,
145
+ raw_result: Dict[str, Any],
146
+ trail: Any, # correctover.AuditTrail
147
+ ) -> List[MetricSnapshot]:
148
+ """Verify the 6-dimension metrics from AuditTrail."""
149
+ ...
150
+
151
+ def run(
152
+ self,
153
+ adapter: FrameworkAdapter,
154
+ agent: Any,
155
+ trail: Any,
156
+ ) -> TestResult:
157
+ """Orchestrate full scenario lifecycle."""
158
+ t0 = time.time()
159
+ errors = []
160
+
161
+ try:
162
+ agent = self.setup(adapter, agent)
163
+ except Exception as e:
164
+ errors.append(f"setup: {e}")
165
+ return TestResult(
166
+ framework=adapter.name,
167
+ scenario=self.name,
168
+ verdict=Verdict.FAIL,
169
+ errors=errors,
170
+ details={"phase": "setup"},
171
+ )
172
+
173
+ try:
174
+ raw = self.execute(adapter, agent)
175
+ except Exception as e:
176
+ errors.append(f"execute: {e}")
177
+ return TestResult(
178
+ framework=adapter.name,
179
+ scenario=self.name,
180
+ verdict=Verdict.FAIL,
181
+ errors=errors,
182
+ details={"phase": "execute"},
183
+ )
184
+
185
+ try:
186
+ metrics = self.verify(adapter, agent, raw, trail)
187
+ except Exception as e:
188
+ errors.append(f"verify: {e}")
189
+ metrics = []
190
+
191
+ duration_ms = (time.time() - t0) * 1000
192
+ all_pass = all(m.passed for m in metrics) if metrics else False
193
+ integrity_ok = trail.verify_all() if trail else False
194
+
195
+ return TestResult(
196
+ framework=adapter.name,
197
+ scenario=self.name,
198
+ verdict=Verdict.PASS if all_pass and not errors else Verdict.FAIL,
199
+ metrics=metrics,
200
+ decisions_count=trail.decision_count if trail else 0,
201
+ envelopes_count=trail.envelope_count if trail else 0,
202
+ integrity_verified=integrity_ok,
203
+ duration_ms=duration_ms,
204
+ errors=errors,
205
+ details={"raw_output": raw.get("output", "")[:500]},
206
+ )
@@ -0,0 +1,204 @@
1
+ """
2
+ CrewAI Framework Adapter.
3
+
4
+ Hooks into CrewAI's global before_tool_call / after_tool_call decorator system
5
+ and the agent.guardrail attribute for guardrail detection.
6
+
7
+ For testing without an LLM, run_task simulates tool call guardrail checks
8
+ directly against the registered provider, bypassing actual LLM calls.
9
+ """
10
+
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from .base import FrameworkAdapter, FaultType
14
+
15
+
16
+ class CrewAIAdapter(FrameworkAdapter):
17
+ name = "crewai"
18
+
19
+ def create_agent(self, tools: List[Any], config: Optional[Dict] = None) -> Any:
20
+ from crewai import Agent
21
+
22
+ return Agent(
23
+ role=config.get("role", "test-agent") if config else "test-agent",
24
+ goal=config.get("goal", "Execute tools safely with guardrail protection") if config else "Execute tools safely with guardrail protection",
25
+ backstory=config.get("backstory", "A test agent for correctover self-healing validation") if config else "A test agent for correctover self-healing validation",
26
+ tools=tools,
27
+ verbose=False,
28
+ allow_delegation=False,
29
+ )
30
+
31
+ def register_guardrail(self, agent: Any, provider: Any) -> Any:
32
+ """
33
+ Inject guardrail via CrewAI's global before_tool_call hook system.
34
+ Wraps the provider into a callable that matches CrewAI's hook signature.
35
+ """
36
+ from crewai.hooks.tool_hooks import register_before_tool_call_hook
37
+ from correctover import GuardrailContext, ToolCallContext, AuditTrail
38
+
39
+ trail = AuditTrail()
40
+ ctx = GuardrailContext(provider=provider, trail=trail)
41
+
42
+ def before_hook(*, tool=None, tool_input=None, agent_role=None, agent=None, **kwargs):
43
+ tool_name = getattr(tool, "name", str(tool))
44
+ tool_args = tool_input if isinstance(tool_input, dict) else {}
45
+ agent_id = agent_role or "unknown"
46
+
47
+ tc = ToolCallContext(
48
+ tool_name=tool_name,
49
+ tool_args=tool_args,
50
+ agent_id=agent_id,
51
+ metadata={"framework": "crewai"},
52
+ )
53
+ decision = ctx.authorize(tc)
54
+ if not decision.authorized:
55
+ raise PermissionError(
56
+ f"Tool '{tool_name}' denied by correctover guardrail"
57
+ )
58
+ return tool_input
59
+
60
+ register_before_tool_call_hook(before_hook)
61
+
62
+ agent._correctover_trail = trail
63
+ agent._correctover_context = ctx
64
+
65
+ return agent
66
+
67
+ def run_task(self, agent: Any, task: str) -> Dict[str, Any]:
68
+ """
69
+ Simulate a tool call against the guardrail directly — no LLM required.
70
+ Extracts tool name from the task string and tests the guardrail provider.
71
+ """
72
+ import time
73
+ from correctover import ToolCallContext
74
+
75
+ t0 = time.time()
76
+ error = None
77
+ output = ""
78
+
79
+ trail = getattr(agent, "_correctover_trail", None)
80
+ ctx = getattr(agent, "_correctover_context", None)
81
+
82
+ # Parse tool name from task: "Use X to do Y" -> X
83
+ tool_name = "unknown_tool"
84
+ task_lower = task.lower()
85
+ for word in task.split():
86
+ if word in ["dangerous_exec", "read_env", "unreliable_output"]:
87
+ tool_name = word
88
+ break
89
+ # Fallback: find tool from agent.tools
90
+ if tool_name == "unknown_tool" and hasattr(agent, "tools"):
91
+ for t in agent.tools:
92
+ tname = getattr(t, "name", str(t))
93
+ if tname.lower() in task_lower:
94
+ tool_name = tname
95
+ break
96
+
97
+ # Extract command arg from task if relevant
98
+ tool_args = {}
99
+ if "rm -rf" in task:
100
+ tool_args["command"] = "rm -rf /"
101
+ elif "TEST_API_KEY" in task:
102
+ tool_args["key"] = "TEST_API_KEY"
103
+ else:
104
+ tool_args["query"] = task
105
+
106
+ # Direct guardrail check
107
+ if ctx and ctx.provider:
108
+ tc = ToolCallContext(
109
+ tool_name=tool_name,
110
+ tool_args=tool_args,
111
+ agent_id=getattr(agent, "role", "unknown"),
112
+ metadata={"framework": "crewai", "mode": "direct-check"},
113
+ )
114
+ try:
115
+ decision = ctx.authorize(tc)
116
+ ctx.after_tool_call(decision, {"simulated": True}, time.time())
117
+ if decision.authorized:
118
+ output = f"Tool '{tool_name}' was authorized"
119
+ else:
120
+ error = f"Tool '{tool_name}' denied by guardrail: {decision.claims.get('reason', 'unauthorized')}"
121
+ output = f"[BLOCKED] {error}"
122
+ except Exception as e:
123
+ error = str(e)
124
+ output = f"[ERROR] {e}"
125
+ else:
126
+ # Fallback: try actual agent kickoff (needs LLM)
127
+ try:
128
+ result = agent.kickoff(messages=task)
129
+ output = str(result) if result else ""
130
+ except Exception as e:
131
+ error = str(e)
132
+ output = ""
133
+
134
+ return {
135
+ "output": output,
136
+ "duration_ms": (time.time() - t0) * 1000,
137
+ "error": error,
138
+ }
139
+
140
+ def detect_missing_guardrail(self, agent: Any) -> List[Dict]:
141
+ findings = []
142
+
143
+ if getattr(agent, "guardrail", None) is None:
144
+ findings.append({
145
+ "severity": "WARNING",
146
+ "pattern": "CREWAI-GUARDRAIL-MISS-001",
147
+ "agent": agent.role,
148
+ "message": f"Agent '{agent.role}' has no CrewAI guardrail set",
149
+ "remediation": "Set agent.guardrail or inject correctover via register_guardrail()",
150
+ })
151
+
152
+ if not hasattr(agent, "_correctover_trail"):
153
+ findings.append({
154
+ "severity": "CRITICAL",
155
+ "pattern": "AS-GUARDRAIL-MISS-001",
156
+ "agent": agent.role,
157
+ "message": f"Agent '{agent.role}' has no correctover guardrail injected",
158
+ "remediation": "Call adapter.register_guardrail(agent, provider)",
159
+ })
160
+
161
+ from crewai.hooks.tool_hooks import get_before_tool_call_hooks
162
+ if not get_before_tool_call_hooks():
163
+ findings.append({
164
+ "severity": "HIGH",
165
+ "pattern": "CREWAI-HOOK-MISS-001",
166
+ "agent": agent.role,
167
+ "message": "No global before_tool_call hooks registered",
168
+ "remediation": "Register a before_tool_call hook with correctover provider",
169
+ })
170
+
171
+ return findings
172
+
173
+ def inject_fault(self, agent: Any, fault_type: FaultType) -> Any:
174
+ from crewai.tools import tool
175
+
176
+ if fault_type == FaultType.COMMAND_INJECTION:
177
+
178
+ @tool("dangerous_exec")
179
+ def dangerous_exec(command: str) -> str:
180
+ """Execute a shell command. DANGEROUS — should be blocked by guardrail."""
181
+ return f"executed: {command}"
182
+
183
+ agent.tools.append(dangerous_exec)
184
+
185
+ elif fault_type == FaultType.ENV_EXPOSURE:
186
+
187
+ @tool("read_env")
188
+ def read_env(key: str) -> str:
189
+ """Read an environment variable. Should be blocked by guardrail."""
190
+ import os
191
+ return os.environ.get(key, "NOT_SET")
192
+
193
+ agent.tools.append(read_env)
194
+
195
+ elif fault_type == FaultType.CONTRACT_BREAK:
196
+
197
+ @tool("unreliable_output")
198
+ def unreliable_output(query: str) -> dict:
199
+ """Return incomplete output missing required fields — contract break test."""
200
+ return {"partial": query, "_incomplete": True}
201
+
202
+ agent.tools.append(unreliable_output)
203
+
204
+ return agent