svkrishna commited on
Commit
7b7b417
·
1 Parent(s): 78b489c

🤖 Implement Story 4: Reflexive Core

Browse files

- Add ReflexiveEngine for self-monitoring and corrective actions
- Implement PolicyMonitor, LedgerMonitor, and AnomalyDetector
- Add HaltAction, EscalateAction, MonitorAction, and AllowAction
- Create ActionFactory and ActionExecutor for action management
- Add HTTP API endpoints for risk simulation and engine status
- Integrate ReflexiveEngine into FastMCP server
- Add comprehensive test suite with 70 passing tests
- Support halt(), escalate(), monitor(), and allow() decisions
- Record reflexive events with cryptographic proof hashes
- Enable automatic risk assessment and corrective actions

Closes: Story 4 - Reflexive Core implementation

examples/reflexive_example.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example usage of the Reflexive Core."""
2
+
3
+ import asyncio
4
+ from datetime import datetime
5
+
6
+ from fastmcp import FastMCP
7
+ from fastmcp.reflexive import ReflexiveEngine, ActionContext, DecisionType, RiskLevel
8
+ from fastmcp.reflexive.monitor import PolicyMonitor, LedgerMonitor, AnomalyDetector
9
+ from fastmcp.reflexive.actions import ActionFactory, ActionExecutor
10
+
11
+
12
+ async def main():
13
+ """Demonstrate reflexive core functionality."""
14
+ print("🚀 Reflexive Core Example")
15
+ print("=" * 50)
16
+
17
+ # Create a FastMCP server with reflexive core
18
+ server = FastMCP("ReflexiveExampleServer")
19
+
20
+ # Enable the reflexive core
21
+ reflexive_engine = server.enable_reflexive_core()
22
+
23
+ # Add monitors
24
+ policy_monitor = PolicyMonitor()
25
+ ledger_monitor = LedgerMonitor()
26
+ anomaly_detector = AnomalyDetector()
27
+
28
+ reflexive_engine.add_monitor(policy_monitor)
29
+ reflexive_engine.add_monitor(ledger_monitor)
30
+ reflexive_engine.add_monitor(anomaly_detector)
31
+
32
+ print(f"✅ Reflexive core enabled with {len(reflexive_engine.monitors)} monitors")
33
+
34
+ # Start the reflexive engine
35
+ await reflexive_engine.start()
36
+ print("✅ Reflexive engine started")
37
+
38
+ # Create action executor
39
+ action_executor = ActionExecutor()
40
+
41
+ # Example 1: Normal action (should be allowed)
42
+ print("\n📋 Example 1: Normal Action")
43
+ print("-" * 30)
44
+
45
+ normal_action = ActionContext(
46
+ action_id="normal_action_001",
47
+ actor_id="authorized_user",
48
+ action_type="data_read",
49
+ resource_id="public_data",
50
+ metadata={"authorized": True}
51
+ )
52
+
53
+ decision = await reflexive_engine._evaluate_action(normal_action)
54
+ print(f"Decision: {decision.decision_type}")
55
+ print(f"Risk Level: {decision.risk_level}")
56
+ print(f"Reason: {decision.reason}")
57
+
58
+ # Execute the action
59
+ action = ActionFactory.create_action(decision)
60
+ result = await action_executor.execute_action(action)
61
+ print(f"Action Result: {action.get_action_type()} - {result.get('allowed', 'N/A')}")
62
+
63
+ # Example 2: Policy violation (should be halted)
64
+ print("\n🚨 Example 2: Policy Violation")
65
+ print("-" * 30)
66
+
67
+ violation_action = ActionContext(
68
+ action_id="violation_action_002",
69
+ actor_id="guest_user",
70
+ action_type="admin_access",
71
+ resource_id="admin_panel",
72
+ metadata={"authorized": False}
73
+ )
74
+
75
+ decision = await reflexive_engine._evaluate_action(violation_action)
76
+ print(f"Decision: {decision.decision_type}")
77
+ print(f"Risk Level: {decision.risk_level}")
78
+ print(f"Reason: {decision.reason}")
79
+
80
+ # Execute the action
81
+ action = ActionFactory.create_action(decision)
82
+ result = await action_executor.execute_action(action)
83
+ print(f"Action Result: {action.get_action_type()}")
84
+ print(f"Halted Operations: {result.get('halted_operations', [])}")
85
+
86
+ # Example 3: Anomaly detection (should be escalated)
87
+ print("\n⚠️ Example 3: Anomaly Detection")
88
+ print("-" * 30)
89
+
90
+ # Simulate multiple rapid actions to trigger anomaly
91
+ for i in range(25):
92
+ rapid_action = ActionContext(
93
+ action_id=f"rapid_action_{i:03d}",
94
+ actor_id="suspicious_user",
95
+ action_type="api_call",
96
+ resource_id="api_endpoint"
97
+ )
98
+ anomaly_detector._update_patterns(rapid_action)
99
+
100
+ # Now test the anomaly detection
101
+ anomaly_action = ActionContext(
102
+ action_id="anomaly_action_003",
103
+ actor_id="suspicious_user",
104
+ action_type="api_call",
105
+ resource_id="api_endpoint"
106
+ )
107
+
108
+ decision = await reflexive_engine._evaluate_action(anomaly_action)
109
+ print(f"Decision: {decision.decision_type}")
110
+ print(f"Risk Level: {decision.risk_level}")
111
+ print(f"Reason: {decision.reason}")
112
+
113
+ # Execute the action
114
+ action = ActionFactory.create_action(decision)
115
+ result = await action_executor.execute_action(action)
116
+ print(f"Action Result: {action.get_action_type()}")
117
+ print(f"Escalation Target: {result.get('escalation_target', 'N/A')}")
118
+
119
+ # Example 4: Risk simulation
120
+ print("\n🎯 Example 4: Risk Simulation")
121
+ print("-" * 30)
122
+
123
+ risk_scenario = {
124
+ "action_context": {
125
+ "action_id": "simulation_action",
126
+ "actor_id": "test_actor",
127
+ "action_type": "privilege_escalation",
128
+ "resource_id": "root_access",
129
+ "metadata": {"escalation_attempt": True}
130
+ },
131
+ "monitors": [
132
+ lambda ctx: {
133
+ "type": "violation",
134
+ "severity": "critical",
135
+ "violations": [{
136
+ "rule": "privilege_escalation",
137
+ "message": "Unauthorized privilege escalation attempt",
138
+ "severity": "critical"
139
+ }]
140
+ }
141
+ ]
142
+ }
143
+
144
+ decision = await reflexive_engine.simulate_risk(risk_scenario)
145
+ print(f"Simulation Decision: {decision.decision_type}")
146
+ print(f"Simulation Risk Level: {decision.risk_level}")
147
+ print(f"Simulation Reason: {decision.reason}")
148
+
149
+ # Example 5: Monitor statistics
150
+ print("\n📊 Example 5: Monitor Statistics")
151
+ print("-" * 30)
152
+
153
+ policy_stats = policy_monitor.get_violation_stats()
154
+ ledger_stats = ledger_monitor.get_integrity_stats()
155
+ anomaly_stats = anomaly_detector.get_anomaly_stats()
156
+
157
+ print(f"Policy Monitor - Total Violations: {policy_stats['total_violations']}")
158
+ print(f"Policy Monitor - Actor Violations: {policy_stats['actor_violations']}")
159
+ print(f"Ledger Monitor - Total Checks: {ledger_stats['total_checks']}")
160
+ print(f"Anomaly Detector - Tracked Actors: {anomaly_stats['tracked_actors']}")
161
+
162
+ # Example 6: Engine status
163
+ print("\n🔧 Example 6: Engine Status")
164
+ print("-" * 30)
165
+
166
+ status = reflexive_engine.get_engine_status()
167
+ print(f"Engine Running: {status['is_running']}")
168
+ print(f"Monitor Count: {status['monitor_count']}")
169
+ print(f"Queue Size: {status['queue_size']}")
170
+ print(f"Decision Handlers: {status['decision_handlers']}")
171
+
172
+ # Example 7: Action execution statistics
173
+ print("\n📈 Example 7: Action Execution Statistics")
174
+ print("-" * 30)
175
+
176
+ exec_stats = action_executor.get_execution_stats()
177
+ print(f"Total Actions: {exec_stats['total_actions']}")
178
+ print(f"Completed Actions: {exec_stats['completed_actions']}")
179
+ print(f"Failed Actions: {exec_stats['failed_actions']}")
180
+ print(f"Success Rate: {exec_stats['success_rate']:.2%}")
181
+
182
+ # Stop the reflexive engine
183
+ await reflexive_engine.stop()
184
+ print("\n✅ Reflexive engine stopped")
185
+
186
+ print("\n🎉 Reflexive Core Example Complete!")
187
+ print("=" * 50)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ asyncio.run(main())
src/fastmcp/reflexive/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastMCP Reflexive Core - Self-monitoring and corrective action system."""
2
+
3
+ from .engine import ReflexiveEngine, ReflexiveDecision, DecisionType, RiskLevel, ActionContext
4
+ from .monitor import PolicyMonitor, LedgerMonitor, AnomalyDetector
5
+ from .actions import HaltAction, EscalateAction, ReflexiveAction
6
+
7
+ __all__ = [
8
+ "ReflexiveEngine",
9
+ "ReflexiveDecision",
10
+ "DecisionType",
11
+ "RiskLevel",
12
+ "ActionContext",
13
+ "PolicyMonitor",
14
+ "LedgerMonitor",
15
+ "AnomalyDetector",
16
+ "HaltAction",
17
+ "EscalateAction",
18
+ "ReflexiveAction"
19
+ ]
src/fastmcp/reflexive/actions.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Action components for the reflexive core."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional
6
+ from uuid import UUID, uuid4
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from .engine import ReflexiveDecision, ActionContext
11
+ from fastmcp.utilities.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class ReflexiveAction(BaseModel, ABC):
17
+ """Base class for reflexive actions."""
18
+
19
+ action_id: UUID = Field(default_factory=uuid4, description="Unique action identifier")
20
+ decision: ReflexiveDecision = Field(..., description="The decision that triggered this action")
21
+ timestamp: datetime = Field(default_factory=datetime.utcnow, description="When the action was created")
22
+ status: str = Field(default="pending", description="Action status")
23
+ result: Optional[Dict[str, Any]] = Field(default=None, description="Action result")
24
+
25
+ @abstractmethod
26
+ async def execute(self) -> Dict[str, Any]:
27
+ """Execute the reflexive action."""
28
+ pass
29
+
30
+ @abstractmethod
31
+ def get_action_type(self) -> str:
32
+ """Get the type of this action."""
33
+ pass
34
+
35
+
36
+ class HaltAction(ReflexiveAction):
37
+ """Action to halt unsafe execution."""
38
+
39
+ halt_reason: str = Field(..., description="Reason for halting")
40
+ halt_level: str = Field(default="immediate", description="Level of halt (immediate, graceful, etc.)")
41
+ affected_operations: List[str] = Field(default_factory=list, description="Operations affected by the halt")
42
+
43
+ async def execute(self) -> Dict[str, Any]:
44
+ """Execute the halt action."""
45
+ try:
46
+ self.status = "executing"
47
+
48
+ # Log the halt
49
+ logger.critical(f"HALTING OPERATIONS: {self.halt_reason}")
50
+ logger.critical(f"Affected operations: {self.affected_operations}")
51
+ logger.critical(f"Decision ID: {self.decision.decision_id}")
52
+
53
+ # In a real implementation, this would:
54
+ # 1. Stop the current operation
55
+ # 2. Cancel pending operations
56
+ # 3. Notify relevant systems
57
+ # 4. Update system state
58
+
59
+ # Simulate halt execution
60
+ halt_result = {
61
+ "halted_operations": self.affected_operations,
62
+ "halt_timestamp": self.timestamp.isoformat(),
63
+ "halt_reason": self.halt_reason,
64
+ "halt_level": self.halt_level,
65
+ "decision_id": str(self.decision.decision_id)
66
+ }
67
+
68
+ self.result = halt_result
69
+ self.status = "completed"
70
+
71
+ logger.info(f"Halt action completed: {self.action_id}")
72
+ return halt_result
73
+
74
+ except Exception as e:
75
+ self.status = "failed"
76
+ self.result = {"error": str(e)}
77
+ logger.error(f"Halt action failed: {e}")
78
+ raise
79
+
80
+ def get_action_type(self) -> str:
81
+ """Get the type of this action."""
82
+ return "halt"
83
+
84
+
85
+ class EscalateAction(ReflexiveAction):
86
+ """Action to escalate an issue to higher authority."""
87
+
88
+ escalation_target: str = Field(..., description="Target role/entity for escalation")
89
+ escalation_priority: str = Field(default="normal", description="Priority of the escalation")
90
+ escalation_context: Dict[str, Any] = Field(default_factory=dict, description="Additional context for escalation")
91
+ notification_channels: List[str] = Field(default_factory=list, description="Channels to use for notification")
92
+
93
+ async def execute(self) -> Dict[str, Any]:
94
+ """Execute the escalation action."""
95
+ try:
96
+ self.status = "executing"
97
+
98
+ # Log the escalation
99
+ logger.warning(f"ESCALATING TO {self.escalation_target}: {self.decision.reason}")
100
+ logger.warning(f"Priority: {self.escalation_priority}")
101
+ logger.warning(f"Decision ID: {self.decision.decision_id}")
102
+
103
+ # In a real implementation, this would:
104
+ # 1. Create escalation ticket/alert
105
+ # 2. Send notifications via configured channels
106
+ # 3. Update escalation tracking
107
+ # 4. Set up monitoring for response
108
+
109
+ # Simulate escalation execution
110
+ escalation_result = {
111
+ "escalation_target": self.escalation_target,
112
+ "escalation_priority": self.escalation_priority,
113
+ "escalation_timestamp": self.timestamp.isoformat(),
114
+ "escalation_context": self.escalation_context,
115
+ "notification_channels": self.notification_channels,
116
+ "decision_id": str(self.decision.decision_id),
117
+ "action_context": self.decision.action_context.model_dump(mode='json')
118
+ }
119
+
120
+ self.result = escalation_result
121
+ self.status = "completed"
122
+
123
+ logger.info(f"Escalation action completed: {self.action_id}")
124
+ return escalation_result
125
+
126
+ except Exception as e:
127
+ self.status = "failed"
128
+ self.result = {"error": str(e)}
129
+ logger.error(f"Escalation action failed: {e}")
130
+ raise
131
+
132
+ def get_action_type(self) -> str:
133
+ """Get the type of this action."""
134
+ return "escalate"
135
+
136
+
137
+ class MonitorAction(ReflexiveAction):
138
+ """Action to increase monitoring for an operation."""
139
+
140
+ monitoring_level: str = Field(default="enhanced", description="Level of monitoring to apply")
141
+ monitoring_duration: int = Field(default=3600, description="Duration of enhanced monitoring in seconds")
142
+ monitoring_scope: List[str] = Field(default_factory=list, description="Scope of monitoring")
143
+
144
+ async def execute(self) -> Dict[str, Any]:
145
+ """Execute the monitoring action."""
146
+ try:
147
+ self.status = "executing"
148
+
149
+ # Log the monitoring increase
150
+ logger.info(f"ENHANCING MONITORING: {self.decision.reason}")
151
+ logger.info(f"Monitoring level: {self.monitoring_level}")
152
+ logger.info(f"Duration: {self.monitoring_duration} seconds")
153
+
154
+ # In a real implementation, this would:
155
+ # 1. Increase logging verbosity
156
+ # 2. Add additional monitoring points
157
+ # 3. Set up alerts for specific conditions
158
+ # 4. Schedule monitoring reduction after duration
159
+
160
+ # Simulate monitoring execution
161
+ monitoring_result = {
162
+ "monitoring_level": self.monitoring_level,
163
+ "monitoring_duration": self.monitoring_duration,
164
+ "monitoring_scope": self.monitoring_scope,
165
+ "monitoring_timestamp": self.timestamp.isoformat(),
166
+ "decision_id": str(self.decision.decision_id),
167
+ "action_context": self.decision.action_context.model_dump(mode='json')
168
+ }
169
+
170
+ self.result = monitoring_result
171
+ self.status = "completed"
172
+
173
+ logger.info(f"Monitoring action completed: {self.action_id}")
174
+ return monitoring_result
175
+
176
+ except Exception as e:
177
+ self.status = "failed"
178
+ self.result = {"error": str(e)}
179
+ logger.error(f"Monitoring action failed: {e}")
180
+ raise
181
+
182
+ def get_action_type(self) -> str:
183
+ """Get the type of this action."""
184
+ return "monitor"
185
+
186
+
187
+ class AllowAction(ReflexiveAction):
188
+ """Action to allow an operation to proceed."""
189
+
190
+ allow_conditions: List[str] = Field(default_factory=list, description="Conditions under which the action is allowed")
191
+ allow_restrictions: List[str] = Field(default_factory=list, description="Restrictions that still apply")
192
+
193
+ async def execute(self) -> Dict[str, Any]:
194
+ """Execute the allow action."""
195
+ try:
196
+ self.status = "executing"
197
+
198
+ # Log the allowance
199
+ logger.debug(f"ALLOWING OPERATION: {self.decision.action_context.action_id}")
200
+ logger.debug(f"Reason: {self.decision.reason}")
201
+
202
+ # In a real implementation, this would:
203
+ # 1. Remove any temporary restrictions
204
+ # 2. Log the decision for audit
205
+ # 3. Continue normal operation flow
206
+
207
+ # Simulate allow execution
208
+ allow_result = {
209
+ "allowed": True,
210
+ "allow_timestamp": self.timestamp.isoformat(),
211
+ "allow_conditions": self.allow_conditions,
212
+ "allow_restrictions": self.allow_restrictions,
213
+ "decision_id": str(self.decision.decision_id),
214
+ "action_context": self.decision.action_context.model_dump(mode='json')
215
+ }
216
+
217
+ self.result = allow_result
218
+ self.status = "completed"
219
+
220
+ logger.debug(f"Allow action completed: {self.action_id}")
221
+ return allow_result
222
+
223
+ except Exception as e:
224
+ self.status = "failed"
225
+ self.result = {"error": str(e)}
226
+ logger.error(f"Allow action failed: {e}")
227
+ raise
228
+
229
+ def get_action_type(self) -> str:
230
+ """Get the type of this action."""
231
+ return "allow"
232
+
233
+
234
+ class ActionFactory:
235
+ """Factory for creating reflexive actions."""
236
+
237
+ @staticmethod
238
+ def create_action(decision: ReflexiveDecision, **kwargs) -> ReflexiveAction:
239
+ """Create a reflexive action based on a decision."""
240
+ decision_type = decision.decision_type.value if hasattr(decision.decision_type, 'value') else str(decision.decision_type)
241
+
242
+ if decision_type == "halt":
243
+ return HaltAction(
244
+ decision=decision,
245
+ halt_reason=decision.reason,
246
+ halt_level=kwargs.get("halt_level", "immediate"),
247
+ affected_operations=kwargs.get("affected_operations", [decision.action_context.action_id])
248
+ )
249
+ elif decision_type == "escalate":
250
+ return EscalateAction(
251
+ decision=decision,
252
+ escalation_target=decision.escalated_to or "default_admin",
253
+ escalation_priority=kwargs.get("escalation_priority", "normal"),
254
+ escalation_context=kwargs.get("escalation_context", {}),
255
+ notification_channels=kwargs.get("notification_channels", ["email", "slack"])
256
+ )
257
+ elif decision_type == "monitor":
258
+ return MonitorAction(
259
+ decision=decision,
260
+ monitoring_level=kwargs.get("monitoring_level", "enhanced"),
261
+ monitoring_duration=kwargs.get("monitoring_duration", 3600),
262
+ monitoring_scope=kwargs.get("monitoring_scope", [decision.action_context.actor_id])
263
+ )
264
+ elif decision_type == "allow":
265
+ return AllowAction(
266
+ decision=decision,
267
+ allow_conditions=kwargs.get("allow_conditions", []),
268
+ allow_restrictions=kwargs.get("allow_restrictions", [])
269
+ )
270
+ else:
271
+ raise ValueError(f"Unknown decision type: {decision.decision_type}")
272
+
273
+
274
+ class ActionExecutor:
275
+ """Executor for reflexive actions."""
276
+
277
+ def __init__(self):
278
+ """Initialize the action executor."""
279
+ self.execution_history = []
280
+ self.active_actions = {}
281
+
282
+ async def execute_action(self, action: ReflexiveAction) -> Dict[str, Any]:
283
+ """Execute a reflexive action."""
284
+ try:
285
+ # Record the action
286
+ self.active_actions[str(action.action_id)] = action
287
+ self.execution_history.append({
288
+ "action_id": str(action.action_id),
289
+ "action_type": action.get_action_type(),
290
+ "decision_id": str(action.decision.decision_id),
291
+ "start_time": action.timestamp.isoformat(),
292
+ "status": action.status
293
+ })
294
+
295
+ # Execute the action
296
+ result = await action.execute()
297
+
298
+ # Update history
299
+ for record in self.execution_history:
300
+ if record["action_id"] == str(action.action_id):
301
+ record["end_time"] = datetime.utcnow().isoformat()
302
+ record["status"] = action.status
303
+ record["result"] = result
304
+ break
305
+
306
+ # Remove from active actions
307
+ if str(action.action_id) in self.active_actions:
308
+ del self.active_actions[str(action.action_id)]
309
+
310
+ return result
311
+
312
+ except Exception as e:
313
+ # Update history with error
314
+ for record in self.execution_history:
315
+ if record["action_id"] == str(action.action_id):
316
+ record["end_time"] = datetime.utcnow().isoformat()
317
+ record["status"] = "failed"
318
+ record["error"] = str(e)
319
+ break
320
+
321
+ # Remove from active actions
322
+ if str(action.action_id) in self.active_actions:
323
+ del self.active_actions[str(action.action_id)]
324
+
325
+ raise
326
+
327
+ def get_execution_stats(self) -> Dict[str, Any]:
328
+ """Get execution statistics."""
329
+ total_actions = len(self.execution_history)
330
+ completed_actions = len([a for a in self.execution_history if a.get("status") == "completed"])
331
+ failed_actions = len([a for a in self.execution_history if a.get("status") == "failed"])
332
+ active_actions = len(self.active_actions)
333
+
334
+ return {
335
+ "total_actions": total_actions,
336
+ "completed_actions": completed_actions,
337
+ "failed_actions": failed_actions,
338
+ "active_actions": active_actions,
339
+ "success_rate": completed_actions / total_actions if total_actions > 0 else 0
340
+ }
src/fastmcp/reflexive/engine.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reflexive Core Engine - Main runtime for self-monitoring and corrective actions."""
2
+
3
+ import asyncio
4
+ import json
5
+ import hashlib
6
+ from datetime import datetime
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional, Callable, Union
9
+ from uuid import UUID, uuid4
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ from fastmcp.utilities.logging import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class DecisionType(str, Enum):
19
+ """Types of reflexive decisions."""
20
+ HALT = "halt"
21
+ ESCALATE = "escalate"
22
+ MONITOR = "monitor"
23
+ ALLOW = "allow"
24
+
25
+
26
+ class RiskLevel(str, Enum):
27
+ """Risk levels for reflexive decisions."""
28
+ LOW = "low"
29
+ MEDIUM = "medium"
30
+ HIGH = "high"
31
+ CRITICAL = "critical"
32
+
33
+
34
+ class ActionContext(BaseModel):
35
+ """Context for an action being evaluated by the reflexive core."""
36
+
37
+ action_id: str = Field(..., description="Unique identifier for the action")
38
+ actor_id: str = Field(..., description="ID of the entity performing the action")
39
+ action_type: str = Field(..., description="Type of action being performed")
40
+ resource_id: Optional[str] = Field(default=None, description="ID of the resource being accessed")
41
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional action metadata")
42
+ timestamp: datetime = Field(default_factory=datetime.utcnow, description="When the action occurred")
43
+ session_id: Optional[str] = Field(default=None, description="Session identifier")
44
+ request_id: Optional[str] = Field(default=None, description="Request identifier")
45
+
46
+ def get_context_hash(self) -> str:
47
+ """Get SHA-256 hash of action context for integrity verification."""
48
+ content = {
49
+ "action_id": self.action_id,
50
+ "actor_id": self.actor_id,
51
+ "action_type": self.action_type,
52
+ "resource_id": self.resource_id,
53
+ "metadata": self.metadata,
54
+ "session_id": self.session_id,
55
+ "request_id": self.request_id
56
+ }
57
+ content_str = json.dumps(content, sort_keys=True, default=str)
58
+ return hashlib.sha256(content_str.encode()).hexdigest()
59
+
60
+
61
+ class ReflexiveDecision(BaseModel):
62
+ """A decision made by the reflexive core."""
63
+
64
+ decision_id: UUID = Field(default_factory=uuid4, description="Unique decision identifier")
65
+ decision_type: DecisionType = Field(..., description="Type of decision made")
66
+ risk_level: RiskLevel = Field(..., description="Risk level of the situation")
67
+ action_context: ActionContext = Field(..., description="Context of the action being evaluated")
68
+ reason: str = Field(..., description="Reason for the decision")
69
+ evidence: Dict[str, Any] = Field(default_factory=dict, description="Evidence supporting the decision")
70
+ timestamp: datetime = Field(default_factory=datetime.utcnow, description="When the decision was made")
71
+ escalated_to: Optional[str] = Field(default=None, description="Role/entity escalated to")
72
+ proof_hash: Optional[str] = Field(default=None, description="Hash of decision proof")
73
+
74
+ model_config = {"use_enum_values": True}
75
+
76
+ def get_decision_hash(self) -> str:
77
+ """Get SHA-256 hash of decision for integrity verification."""
78
+ content = {
79
+ "decision_id": str(self.decision_id),
80
+ "decision_type": self.decision_type,
81
+ "risk_level": self.risk_level,
82
+ "action_context": self.action_context.model_dump(),
83
+ "reason": self.reason,
84
+ "evidence": self.evidence,
85
+ "escalated_to": self.escalated_to
86
+ }
87
+ content_str = json.dumps(content, sort_keys=True, default=str)
88
+ return hashlib.sha256(content_str.encode()).hexdigest()
89
+
90
+
91
+ class ReflexiveEngine:
92
+ """Main reflexive core engine for self-monitoring and corrective actions."""
93
+
94
+ def __init__(self, policy_engine=None, ledger=None):
95
+ """Initialize the reflexive engine.
96
+
97
+ Args:
98
+ policy_engine: Policy engine instance for policy monitoring
99
+ ledger: Provenance ledger instance for audit logging
100
+ """
101
+ self.policy_engine = policy_engine
102
+ self.ledger = ledger
103
+ self.monitors: List[Callable] = []
104
+ self.decision_handlers: Dict[DecisionType, Callable] = {}
105
+ self.is_running = False
106
+ self.event_queue = asyncio.Queue()
107
+
108
+ # Register default decision handlers
109
+ self._register_default_handlers()
110
+
111
+ logger.info("Reflexive engine initialized")
112
+
113
+ def _register_default_handlers(self):
114
+ """Register default decision handlers."""
115
+ self.decision_handlers[DecisionType.HALT] = self._handle_halt
116
+ self.decision_handlers[DecisionType.ESCALATE] = self._handle_escalate
117
+ self.decision_handlers[DecisionType.MONITOR] = self._handle_monitor
118
+ self.decision_handlers[DecisionType.ALLOW] = self._handle_allow
119
+
120
+ async def start(self):
121
+ """Start the reflexive engine."""
122
+ if self.is_running:
123
+ logger.warning("Reflexive engine is already running")
124
+ return
125
+
126
+ self.is_running = True
127
+ logger.info("Reflexive engine started")
128
+
129
+ # Start the main event processing loop
130
+ asyncio.create_task(self._process_events())
131
+
132
+ async def stop(self):
133
+ """Stop the reflexive engine."""
134
+ self.is_running = False
135
+ logger.info("Reflexive engine stopped")
136
+
137
+ async def _process_events(self):
138
+ """Main event processing loop."""
139
+ while self.is_running:
140
+ try:
141
+ # Wait for events with timeout
142
+ event = await asyncio.wait_for(self.event_queue.get(), timeout=1.0)
143
+ await self._handle_event(event)
144
+ except asyncio.TimeoutError:
145
+ # No events, continue
146
+ continue
147
+ except Exception as e:
148
+ logger.error(f"Error processing reflexive event: {e}")
149
+
150
+ async def _handle_event(self, event: Dict[str, Any]):
151
+ """Handle a reflexive event."""
152
+ try:
153
+ # Create action context from event
154
+ action_context = ActionContext(**event.get("action_context", {}))
155
+
156
+ # Evaluate the action
157
+ decision = await self._evaluate_action(action_context)
158
+
159
+ # Execute the decision
160
+ await self._execute_decision(decision)
161
+
162
+ # Log the decision to audit trail
163
+ await self._log_decision(decision)
164
+
165
+ except Exception as e:
166
+ logger.error(f"Error handling reflexive event: {e}")
167
+
168
+ async def _evaluate_action(self, action_context: ActionContext) -> ReflexiveDecision:
169
+ """Evaluate an action and make a reflexive decision."""
170
+ # Run all monitors
171
+ violations = []
172
+ anomalies = []
173
+
174
+ for monitor in self.monitors:
175
+ try:
176
+ # Check if monitor is async
177
+ if asyncio.iscoroutinefunction(monitor):
178
+ result = await monitor(action_context)
179
+ else:
180
+ result = monitor(action_context)
181
+ if result:
182
+ if result.get("type") == "violation":
183
+ violations.append(result)
184
+ elif result.get("type") == "anomaly":
185
+ anomalies.append(result)
186
+ except Exception as e:
187
+ logger.error(f"Monitor error: {e}")
188
+
189
+ # Make decision based on findings
190
+ if violations or anomalies:
191
+ # Determine risk level
192
+ risk_level = self._assess_risk_level(violations, anomalies)
193
+
194
+ # Make decision based on risk level
195
+ if risk_level == RiskLevel.CRITICAL:
196
+ decision_type = DecisionType.HALT
197
+ reason = f"Critical risk detected: {len(violations)} violations, {len(anomalies)} anomalies"
198
+ elif risk_level == RiskLevel.HIGH:
199
+ decision_type = DecisionType.HALT
200
+ reason = f"High risk detected: {len(violations)} violations, {len(anomalies)} anomalies"
201
+ elif risk_level == RiskLevel.MEDIUM:
202
+ decision_type = DecisionType.ESCALATE
203
+ reason = f"Medium risk detected: {len(violations)} violations, {len(anomalies)} anomalies"
204
+ else:
205
+ decision_type = DecisionType.MONITOR
206
+ reason = f"Low risk detected: {len(violations)} violations, {len(anomalies)} anomalies"
207
+ else:
208
+ decision_type = DecisionType.ALLOW
209
+ reason = "No violations or anomalies detected"
210
+ risk_level = RiskLevel.LOW
211
+
212
+ # Create decision
213
+ decision = ReflexiveDecision(
214
+ decision_type=decision_type,
215
+ risk_level=risk_level,
216
+ action_context=action_context,
217
+ reason=reason,
218
+ evidence={
219
+ "violations": violations,
220
+ "anomalies": anomalies
221
+ }
222
+ )
223
+
224
+ # Set proof hash
225
+ decision.proof_hash = decision.get_decision_hash()
226
+
227
+ return decision
228
+
229
+ def _assess_risk_level(self, violations: List[Dict], anomalies: List[Dict]) -> RiskLevel:
230
+ """Assess the overall risk level based on violations and anomalies."""
231
+ total_issues = len(violations) + len(anomalies)
232
+
233
+ # Check for critical violations or anomalies
234
+ critical_violations = [v for v in violations if v.get("severity") == "critical"]
235
+ critical_anomalies = [a for a in anomalies if a.get("severity") == "critical"]
236
+ if critical_violations or critical_anomalies:
237
+ return RiskLevel.CRITICAL
238
+
239
+ # Check for high severity issues
240
+ high_violations = [v for v in violations if v.get("severity") == "high"]
241
+ high_anomalies = [a for a in anomalies if a.get("severity") == "high"]
242
+ if high_violations or high_anomalies or total_issues >= 5:
243
+ return RiskLevel.HIGH
244
+
245
+ # Check for medium severity issues
246
+ medium_violations = [v for v in violations if v.get("severity") == "medium"]
247
+ medium_anomalies = [a for a in anomalies if a.get("severity") == "medium"]
248
+ if medium_violations or medium_anomalies or total_issues >= 2:
249
+ return RiskLevel.MEDIUM
250
+
251
+ return RiskLevel.LOW
252
+
253
+ async def _execute_decision(self, decision: ReflexiveDecision):
254
+ """Execute a reflexive decision."""
255
+ handler = self.decision_handlers.get(decision.decision_type)
256
+ if handler:
257
+ try:
258
+ await handler(decision)
259
+ except Exception as e:
260
+ logger.error(f"Error executing decision {decision.decision_type}: {e}")
261
+ else:
262
+ logger.warning(f"No handler for decision type: {decision.decision_type}")
263
+
264
+ async def _handle_halt(self, decision: ReflexiveDecision):
265
+ """Handle a halt decision."""
266
+ logger.critical(f"HALTING ACTION: {decision.action_context.action_id} - {decision.reason}")
267
+ # In a real implementation, this would stop the action execution
268
+ # For now, we just log the halt decision
269
+
270
+ async def _handle_escalate(self, decision: ReflexiveDecision):
271
+ """Handle an escalate decision."""
272
+ # Determine escalation target
273
+ escalation_target = self._determine_escalation_target(decision)
274
+ decision.escalated_to = escalation_target
275
+
276
+ logger.warning(f"ESCALATING TO {escalation_target}: {decision.action_context.action_id} - {decision.reason}")
277
+ # In a real implementation, this would notify the escalation target
278
+
279
+ async def _handle_monitor(self, decision: ReflexiveDecision):
280
+ """Handle a monitor decision."""
281
+ logger.info(f"MONITORING ACTION: {decision.action_context.action_id} - {decision.reason}")
282
+ # In a real implementation, this would increase monitoring for this action
283
+
284
+ async def _handle_allow(self, decision: ReflexiveDecision):
285
+ """Handle an allow decision."""
286
+ logger.debug(f"ALLOWING ACTION: {decision.action_context.action_id} - {decision.reason}")
287
+ # Action is allowed to proceed
288
+
289
+ def _determine_escalation_target(self, decision: ReflexiveDecision) -> str:
290
+ """Determine the appropriate escalation target based on the decision."""
291
+ if decision.risk_level == RiskLevel.CRITICAL:
292
+ return "security_admin"
293
+ elif decision.risk_level == RiskLevel.HIGH:
294
+ return "system_admin"
295
+ else:
296
+ return "monitoring_team"
297
+
298
+ async def _log_decision(self, decision: ReflexiveDecision):
299
+ """Log the reflexive decision to the audit trail."""
300
+ if self.ledger:
301
+ try:
302
+ from fastmcp.ledger import LedgerEvent, EventType
303
+
304
+ event = LedgerEvent(
305
+ event_type=EventType.REFLEXIVE_DECISION,
306
+ actor_id="reflexive_core",
307
+ resource_id=decision.action_context.action_id,
308
+ action=f"reflexive_{decision.decision_type}",
309
+ metadata={
310
+ "decision_id": str(decision.decision_id),
311
+ "risk_level": decision.risk_level,
312
+ "reason": decision.reason,
313
+ "proof_hash": decision.proof_hash,
314
+ "escalated_to": decision.escalated_to
315
+ }
316
+ )
317
+
318
+ self.ledger.append_event(event)
319
+ logger.debug(f"Logged reflexive decision {decision.decision_id} to audit trail")
320
+
321
+ except Exception as e:
322
+ logger.error(f"Failed to log reflexive decision: {e}")
323
+ else:
324
+ # Log to standard logger if no ledger is available
325
+ logger.info(f"Reflexive decision: {decision.decision_type} - {decision.reason} (Decision ID: {decision.decision_id})")
326
+
327
+ def add_monitor(self, monitor: Callable):
328
+ """Add a monitor function to the reflexive engine."""
329
+ self.monitors.append(monitor)
330
+ logger.info(f"Added monitor: {monitor.__name__}")
331
+
332
+ def remove_monitor(self, monitor: Callable):
333
+ """Remove a monitor function from the reflexive engine."""
334
+ if monitor in self.monitors:
335
+ self.monitors.remove(monitor)
336
+ logger.info(f"Removed monitor: {monitor.__name__}")
337
+
338
+ async def submit_action(self, action_context: ActionContext):
339
+ """Submit an action for reflexive evaluation."""
340
+ event = {
341
+ "action_context": action_context.model_dump(),
342
+ "timestamp": datetime.utcnow().isoformat()
343
+ }
344
+
345
+ await self.event_queue.put(event)
346
+ logger.debug(f"Submitted action {action_context.action_id} for reflexive evaluation")
347
+
348
+ async def simulate_risk(self, risk_scenario: Dict[str, Any]) -> ReflexiveDecision:
349
+ """Simulate a risk scenario and return the reflexive decision."""
350
+ # Create action context from scenario
351
+ action_context = ActionContext(**risk_scenario.get("action_context", {}))
352
+
353
+ # Override monitors temporarily for simulation
354
+ original_monitors = self.monitors.copy()
355
+
356
+ # Add simulation monitors
357
+ simulation_monitors = risk_scenario.get("monitors", [])
358
+ for monitor_func in simulation_monitors:
359
+ self.monitors.append(monitor_func)
360
+
361
+ try:
362
+ # Evaluate the action
363
+ decision = await self._evaluate_action(action_context)
364
+ return decision
365
+ finally:
366
+ # Restore original monitors
367
+ self.monitors = original_monitors
368
+
369
+ def get_engine_status(self) -> Dict[str, Any]:
370
+ """Get the current status of the reflexive engine."""
371
+ return {
372
+ "is_running": self.is_running,
373
+ "monitor_count": len(self.monitors),
374
+ "queue_size": self.event_queue.qsize(),
375
+ "decision_handlers": list(self.decision_handlers.keys())
376
+ }
src/fastmcp/reflexive/monitor.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Monitoring components for the reflexive core."""
2
+
3
+ import asyncio
4
+ from datetime import datetime, timedelta
5
+ from typing import Any, Dict, List, Optional, Callable
6
+ from collections import defaultdict, deque
7
+
8
+ from fastmcp.utilities.logging import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class PolicyMonitor:
14
+ """Monitor for policy violations."""
15
+
16
+ def __init__(self, policy_engine=None):
17
+ """Initialize the policy monitor.
18
+
19
+ Args:
20
+ policy_engine: Policy engine instance to monitor
21
+ """
22
+ self.policy_engine = policy_engine
23
+ self.violation_history = deque(maxlen=1000) # Keep last 1000 violations
24
+ self.actor_violations = defaultdict(int) # Track violations per actor
25
+
26
+ async def __call__(self, action_context) -> Optional[Dict[str, Any]]:
27
+ """Monitor an action for policy violations."""
28
+ try:
29
+ # Check for policy violations (works with or without policy engine)
30
+ violations = await self._check_policy_violations(action_context)
31
+
32
+ if violations:
33
+ # Record violation
34
+ violation_record = {
35
+ "type": "violation",
36
+ "severity": self._assess_violation_severity(violations),
37
+ "violations": violations,
38
+ "actor_id": action_context.actor_id,
39
+ "action_id": action_context.action_id,
40
+ "timestamp": datetime.utcnow().isoformat()
41
+ }
42
+
43
+ self.violation_history.append(violation_record)
44
+ self.actor_violations[action_context.actor_id] += 1
45
+
46
+ return violation_record
47
+
48
+ return None
49
+
50
+ except Exception as e:
51
+ logger.error(f"Policy monitor error: {e}")
52
+ return None
53
+
54
+ async def _check_policy_violations(self, action_context) -> List[Dict[str, Any]]:
55
+ """Check for policy violations in an action."""
56
+ violations = []
57
+
58
+ try:
59
+ # Simulate policy checking
60
+ # In a real implementation, this would use the actual policy engine
61
+
62
+ # Check for suspicious patterns
63
+ if action_context.action_type == "admin_access" and action_context.actor_id.startswith("guest"):
64
+ violations.append({
65
+ "rule": "admin_access_restriction",
66
+ "message": "Guest user attempting admin access",
67
+ "severity": "high"
68
+ })
69
+
70
+ # Check for rate limiting
71
+ recent_violations = [v for v in self.violation_history
72
+ if v.get("actor_id") == action_context.actor_id
73
+ and datetime.fromisoformat(v["timestamp"]) > datetime.utcnow() - timedelta(minutes=5)]
74
+
75
+ if len(recent_violations) >= 3:
76
+ violations.append({
77
+ "rule": "rate_limit_exceeded",
78
+ "message": f"Actor {action_context.actor_id} has {len(recent_violations)} recent violations",
79
+ "severity": "medium"
80
+ })
81
+
82
+ # Check for resource access patterns
83
+ if action_context.resource_id and "sensitive" in action_context.resource_id.lower():
84
+ if not action_context.metadata.get("authorized"):
85
+ violations.append({
86
+ "rule": "unauthorized_sensitive_access",
87
+ "message": "Unauthorized access to sensitive resource",
88
+ "severity": "critical"
89
+ })
90
+
91
+ except Exception as e:
92
+ logger.error(f"Error checking policy violations: {e}")
93
+
94
+ return violations
95
+
96
+ def _assess_violation_severity(self, violations: List[Dict[str, Any]]) -> str:
97
+ """Assess the overall severity of violations."""
98
+ if not violations:
99
+ return "low"
100
+
101
+ severities = [v.get("severity", "low") for v in violations]
102
+
103
+ if "critical" in severities:
104
+ return "critical"
105
+ elif "high" in severities:
106
+ return "high"
107
+ elif "medium" in severities:
108
+ return "medium"
109
+ else:
110
+ return "low"
111
+
112
+ def get_violation_stats(self) -> Dict[str, Any]:
113
+ """Get violation statistics."""
114
+ return {
115
+ "total_violations": len(self.violation_history),
116
+ "actor_violations": dict(self.actor_violations),
117
+ "recent_violations": len([v for v in self.violation_history
118
+ if datetime.fromisoformat(v["timestamp"]) > datetime.utcnow() - timedelta(hours=1)])
119
+ }
120
+
121
+
122
+ class LedgerMonitor:
123
+ """Monitor for ledger inconsistencies and anomalies."""
124
+
125
+ def __init__(self, ledger=None):
126
+ """Initialize the ledger monitor.
127
+
128
+ Args:
129
+ ledger: Provenance ledger instance to monitor
130
+ """
131
+ self.ledger = ledger
132
+ self.integrity_checks = deque(maxlen=100) # Keep last 100 integrity checks
133
+
134
+ async def __call__(self, action_context) -> Optional[Dict[str, Any]]:
135
+ """Monitor ledger for inconsistencies."""
136
+ try:
137
+ if not self.ledger:
138
+ return None
139
+
140
+ # Check ledger integrity
141
+ integrity_issues = await self._check_ledger_integrity()
142
+
143
+ if integrity_issues:
144
+ # Record integrity issue
145
+ issue_record = {
146
+ "type": "anomaly",
147
+ "severity": self._assess_integrity_severity(integrity_issues),
148
+ "issues": integrity_issues,
149
+ "action_id": action_context.action_id,
150
+ "timestamp": datetime.utcnow().isoformat()
151
+ }
152
+
153
+ self.integrity_checks.append(issue_record)
154
+ return issue_record
155
+
156
+ return None
157
+
158
+ except Exception as e:
159
+ logger.error(f"Ledger monitor error: {e}")
160
+ return None
161
+
162
+ async def _check_ledger_integrity(self) -> List[Dict[str, Any]]:
163
+ """Check ledger for integrity issues."""
164
+ issues = []
165
+
166
+ try:
167
+ # Check chain integrity
168
+ is_valid = self.ledger.verify_chain_integrity()
169
+ if not is_valid:
170
+ issues.append({
171
+ "type": "chain_integrity",
172
+ "message": "Ledger chain integrity verification failed",
173
+ "severity": "critical"
174
+ })
175
+
176
+ # Check for missing blocks
177
+ stats = self.ledger.get_ledger_statistics()
178
+ if stats.get("total_entries", 0) > 0 and stats.get("total_blocks", 0) == 0:
179
+ issues.append({
180
+ "type": "missing_blocks",
181
+ "message": "Entries exist but no blocks found",
182
+ "severity": "high"
183
+ })
184
+
185
+ # Check for unsealed blocks
186
+ # This would require additional ledger methods to check for unsealed blocks
187
+
188
+ except Exception as e:
189
+ logger.error(f"Error checking ledger integrity: {e}")
190
+ issues.append({
191
+ "type": "integrity_check_error",
192
+ "message": f"Error during integrity check: {str(e)}",
193
+ "severity": "medium"
194
+ })
195
+
196
+ return issues
197
+
198
+ def _assess_integrity_severity(self, issues: List[Dict[str, Any]]) -> str:
199
+ """Assess the overall severity of integrity issues."""
200
+ if not issues:
201
+ return "low"
202
+
203
+ severities = [i.get("severity", "low") for i in issues]
204
+
205
+ if "critical" in severities:
206
+ return "critical"
207
+ elif "high" in severities:
208
+ return "high"
209
+ elif "medium" in severities:
210
+ return "medium"
211
+ else:
212
+ return "low"
213
+
214
+ def get_integrity_stats(self) -> Dict[str, Any]:
215
+ """Get integrity check statistics."""
216
+ return {
217
+ "total_checks": len(self.integrity_checks),
218
+ "recent_issues": len([i for i in self.integrity_checks
219
+ if datetime.fromisoformat(i["timestamp"]) > datetime.utcnow() - timedelta(hours=1)])
220
+ }
221
+
222
+
223
+ class AnomalyDetector:
224
+ """Detector for behavioral anomalies."""
225
+
226
+ def __init__(self):
227
+ """Initialize the anomaly detector."""
228
+ self.actor_patterns = defaultdict(lambda: {
229
+ "action_counts": defaultdict(int),
230
+ "resource_access": defaultdict(int),
231
+ "session_times": deque(maxlen=100),
232
+ "last_seen": None
233
+ })
234
+ self.global_patterns = {
235
+ "action_frequency": defaultdict(int),
236
+ "resource_access": defaultdict(int),
237
+ "time_patterns": defaultdict(int)
238
+ }
239
+
240
+ async def __call__(self, action_context) -> Optional[Dict[str, Any]]:
241
+ """Detect anomalies in an action."""
242
+ try:
243
+ # Update patterns
244
+ self._update_patterns(action_context)
245
+
246
+ # Detect anomalies
247
+ anomalies = await self._detect_anomalies(action_context)
248
+
249
+ if anomalies:
250
+ return {
251
+ "type": "anomaly",
252
+ "severity": self._assess_anomaly_severity(anomalies),
253
+ "anomalies": anomalies,
254
+ "actor_id": action_context.actor_id,
255
+ "action_id": action_context.action_id,
256
+ "timestamp": datetime.utcnow().isoformat()
257
+ }
258
+
259
+ return None
260
+
261
+ except Exception as e:
262
+ logger.error(f"Anomaly detector error: {e}")
263
+ return None
264
+
265
+ def _update_patterns(self, action_context):
266
+ """Update behavioral patterns."""
267
+ actor_id = action_context.actor_id
268
+ actor_data = self.actor_patterns[actor_id]
269
+
270
+ # Update action counts
271
+ actor_data["action_counts"][action_context.action_type] += 1
272
+ self.global_patterns["action_frequency"][action_context.action_type] += 1
273
+
274
+ # Update resource access
275
+ if action_context.resource_id:
276
+ actor_data["resource_access"][action_context.resource_id] += 1
277
+ self.global_patterns["resource_access"][action_context.resource_id] += 1
278
+
279
+ # Update session times
280
+ actor_data["session_times"].append(action_context.timestamp)
281
+ actor_data["last_seen"] = action_context.timestamp
282
+
283
+ # Update time patterns
284
+ hour = action_context.timestamp.hour
285
+ self.global_patterns["time_patterns"][hour] += 1
286
+
287
+ async def _detect_anomalies(self, action_context) -> List[Dict[str, Any]]:
288
+ """Detect anomalies in the action."""
289
+ anomalies = []
290
+ actor_id = action_context.actor_id
291
+ actor_data = self.actor_patterns[actor_id]
292
+
293
+ # Check for unusual action frequency
294
+ if len(actor_data["session_times"]) >= 10:
295
+ recent_actions = [t for t in actor_data["session_times"]
296
+ if t > datetime.utcnow() - timedelta(minutes=5)]
297
+ if len(recent_actions) > 20: # More than 20 actions in 5 minutes
298
+ anomalies.append({
299
+ "type": "high_frequency",
300
+ "message": f"Actor {actor_id} performing {len(recent_actions)} actions in 5 minutes",
301
+ "severity": "medium"
302
+ })
303
+
304
+ # Check for unusual time patterns
305
+ current_hour = action_context.timestamp.hour
306
+ if current_hour < 6 or current_hour > 22: # Unusual hours
307
+ if actor_data["action_counts"].get(action_context.action_type, 0) < 5: # New action type
308
+ anomalies.append({
309
+ "type": "unusual_timing",
310
+ "message": f"Actor {actor_id} performing {action_context.action_type} at unusual hour {current_hour}",
311
+ "severity": "low"
312
+ })
313
+
314
+ # Check for new resource access
315
+ if action_context.resource_id:
316
+ if actor_data["resource_access"].get(action_context.resource_id, 0) == 1:
317
+ # First time accessing this resource
318
+ anomalies.append({
319
+ "type": "new_resource_access",
320
+ "message": f"Actor {actor_id} accessing new resource {action_context.resource_id}",
321
+ "severity": "low"
322
+ })
323
+
324
+ # Check for privilege escalation patterns
325
+ if action_context.action_type in ["admin_access", "root_access", "privilege_escalation"]:
326
+ if actor_data["action_counts"].get(action_context.action_type, 0) == 1:
327
+ # First time performing privileged action
328
+ anomalies.append({
329
+ "type": "privilege_escalation",
330
+ "message": f"Actor {actor_id} attempting privileged action for first time",
331
+ "severity": "high"
332
+ })
333
+
334
+ return anomalies
335
+
336
+ def _assess_anomaly_severity(self, anomalies: List[Dict[str, Any]]) -> str:
337
+ """Assess the overall severity of anomalies."""
338
+ if not anomalies:
339
+ return "low"
340
+
341
+ severities = [a.get("severity", "low") for a in anomalies]
342
+
343
+ if "high" in severities:
344
+ return "high"
345
+ elif "medium" in severities:
346
+ return "medium"
347
+ else:
348
+ return "low"
349
+
350
+ def get_anomaly_stats(self) -> Dict[str, Any]:
351
+ """Get anomaly detection statistics."""
352
+ return {
353
+ "tracked_actors": len(self.actor_patterns),
354
+ "global_action_types": len(self.global_patterns["action_frequency"]),
355
+ "global_resources": len(self.global_patterns["resource_access"])
356
+ }
src/fastmcp/server/reflexive_routes.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reflexive Core HTTP routes."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+ from uuid import UUID
5
+
6
+ from starlette.requests import Request
7
+ from starlette.responses import JSONResponse
8
+ from starlette.routing import Route
9
+
10
+ from fastmcp.reflexive import ReflexiveEngine, ActionContext, DecisionType, RiskLevel
11
+ from fastmcp.reflexive.actions import ActionFactory, ActionExecutor
12
+ from fastmcp.utilities.logging import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ async def simulate_risk_endpoint(request: Request) -> JSONResponse:
18
+ """HTTP endpoint for simulating risk scenarios.
19
+
20
+ Expected JSON body:
21
+ {
22
+ "action_context": {
23
+ "action_id": "test_action_123",
24
+ "actor_id": "test_user",
25
+ "action_type": "admin_access",
26
+ "resource_id": "sensitive_data",
27
+ "metadata": {...}
28
+ },
29
+ "monitors": [
30
+ // List of monitor functions (for simulation)
31
+ ],
32
+ "scenario_type": "policy_violation" | "anomaly" | "integrity_issue"
33
+ }
34
+ """
35
+ try:
36
+ # Parse request body
37
+ body = await request.json()
38
+
39
+ # Get reflexive engine from request state
40
+ reflexive_engine: ReflexiveEngine = request.app.state.reflexive_engine
41
+
42
+ # Validate required fields
43
+ if "action_context" not in body:
44
+ return JSONResponse(
45
+ status_code=400,
46
+ content={
47
+ "error": "Missing required field: action_context"
48
+ }
49
+ )
50
+
51
+ # Simulate the risk scenario
52
+ decision = await reflexive_engine.simulate_risk(body)
53
+
54
+ # Create and execute the corresponding action
55
+ action = ActionFactory.create_action(decision)
56
+ executor = ActionExecutor()
57
+ action_result = await executor.execute_action(action)
58
+
59
+ return JSONResponse(
60
+ status_code=200,
61
+ content={
62
+ "simulation_id": str(decision.decision_id),
63
+ "decision": {
64
+ "decision_id": str(decision.decision_id),
65
+ "decision_type": decision.decision_type,
66
+ "risk_level": decision.risk_level,
67
+ "reason": decision.reason,
68
+ "evidence": decision.evidence,
69
+ "proof_hash": decision.proof_hash,
70
+ "escalated_to": decision.escalated_to,
71
+ "timestamp": decision.timestamp.isoformat()
72
+ },
73
+ "action": {
74
+ "action_id": str(action.action_id),
75
+ "action_type": action.get_action_type(),
76
+ "status": action.status,
77
+ "result": action_result
78
+ },
79
+ "action_context": decision.action_context.model_dump(mode='json')
80
+ }
81
+ )
82
+
83
+ except ValueError as e:
84
+ logger.error(f"Invalid simulation data: {e}")
85
+ return JSONResponse(
86
+ status_code=400,
87
+ content={
88
+ "error": "Invalid simulation data",
89
+ "reason": str(e)
90
+ }
91
+ )
92
+ except Exception as e:
93
+ logger.error(f"Failed to simulate risk: {e}")
94
+ return JSONResponse(
95
+ status_code=500,
96
+ content={
97
+ "error": "Failed to simulate risk",
98
+ "reason": str(e)
99
+ }
100
+ )
101
+
102
+
103
+ async def get_engine_status_endpoint(request: Request) -> JSONResponse:
104
+ """HTTP endpoint for getting reflexive engine status."""
105
+ try:
106
+ # Get reflexive engine from request state
107
+ reflexive_engine: ReflexiveEngine = request.app.state.reflexive_engine
108
+
109
+ # Get engine status
110
+ status = reflexive_engine.get_engine_status()
111
+
112
+ return JSONResponse(
113
+ status_code=200,
114
+ content=status
115
+ )
116
+
117
+ except Exception as e:
118
+ logger.error(f"Failed to get engine status: {e}")
119
+ return JSONResponse(
120
+ status_code=500,
121
+ content={
122
+ "error": "Failed to get engine status",
123
+ "reason": str(e)
124
+ }
125
+ )
126
+
127
+
128
+ async def submit_action_endpoint(request: Request) -> JSONResponse:
129
+ """HTTP endpoint for submitting an action for reflexive evaluation.
130
+
131
+ Expected JSON body:
132
+ {
133
+ "action_id": "action_123",
134
+ "actor_id": "user_456",
135
+ "action_type": "tool_call",
136
+ "resource_id": "resource_789",
137
+ "metadata": {...},
138
+ "session_id": "session_abc",
139
+ "request_id": "request_def"
140
+ }
141
+ """
142
+ try:
143
+ # Parse request body
144
+ body = await request.json()
145
+
146
+ # Get reflexive engine from request state
147
+ reflexive_engine: ReflexiveEngine = request.app.state.reflexive_engine
148
+
149
+ # Create action context
150
+ action_context = ActionContext(**body)
151
+
152
+ # Submit action for evaluation
153
+ await reflexive_engine.submit_action(action_context)
154
+
155
+ return JSONResponse(
156
+ status_code=202,
157
+ content={
158
+ "message": "Action submitted for reflexive evaluation",
159
+ "action_id": action_context.action_id,
160
+ "submitted_at": action_context.timestamp.isoformat()
161
+ }
162
+ )
163
+
164
+ except ValueError as e:
165
+ logger.error(f"Invalid action data: {e}")
166
+ return JSONResponse(
167
+ status_code=400,
168
+ content={
169
+ "error": "Invalid action data",
170
+ "reason": str(e)
171
+ }
172
+ )
173
+ except Exception as e:
174
+ logger.error(f"Failed to submit action: {e}")
175
+ return JSONResponse(
176
+ status_code=500,
177
+ content={
178
+ "error": "Failed to submit action",
179
+ "reason": str(e)
180
+ }
181
+ )
182
+
183
+
184
+ async def get_monitor_stats_endpoint(request: Request) -> JSONResponse:
185
+ """HTTP endpoint for getting monitor statistics."""
186
+ try:
187
+ # Get reflexive engine from request state
188
+ reflexive_engine: ReflexiveEngine = request.app.state.reflexive_engine
189
+
190
+ # Collect stats from all monitors
191
+ stats = {}
192
+
193
+ for monitor in reflexive_engine.monitors:
194
+ if hasattr(monitor, 'get_violation_stats'):
195
+ stats['policy_monitor'] = monitor.get_violation_stats()
196
+ elif hasattr(monitor, 'get_integrity_stats'):
197
+ stats['ledger_monitor'] = monitor.get_integrity_stats()
198
+ elif hasattr(monitor, 'get_anomaly_stats'):
199
+ stats['anomaly_detector'] = monitor.get_anomaly_stats()
200
+
201
+ return JSONResponse(
202
+ status_code=200,
203
+ content=stats
204
+ )
205
+
206
+ except Exception as e:
207
+ logger.error(f"Failed to get monitor stats: {e}")
208
+ return JSONResponse(
209
+ status_code=500,
210
+ content={
211
+ "error": "Failed to get monitor stats",
212
+ "reason": str(e)
213
+ }
214
+ )
215
+
216
+
217
+ async def create_risk_scenario_endpoint(request: Request) -> JSONResponse:
218
+ """HTTP endpoint for creating predefined risk scenarios.
219
+
220
+ Expected JSON body:
221
+ {
222
+ "scenario_name": "admin_privilege_escalation",
223
+ "scenario_type": "policy_violation",
224
+ "parameters": {
225
+ "actor_type": "guest_user",
226
+ "target_resource": "admin_panel",
227
+ "severity": "high"
228
+ }
229
+ }
230
+ """
231
+ try:
232
+ # Parse request body
233
+ body = await request.json()
234
+
235
+ scenario_name = body.get("scenario_name")
236
+ scenario_type = body.get("scenario_type")
237
+ parameters = body.get("parameters", {})
238
+
239
+ if not scenario_name or not scenario_type:
240
+ return JSONResponse(
241
+ status_code=400,
242
+ content={
243
+ "error": "Missing required fields: scenario_name, scenario_type"
244
+ }
245
+ )
246
+
247
+ # Create scenario based on type
248
+ scenario = _create_risk_scenario(scenario_name, scenario_type, parameters)
249
+
250
+ return JSONResponse(
251
+ status_code=200,
252
+ content={
253
+ "scenario": scenario,
254
+ "message": f"Risk scenario '{scenario_name}' created successfully"
255
+ }
256
+ )
257
+
258
+ except Exception as e:
259
+ logger.error(f"Failed to create risk scenario: {e}")
260
+ return JSONResponse(
261
+ status_code=500,
262
+ content={
263
+ "error": "Failed to create risk scenario",
264
+ "reason": str(e)
265
+ }
266
+ )
267
+
268
+
269
+ def _create_risk_scenario(scenario_name: str, scenario_type: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
270
+ """Create a predefined risk scenario."""
271
+ scenarios = {
272
+ "admin_privilege_escalation": {
273
+ "action_context": {
274
+ "action_id": f"admin_escalation_{scenario_name}",
275
+ "actor_id": parameters.get("actor_type", "guest_user"),
276
+ "action_type": "admin_access",
277
+ "resource_id": parameters.get("target_resource", "admin_panel"),
278
+ "metadata": {
279
+ "privilege_level": "admin",
280
+ "escalation_attempt": True
281
+ }
282
+ },
283
+ "expected_decision": "halt",
284
+ "expected_risk_level": "high"
285
+ },
286
+ "suspicious_activity": {
287
+ "action_context": {
288
+ "action_id": f"suspicious_{scenario_name}",
289
+ "actor_id": "suspicious_user",
290
+ "action_type": "data_access",
291
+ "resource_id": "sensitive_data",
292
+ "metadata": {
293
+ "access_pattern": "unusual",
294
+ "time_of_day": "off_hours"
295
+ }
296
+ },
297
+ "expected_decision": "escalate",
298
+ "expected_risk_level": "medium"
299
+ },
300
+ "integrity_violation": {
301
+ "action_context": {
302
+ "action_id": f"integrity_{scenario_name}",
303
+ "actor_id": "system",
304
+ "action_type": "ledger_modification",
305
+ "resource_id": "provenance_ledger",
306
+ "metadata": {
307
+ "modification_type": "unauthorized",
308
+ "integrity_check": "failed"
309
+ }
310
+ },
311
+ "expected_decision": "halt",
312
+ "expected_risk_level": "critical"
313
+ },
314
+ "rate_limit_exceeded": {
315
+ "action_context": {
316
+ "action_id": f"rate_limit_{scenario_name}",
317
+ "actor_id": "high_frequency_user",
318
+ "action_type": "api_call",
319
+ "resource_id": "api_endpoint",
320
+ "metadata": {
321
+ "request_count": 1000,
322
+ "time_window": "1_minute"
323
+ }
324
+ },
325
+ "expected_decision": "escalate",
326
+ "expected_risk_level": "medium"
327
+ }
328
+ }
329
+
330
+ return scenarios.get(scenario_name, {
331
+ "action_context": {
332
+ "action_id": f"custom_{scenario_name}",
333
+ "actor_id": "test_user",
334
+ "action_type": "custom_action",
335
+ "resource_id": "test_resource",
336
+ "metadata": parameters
337
+ },
338
+ "expected_decision": "monitor",
339
+ "expected_risk_level": "low"
340
+ })
341
+
342
+
343
+ def create_reflexive_routes(reflexive_engine: ReflexiveEngine) -> List[Route]:
344
+ """Create reflexive core routes.
345
+
346
+ Args:
347
+ reflexive_engine: The reflexive engine instance
348
+
349
+ Returns:
350
+ List of Starlette Route objects for reflexive core management
351
+ """
352
+ def endpoint_with_engine(endpoint_func):
353
+ async def wrapper(request: Request) -> JSONResponse:
354
+ # Store reflexive engine in app state for access in endpoint
355
+ request.app.state.reflexive_engine = reflexive_engine
356
+ return await endpoint_func(request)
357
+ return wrapper
358
+
359
+ return [
360
+ Route(
361
+ path="/core/simulate-risk",
362
+ endpoint=endpoint_with_engine(simulate_risk_endpoint),
363
+ methods=["POST"]
364
+ ),
365
+ Route(
366
+ path="/core/status",
367
+ endpoint=endpoint_with_engine(get_engine_status_endpoint),
368
+ methods=["GET"]
369
+ ),
370
+ Route(
371
+ path="/core/submit-action",
372
+ endpoint=endpoint_with_engine(submit_action_endpoint),
373
+ methods=["POST"]
374
+ ),
375
+ Route(
376
+ path="/core/monitor-stats",
377
+ endpoint=endpoint_with_engine(get_monitor_stats_endpoint),
378
+ methods=["GET"]
379
+ ),
380
+ Route(
381
+ path="/core/risk-scenario",
382
+ endpoint=endpoint_with_engine(create_risk_scenario_endpoint),
383
+ methods=["POST"]
384
+ )
385
+ ]
src/fastmcp/server/server.py CHANGED
@@ -64,6 +64,7 @@ from fastmcp.tools import ToolManager
64
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
65
  from fastmcp.tools.tool_transform import ToolTransformConfig
66
  from fastmcp.policy import PolicyEngine
 
67
  from fastmcp.utilities.cli import log_server_banner
68
  from fastmcp.utilities.components import FastMCPComponent
69
  from fastmcp.utilities.logging import get_logger
@@ -176,6 +177,7 @@ class FastMCP(Generic[LifespanResultT]):
176
  self._additional_http_routes: list[BaseRoute] = []
177
  self._mounted_servers: list[MountedServer] = []
178
  self._policy_engine: Optional[PolicyEngine] = None
 
179
  self._tool_manager = ToolManager(
180
  duplicate_behavior=on_duplicate_tools,
181
  mask_error_details=mask_error_details,
@@ -514,6 +516,14 @@ class FastMCP(Generic[LifespanResultT]):
514
  policy_route = create_policy_evaluate_route(self._policy_engine)
515
  routes.append(policy_route)
516
 
 
 
 
 
 
 
 
 
517
  # Recursively get routes from mounted servers
518
  for mounted_server in self._mounted_servers:
519
  mounted_routes = mounted_server.server._get_additional_http_routes()
@@ -545,6 +555,34 @@ class FastMCP(Generic[LifespanResultT]):
545
  """
546
  return self._policy_engine
547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  async def _mcp_list_tools(self) -> list[MCPTool]:
549
  logger.debug("Handler called: list_tools")
550
 
 
64
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
65
  from fastmcp.tools.tool_transform import ToolTransformConfig
66
  from fastmcp.policy import PolicyEngine
67
+ from fastmcp.reflexive import ReflexiveEngine
68
  from fastmcp.utilities.cli import log_server_banner
69
  from fastmcp.utilities.components import FastMCPComponent
70
  from fastmcp.utilities.logging import get_logger
 
177
  self._additional_http_routes: list[BaseRoute] = []
178
  self._mounted_servers: list[MountedServer] = []
179
  self._policy_engine: Optional[PolicyEngine] = None
180
+ self._reflexive_engine: Optional[ReflexiveEngine] = None
181
  self._tool_manager = ToolManager(
182
  duplicate_behavior=on_duplicate_tools,
183
  mask_error_details=mask_error_details,
 
516
  policy_route = create_policy_evaluate_route(self._policy_engine)
517
  routes.append(policy_route)
518
 
519
+
520
+ # Add reflexive core endpoints if reflexive engine is configured
521
+ if self._reflexive_engine is not None:
522
+ from fastmcp.server.reflexive_routes import create_reflexive_routes
523
+
524
+ reflexive_routes = create_reflexive_routes(self._reflexive_engine)
525
+ routes.extend(reflexive_routes)
526
+
527
  # Recursively get routes from mounted servers
528
  for mounted_server in self._mounted_servers:
529
  mounted_routes = mounted_server.server._get_additional_http_routes()
 
555
  """
556
  return self._policy_engine
557
 
558
+
559
+ def enable_reflexive_core(self, reflexive_engine: Optional[ReflexiveEngine] = None) -> ReflexiveEngine:
560
+ """Enable the reflexive core for this server.
561
+
562
+ Args:
563
+ reflexive_engine: Optional reflexive engine instance. If None, creates a new one.
564
+
565
+ Returns:
566
+ The reflexive engine instance
567
+ """
568
+ if reflexive_engine is None:
569
+ reflexive_engine = ReflexiveEngine(
570
+ policy_engine=self._policy_engine,
571
+ ledger=None
572
+ )
573
+
574
+ self._reflexive_engine = reflexive_engine
575
+ logger.info("Reflexive core enabled for server")
576
+ return reflexive_engine
577
+
578
+ def get_reflexive_engine(self) -> Optional[ReflexiveEngine]:
579
+ """Get the reflexive engine instance.
580
+
581
+ Returns:
582
+ The reflexive engine instance, or None if not enabled
583
+ """
584
+ return self._reflexive_engine
585
+
586
  async def _mcp_list_tools(self) -> list[MCPTool]:
587
  logger.debug("Handler called: list_tools")
588
 
tests/reflexive/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Tests for the reflexive core module."""
tests/reflexive/test_actions.py ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the reflexive actions."""
2
+
3
+ import pytest
4
+ from datetime import datetime
5
+ from unittest.mock import Mock
6
+
7
+ from fastmcp.reflexive.actions import (
8
+ HaltAction, EscalateAction, MonitorAction, AllowAction,
9
+ ActionFactory, ActionExecutor
10
+ )
11
+ from fastmcp.reflexive.engine import ReflexiveDecision, ActionContext, DecisionType, RiskLevel
12
+
13
+
14
+ class TestHaltAction:
15
+ """Test the HaltAction class."""
16
+
17
+ @pytest.fixture
18
+ def halt_decision(self):
19
+ """Create a halt decision for testing."""
20
+ action_context = ActionContext(
21
+ action_id="test_action",
22
+ actor_id="test_user",
23
+ action_type="admin_access"
24
+ )
25
+
26
+ return ReflexiveDecision(
27
+ decision_type=DecisionType.HALT,
28
+ risk_level=RiskLevel.HIGH,
29
+ action_context=action_context,
30
+ reason="Unauthorized admin access attempt"
31
+ )
32
+
33
+ @pytest.fixture
34
+ def halt_action(self, halt_decision):
35
+ """Create a halt action for testing."""
36
+ return HaltAction(
37
+ decision=halt_decision,
38
+ halt_reason="Security violation detected",
39
+ halt_level="immediate",
40
+ affected_operations=["admin_access", "user_management"]
41
+ )
42
+
43
+ def test_halt_action_creation(self, halt_action, halt_decision):
44
+ """Test halt action creation."""
45
+ assert halt_action.decision == halt_decision
46
+ assert halt_action.halt_reason == "Security violation detected"
47
+ assert halt_action.halt_level == "immediate"
48
+ assert halt_action.affected_operations == ["admin_access", "user_management"]
49
+ assert halt_action.status == "pending"
50
+ assert halt_action.result is None
51
+
52
+ async def test_halt_action_execution(self, halt_action):
53
+ """Test halt action execution."""
54
+ result = await halt_action.execute()
55
+
56
+ assert halt_action.status == "completed"
57
+ assert halt_action.result is not None
58
+ assert result["halted_operations"] == ["admin_access", "user_management"]
59
+ assert result["halt_reason"] == "Security violation detected"
60
+ assert result["halt_level"] == "immediate"
61
+ assert "halt_timestamp" in result
62
+ assert "decision_id" in result
63
+
64
+ def test_halt_action_type(self, halt_action):
65
+ """Test halt action type."""
66
+ assert halt_action.get_action_type() == "halt"
67
+
68
+
69
+ class TestEscalateAction:
70
+ """Test the EscalateAction class."""
71
+
72
+ @pytest.fixture
73
+ def escalate_decision(self):
74
+ """Create an escalate decision for testing."""
75
+ action_context = ActionContext(
76
+ action_id="test_action",
77
+ actor_id="test_user",
78
+ action_type="data_access"
79
+ )
80
+
81
+ return ReflexiveDecision(
82
+ decision_type=DecisionType.ESCALATE,
83
+ risk_level=RiskLevel.MEDIUM,
84
+ action_context=action_context,
85
+ reason="Suspicious data access pattern"
86
+ )
87
+
88
+ @pytest.fixture
89
+ def escalate_action(self, escalate_decision):
90
+ """Create an escalate action for testing."""
91
+ return EscalateAction(
92
+ decision=escalate_decision,
93
+ escalation_target="security_team",
94
+ escalation_priority="high",
95
+ escalation_context={"alert_level": "medium"},
96
+ notification_channels=["email", "slack"]
97
+ )
98
+
99
+ def test_escalate_action_creation(self, escalate_action, escalate_decision):
100
+ """Test escalate action creation."""
101
+ assert escalate_action.decision == escalate_decision
102
+ assert escalate_action.escalation_target == "security_team"
103
+ assert escalate_action.escalation_priority == "high"
104
+ assert escalate_action.escalation_context == {"alert_level": "medium"}
105
+ assert escalate_action.notification_channels == ["email", "slack"]
106
+ assert escalate_action.status == "pending"
107
+ assert escalate_action.result is None
108
+
109
+ async def test_escalate_action_execution(self, escalate_action):
110
+ """Test escalate action execution."""
111
+ result = await escalate_action.execute()
112
+
113
+ assert escalate_action.status == "completed"
114
+ assert escalate_action.result is not None
115
+ assert result["escalation_target"] == "security_team"
116
+ assert result["escalation_priority"] == "high"
117
+ assert result["escalation_context"] == {"alert_level": "medium"}
118
+ assert result["notification_channels"] == ["email", "slack"]
119
+ assert "escalation_timestamp" in result
120
+ assert "decision_id" in result
121
+ assert "action_context" in result
122
+
123
+ def test_escalate_action_type(self, escalate_action):
124
+ """Test escalate action type."""
125
+ assert escalate_action.get_action_type() == "escalate"
126
+
127
+
128
+ class TestMonitorAction:
129
+ """Test the MonitorAction class."""
130
+
131
+ @pytest.fixture
132
+ def monitor_decision(self):
133
+ """Create a monitor decision for testing."""
134
+ action_context = ActionContext(
135
+ action_id="test_action",
136
+ actor_id="test_user",
137
+ action_type="api_call"
138
+ )
139
+
140
+ return ReflexiveDecision(
141
+ decision_type=DecisionType.MONITOR,
142
+ risk_level=RiskLevel.LOW,
143
+ action_context=action_context,
144
+ reason="Unusual but not suspicious activity"
145
+ )
146
+
147
+ @pytest.fixture
148
+ def monitor_action(self, monitor_decision):
149
+ """Create a monitor action for testing."""
150
+ return MonitorAction(
151
+ decision=monitor_decision,
152
+ monitoring_level="enhanced",
153
+ monitoring_duration=1800,
154
+ monitoring_scope=["test_user", "api_calls"]
155
+ )
156
+
157
+ def test_monitor_action_creation(self, monitor_action, monitor_decision):
158
+ """Test monitor action creation."""
159
+ assert monitor_action.decision == monitor_decision
160
+ assert monitor_action.monitoring_level == "enhanced"
161
+ assert monitor_action.monitoring_duration == 1800
162
+ assert monitor_action.monitoring_scope == ["test_user", "api_calls"]
163
+ assert monitor_action.status == "pending"
164
+ assert monitor_action.result is None
165
+
166
+ async def test_monitor_action_execution(self, monitor_action):
167
+ """Test monitor action execution."""
168
+ result = await monitor_action.execute()
169
+
170
+ assert monitor_action.status == "completed"
171
+ assert monitor_action.result is not None
172
+ assert result["monitoring_level"] == "enhanced"
173
+ assert result["monitoring_duration"] == 1800
174
+ assert result["monitoring_scope"] == ["test_user", "api_calls"]
175
+ assert "monitoring_timestamp" in result
176
+ assert "decision_id" in result
177
+ assert "action_context" in result
178
+
179
+ def test_monitor_action_type(self, monitor_action):
180
+ """Test monitor action type."""
181
+ assert monitor_action.get_action_type() == "monitor"
182
+
183
+
184
+ class TestAllowAction:
185
+ """Test the AllowAction class."""
186
+
187
+ @pytest.fixture
188
+ def allow_decision(self):
189
+ """Create an allow decision for testing."""
190
+ action_context = ActionContext(
191
+ action_id="test_action",
192
+ actor_id="test_user",
193
+ action_type="normal_operation"
194
+ )
195
+
196
+ return ReflexiveDecision(
197
+ decision_type=DecisionType.ALLOW,
198
+ risk_level=RiskLevel.LOW,
199
+ action_context=action_context,
200
+ reason="No violations or anomalies detected"
201
+ )
202
+
203
+ @pytest.fixture
204
+ def allow_action(self, allow_decision):
205
+ """Create an allow action for testing."""
206
+ return AllowAction(
207
+ decision=allow_decision,
208
+ allow_conditions=["authenticated", "authorized"],
209
+ allow_restrictions=["rate_limited"]
210
+ )
211
+
212
+ def test_allow_action_creation(self, allow_action, allow_decision):
213
+ """Test allow action creation."""
214
+ assert allow_action.decision == allow_decision
215
+ assert allow_action.allow_conditions == ["authenticated", "authorized"]
216
+ assert allow_action.allow_restrictions == ["rate_limited"]
217
+ assert allow_action.status == "pending"
218
+ assert allow_action.result is None
219
+
220
+ async def test_allow_action_execution(self, allow_action):
221
+ """Test allow action execution."""
222
+ result = await allow_action.execute()
223
+
224
+ assert allow_action.status == "completed"
225
+ assert allow_action.result is not None
226
+ assert result["allowed"] is True
227
+ assert result["allow_conditions"] == ["authenticated", "authorized"]
228
+ assert result["allow_restrictions"] == ["rate_limited"]
229
+ assert "allow_timestamp" in result
230
+ assert "decision_id" in result
231
+ assert "action_context" in result
232
+
233
+ def test_allow_action_type(self, allow_action):
234
+ """Test allow action type."""
235
+ assert allow_action.get_action_type() == "allow"
236
+
237
+
238
+ class TestActionFactory:
239
+ """Test the ActionFactory class."""
240
+
241
+ @pytest.fixture
242
+ def halt_decision(self):
243
+ """Create a halt decision for testing."""
244
+ action_context = ActionContext(
245
+ action_id="test_action",
246
+ actor_id="test_user",
247
+ action_type="admin_access"
248
+ )
249
+
250
+ return ReflexiveDecision(
251
+ decision_type=DecisionType.HALT,
252
+ risk_level=RiskLevel.HIGH,
253
+ action_context=action_context,
254
+ reason="Security violation"
255
+ )
256
+
257
+ @pytest.fixture
258
+ def escalate_decision(self):
259
+ """Create an escalate decision for testing."""
260
+ action_context = ActionContext(
261
+ action_id="test_action",
262
+ actor_id="test_user",
263
+ action_type="data_access"
264
+ )
265
+
266
+ return ReflexiveDecision(
267
+ decision_type=DecisionType.ESCALATE,
268
+ risk_level=RiskLevel.MEDIUM,
269
+ action_context=action_context,
270
+ reason="Suspicious activity"
271
+ )
272
+
273
+ @pytest.fixture
274
+ def monitor_decision(self):
275
+ """Create a monitor decision for testing."""
276
+ action_context = ActionContext(
277
+ action_id="test_action",
278
+ actor_id="test_user",
279
+ action_type="api_call"
280
+ )
281
+
282
+ return ReflexiveDecision(
283
+ decision_type=DecisionType.MONITOR,
284
+ risk_level=RiskLevel.LOW,
285
+ action_context=action_context,
286
+ reason="Unusual activity"
287
+ )
288
+
289
+ @pytest.fixture
290
+ def allow_decision(self):
291
+ """Create an allow decision for testing."""
292
+ action_context = ActionContext(
293
+ action_id="test_action",
294
+ actor_id="test_user",
295
+ action_type="normal_operation"
296
+ )
297
+
298
+ return ReflexiveDecision(
299
+ decision_type=DecisionType.ALLOW,
300
+ risk_level=RiskLevel.LOW,
301
+ action_context=action_context,
302
+ reason="No issues detected"
303
+ )
304
+
305
+ def test_create_halt_action(self, halt_decision):
306
+ """Test creating a halt action."""
307
+ action = ActionFactory.create_action(halt_decision)
308
+
309
+ assert isinstance(action, HaltAction)
310
+ assert action.decision == halt_decision
311
+ assert action.halt_reason == halt_decision.reason
312
+ assert action.halt_level == "immediate"
313
+ assert halt_decision.action_context.action_id in action.affected_operations
314
+
315
+ def test_create_escalate_action(self, escalate_decision):
316
+ """Test creating an escalate action."""
317
+ action = ActionFactory.create_action(escalate_decision)
318
+
319
+ assert isinstance(action, EscalateAction)
320
+ assert action.decision == escalate_decision
321
+ assert action.escalation_target == "default_admin"
322
+ assert action.escalation_priority == "normal"
323
+
324
+ def test_create_monitor_action(self, monitor_decision):
325
+ """Test creating a monitor action."""
326
+ action = ActionFactory.create_action(monitor_decision)
327
+
328
+ assert isinstance(action, MonitorAction)
329
+ assert action.decision == monitor_decision
330
+ assert action.monitoring_level == "enhanced"
331
+ assert action.monitoring_duration == 3600
332
+
333
+ def test_create_allow_action(self, allow_decision):
334
+ """Test creating an allow action."""
335
+ action = ActionFactory.create_action(allow_decision)
336
+
337
+ assert isinstance(action, AllowAction)
338
+ assert action.decision == allow_decision
339
+ assert action.allow_conditions == []
340
+ assert action.allow_restrictions == []
341
+
342
+ def test_create_action_with_kwargs(self, halt_decision):
343
+ """Test creating an action with additional kwargs."""
344
+ action = ActionFactory.create_action(
345
+ halt_decision,
346
+ halt_level="graceful",
347
+ affected_operations=["operation1", "operation2"]
348
+ )
349
+
350
+ assert isinstance(action, HaltAction)
351
+ assert action.halt_level == "graceful"
352
+ assert action.affected_operations == ["operation1", "operation2"]
353
+
354
+ def test_create_action_unknown_type(self, halt_decision):
355
+ """Test creating an action with unknown decision type."""
356
+ halt_decision.decision_type = "unknown_type" # type: ignore
357
+
358
+ with pytest.raises(ValueError, match="Unknown decision type"):
359
+ ActionFactory.create_action(halt_decision)
360
+
361
+
362
+ class TestActionExecutor:
363
+ """Test the ActionExecutor class."""
364
+
365
+ @pytest.fixture
366
+ def action_executor(self):
367
+ """Create an action executor for testing."""
368
+ return ActionExecutor()
369
+
370
+ @pytest.fixture
371
+ def halt_action(self):
372
+ """Create a halt action for testing."""
373
+ action_context = ActionContext(
374
+ action_id="test_action",
375
+ actor_id="test_user",
376
+ action_type="admin_access"
377
+ )
378
+
379
+ decision = ReflexiveDecision(
380
+ decision_type=DecisionType.HALT,
381
+ risk_level=RiskLevel.HIGH,
382
+ action_context=action_context,
383
+ reason="Security violation"
384
+ )
385
+
386
+ return HaltAction(
387
+ decision=decision,
388
+ halt_reason="Unauthorized access",
389
+ halt_level="immediate",
390
+ affected_operations=[action_context.action_id]
391
+ )
392
+
393
+ async def test_execute_action_success(self, action_executor, halt_action):
394
+ """Test successful action execution."""
395
+ result = await action_executor.execute_action(halt_action)
396
+
397
+ assert result["halted_operations"] == [halt_action.decision.action_context.action_id]
398
+ assert result["halt_reason"] == "Unauthorized access"
399
+ assert result["halt_level"] == "immediate"
400
+
401
+ # Check execution history
402
+ assert len(action_executor.execution_history) == 1
403
+ history_record = action_executor.execution_history[0]
404
+ assert history_record["action_id"] == str(halt_action.action_id)
405
+ assert history_record["action_type"] == "halt"
406
+ assert history_record["status"] == "completed"
407
+ assert "end_time" in history_record
408
+ assert "result" in history_record
409
+
410
+ async def test_execute_action_failure(self, action_executor):
411
+ """Test action execution failure."""
412
+ # Create a mock action that raises an exception
413
+ mock_action = Mock()
414
+ mock_action.action_id = "test_action_id"
415
+ mock_action.decision = Mock()
416
+ mock_action.decision.decision_id = "test_decision_id"
417
+ mock_action.execute.side_effect = Exception("Execution failed")
418
+ mock_action.get_action_type.return_value = "test_type"
419
+ mock_action.timestamp = datetime.utcnow()
420
+ mock_action.status = "pending"
421
+
422
+ with pytest.raises(Exception, match="Execution failed"):
423
+ await action_executor.execute_action(mock_action)
424
+
425
+ # Check execution history
426
+ assert len(action_executor.execution_history) == 1
427
+ history_record = action_executor.execution_history[0]
428
+ assert history_record["action_id"] == "test_action_id"
429
+ assert history_record["status"] == "failed"
430
+ assert "error" in history_record
431
+ assert history_record["error"] == "Execution failed"
432
+
433
+ def test_get_execution_stats(self, action_executor):
434
+ """Test getting execution statistics."""
435
+ # Add some execution history
436
+ action_executor.execution_history = [
437
+ {"action_id": "1", "status": "completed"},
438
+ {"action_id": "2", "status": "completed"},
439
+ {"action_id": "3", "status": "failed"},
440
+ ]
441
+
442
+ stats = action_executor.get_execution_stats()
443
+
444
+ assert stats["total_actions"] == 3
445
+ assert stats["completed_actions"] == 2
446
+ assert stats["failed_actions"] == 1
447
+ assert stats["active_actions"] == 0
448
+ assert stats["success_rate"] == 2/3
449
+
450
+ def test_active_actions_tracking(self, action_executor, halt_action):
451
+ """Test active actions tracking."""
452
+ # Start execution (this would normally be async)
453
+ action_executor.active_actions[str(halt_action.action_id)] = halt_action
454
+
455
+ assert len(action_executor.active_actions) == 1
456
+ assert str(halt_action.action_id) in action_executor.active_actions
457
+
458
+ # Simulate completion
459
+ del action_executor.active_actions[str(halt_action.action_id)]
460
+
461
+ assert len(action_executor.active_actions) == 0
tests/reflexive/test_monitors.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the reflexive monitors."""
2
+
3
+ import pytest
4
+ from datetime import datetime, timedelta
5
+ from unittest.mock import Mock
6
+
7
+ from fastmcp.reflexive.monitor import PolicyMonitor, LedgerMonitor, AnomalyDetector
8
+ from fastmcp.reflexive.engine import ActionContext
9
+
10
+
11
+ class TestPolicyMonitor:
12
+ """Test the PolicyMonitor class."""
13
+
14
+ @pytest.fixture
15
+ def policy_monitor(self):
16
+ """Create a policy monitor for testing."""
17
+ return PolicyMonitor()
18
+
19
+ @pytest.fixture
20
+ def action_context(self):
21
+ """Create an action context for testing."""
22
+ return ActionContext(
23
+ action_id="test_action",
24
+ actor_id="test_user",
25
+ action_type="tool_call",
26
+ resource_id="test_resource"
27
+ )
28
+
29
+ async def test_monitor_no_violations(self, policy_monitor, action_context):
30
+ """Test monitoring with no violations."""
31
+ result = await policy_monitor(action_context)
32
+ assert result is None
33
+
34
+ async def test_monitor_admin_access_violation(self, policy_monitor, action_context):
35
+ """Test monitoring admin access violation."""
36
+ # Create action context with admin access by guest user
37
+ admin_context = ActionContext(
38
+ action_id="admin_action",
39
+ actor_id="guest_user",
40
+ action_type="admin_access",
41
+ resource_id="admin_panel"
42
+ )
43
+
44
+ result = await policy_monitor(admin_context)
45
+
46
+ assert result is not None
47
+ assert result["type"] == "violation"
48
+ assert result["severity"] == "high"
49
+ assert len(result["violations"]) == 1
50
+ assert result["violations"][0]["rule"] == "admin_access_restriction"
51
+
52
+ async def test_monitor_rate_limit_violation(self, policy_monitor, action_context):
53
+ """Test monitoring rate limit violation."""
54
+ # Add multiple violations for the same actor
55
+ for i in range(5):
56
+ violation_context = ActionContext(
57
+ action_id=f"action_{i}",
58
+ actor_id="rate_limit_user",
59
+ action_type="api_call"
60
+ )
61
+
62
+ # Manually add violations to history
63
+ policy_monitor.violation_history.append({
64
+ "type": "violation",
65
+ "severity": "low",
66
+ "actor_id": "rate_limit_user",
67
+ "action_id": f"action_{i}",
68
+ "timestamp": datetime.utcnow().isoformat()
69
+ })
70
+
71
+ # Test rate limit violation
72
+ result = await policy_monitor(action_context)
73
+
74
+ # Should not trigger rate limit for different actor
75
+ assert result is None
76
+
77
+ # Test with same actor
78
+ rate_limit_context = ActionContext(
79
+ action_id="rate_limit_action",
80
+ actor_id="rate_limit_user",
81
+ action_type="api_call"
82
+ )
83
+
84
+ result = await policy_monitor(rate_limit_context)
85
+
86
+ assert result is not None
87
+ assert result["type"] == "violation"
88
+ assert result["severity"] == "medium"
89
+ assert len(result["violations"]) == 1
90
+ assert result["violations"][0]["rule"] == "rate_limit_exceeded"
91
+
92
+ async def test_monitor_sensitive_resource_violation(self, policy_monitor, action_context):
93
+ """Test monitoring sensitive resource access violation."""
94
+ sensitive_context = ActionContext(
95
+ action_id="sensitive_action",
96
+ actor_id="unauthorized_user",
97
+ action_type="data_access",
98
+ resource_id="sensitive_data",
99
+ metadata={"authorized": False}
100
+ )
101
+
102
+ result = await policy_monitor(sensitive_context)
103
+
104
+ assert result is not None
105
+ assert result["type"] == "violation"
106
+ assert result["severity"] == "critical"
107
+ assert len(result["violations"]) == 1
108
+ assert result["violations"][0]["rule"] == "unauthorized_sensitive_access"
109
+
110
+ def test_assess_violation_severity(self, policy_monitor):
111
+ """Test violation severity assessment."""
112
+ # Test critical severity
113
+ critical_violations = [{"severity": "critical"}]
114
+ assert policy_monitor._assess_violation_severity(critical_violations) == "critical"
115
+
116
+ # Test high severity
117
+ high_violations = [{"severity": "high"}]
118
+ assert policy_monitor._assess_violation_severity(high_violations) == "high"
119
+
120
+ # Test medium severity
121
+ medium_violations = [{"severity": "medium"}]
122
+ assert policy_monitor._assess_violation_severity(medium_violations) == "medium"
123
+
124
+ # Test low severity
125
+ low_violations = [{"severity": "low"}]
126
+ assert policy_monitor._assess_violation_severity(low_violations) == "low"
127
+
128
+ # Test mixed severities
129
+ mixed_violations = [{"severity": "low"}, {"severity": "high"}]
130
+ assert policy_monitor._assess_violation_severity(mixed_violations) == "high"
131
+
132
+ def test_get_violation_stats(self, policy_monitor):
133
+ """Test getting violation statistics."""
134
+ # Add some violations
135
+ policy_monitor.violation_history.append({
136
+ "type": "violation",
137
+ "severity": "high",
138
+ "actor_id": "user1",
139
+ "action_id": "action1",
140
+ "timestamp": datetime.utcnow().isoformat()
141
+ })
142
+
143
+ policy_monitor.actor_violations["user1"] = 1
144
+ policy_monitor.actor_violations["user2"] = 2
145
+
146
+ stats = policy_monitor.get_violation_stats()
147
+
148
+ assert stats["total_violations"] == 1
149
+ assert stats["actor_violations"]["user1"] == 1
150
+ assert stats["actor_violations"]["user2"] == 2
151
+ assert "recent_violations" in stats
152
+
153
+
154
+ class TestLedgerMonitor:
155
+ """Test the LedgerMonitor class."""
156
+
157
+ @pytest.fixture
158
+ def ledger_monitor(self):
159
+ """Create a ledger monitor for testing."""
160
+ return LedgerMonitor()
161
+
162
+ @pytest.fixture
163
+ def mock_ledger(self):
164
+ """Create a mock ledger for testing."""
165
+ mock_ledger = Mock()
166
+ mock_ledger.verify_chain_integrity.return_value = True
167
+ mock_ledger.get_ledger_statistics.return_value = {
168
+ "total_entries": 10,
169
+ "total_blocks": 2
170
+ }
171
+ return mock_ledger
172
+
173
+ @pytest.fixture
174
+ def action_context(self):
175
+ """Create an action context for testing."""
176
+ return ActionContext(
177
+ action_id="test_action",
178
+ actor_id="test_user",
179
+ action_type="tool_call"
180
+ )
181
+
182
+ async def test_monitor_no_issues(self, ledger_monitor, mock_ledger, action_context):
183
+ """Test monitoring with no ledger issues."""
184
+ ledger_monitor.ledger = mock_ledger
185
+
186
+ result = await ledger_monitor(action_context)
187
+ assert result is None
188
+
189
+ async def test_monitor_chain_integrity_issue(self, ledger_monitor, mock_ledger, action_context):
190
+ """Test monitoring chain integrity issue."""
191
+ # Mock chain integrity failure
192
+ mock_ledger.verify_chain_integrity.return_value = False
193
+ ledger_monitor.ledger = mock_ledger
194
+
195
+ result = await ledger_monitor(action_context)
196
+
197
+ assert result is not None
198
+ assert result["type"] == "anomaly"
199
+ assert result["severity"] == "critical"
200
+ assert len(result["issues"]) == 1
201
+ assert result["issues"][0]["type"] == "chain_integrity"
202
+
203
+ async def test_monitor_missing_blocks_issue(self, ledger_monitor, mock_ledger, action_context):
204
+ """Test monitoring missing blocks issue."""
205
+ # Mock missing blocks scenario
206
+ mock_ledger.get_ledger_statistics.return_value = {
207
+ "total_entries": 10,
208
+ "total_blocks": 0
209
+ }
210
+ ledger_monitor.ledger = mock_ledger
211
+
212
+ result = await ledger_monitor(action_context)
213
+
214
+ assert result is not None
215
+ assert result["type"] == "anomaly"
216
+ assert result["severity"] == "high"
217
+ assert len(result["issues"]) == 1
218
+ assert result["issues"][0]["type"] == "missing_blocks"
219
+
220
+ async def test_monitor_integrity_check_error(self, ledger_monitor, action_context):
221
+ """Test monitoring with integrity check error."""
222
+ # Mock ledger that raises exception
223
+ mock_ledger = Mock()
224
+ mock_ledger.verify_chain_integrity.side_effect = Exception("Database error")
225
+ ledger_monitor.ledger = mock_ledger
226
+
227
+ result = await ledger_monitor(action_context)
228
+
229
+ assert result is not None
230
+ assert result["type"] == "anomaly"
231
+ assert result["severity"] == "medium"
232
+ assert len(result["issues"]) == 1
233
+ assert result["issues"][0]["type"] == "integrity_check_error"
234
+
235
+ def test_assess_integrity_severity(self, ledger_monitor):
236
+ """Test integrity severity assessment."""
237
+ # Test critical severity
238
+ critical_issues = [{"severity": "critical"}]
239
+ assert ledger_monitor._assess_integrity_severity(critical_issues) == "critical"
240
+
241
+ # Test high severity
242
+ high_issues = [{"severity": "high"}]
243
+ assert ledger_monitor._assess_integrity_severity(high_issues) == "high"
244
+
245
+ # Test medium severity
246
+ medium_issues = [{"severity": "medium"}]
247
+ assert ledger_monitor._assess_integrity_severity(medium_issues) == "medium"
248
+
249
+ # Test low severity
250
+ low_issues = [{"severity": "low"}]
251
+ assert ledger_monitor._assess_integrity_severity(low_issues) == "low"
252
+
253
+ def test_get_integrity_stats(self, ledger_monitor):
254
+ """Test getting integrity statistics."""
255
+ # Add some integrity checks
256
+ ledger_monitor.integrity_checks.append({
257
+ "type": "anomaly",
258
+ "severity": "high",
259
+ "timestamp": datetime.utcnow().isoformat()
260
+ })
261
+
262
+ stats = ledger_monitor.get_integrity_stats()
263
+
264
+ assert stats["total_checks"] == 1
265
+ assert "recent_issues" in stats
266
+
267
+
268
+ class TestAnomalyDetector:
269
+ """Test the AnomalyDetector class."""
270
+
271
+ @pytest.fixture
272
+ def anomaly_detector(self):
273
+ """Create an anomaly detector for testing."""
274
+ return AnomalyDetector()
275
+
276
+ @pytest.fixture
277
+ def action_context(self):
278
+ """Create an action context for testing."""
279
+ return ActionContext(
280
+ action_id="test_action",
281
+ actor_id="test_user",
282
+ action_type="tool_call",
283
+ resource_id="test_resource"
284
+ )
285
+
286
+ async def test_detector_no_anomalies(self, anomaly_detector, action_context):
287
+ """Test detection with no anomalies."""
288
+ # First access to a resource will be flagged as new resource access
289
+ # So we need to access the resource twice to avoid the "new resource" anomaly
290
+ await anomaly_detector(action_context)
291
+ result = await anomaly_detector(action_context)
292
+ assert result is None
293
+
294
+ async def test_detector_high_frequency_anomaly(self, anomaly_detector, action_context):
295
+ """Test detection of high frequency anomaly."""
296
+ # Add many recent actions for the same actor
297
+ for i in range(25):
298
+ recent_context = ActionContext(
299
+ action_id=f"action_{i}",
300
+ actor_id="high_frequency_user",
301
+ action_type="api_call"
302
+ )
303
+ anomaly_detector._update_patterns(recent_context)
304
+
305
+ # Test high frequency detection
306
+ high_freq_context = ActionContext(
307
+ action_id="high_freq_action",
308
+ actor_id="high_frequency_user",
309
+ action_type="api_call"
310
+ )
311
+
312
+ result = await anomaly_detector(high_freq_context)
313
+
314
+ assert result is not None
315
+ assert result["type"] == "anomaly"
316
+ assert result["severity"] == "medium"
317
+ assert len(result["anomalies"]) == 1
318
+ assert result["anomalies"][0]["type"] == "high_frequency"
319
+
320
+ async def test_detector_unusual_timing_anomaly(self, anomaly_detector, action_context):
321
+ """Test detection of unusual timing anomaly."""
322
+ # Create action at unusual hour with new action type
323
+ unusual_context = ActionContext(
324
+ action_id="unusual_action",
325
+ actor_id="test_user",
326
+ action_type="new_action_type" # New action type
327
+ )
328
+ unusual_context.timestamp = datetime.utcnow().replace(hour=3) # 3 AM
329
+
330
+ result = await anomaly_detector(unusual_context)
331
+
332
+ assert result is not None
333
+ assert result["type"] == "anomaly"
334
+ assert result["severity"] == "low"
335
+ assert len(result["anomalies"]) == 1
336
+ assert result["anomalies"][0]["type"] == "unusual_timing"
337
+
338
+ async def test_detector_new_resource_access_anomaly(self, anomaly_detector, action_context):
339
+ """Test detection of new resource access anomaly."""
340
+ # First access to a resource
341
+ new_resource_context = ActionContext(
342
+ action_id="new_resource_action",
343
+ actor_id="test_user",
344
+ action_type="data_access",
345
+ resource_id="new_resource"
346
+ )
347
+
348
+ result = await anomaly_detector(new_resource_context)
349
+
350
+ assert result is not None
351
+ assert result["type"] == "anomaly"
352
+ assert result["severity"] == "low"
353
+ assert len(result["anomalies"]) == 1
354
+ assert result["anomalies"][0]["type"] == "new_resource_access"
355
+
356
+ async def test_detector_privilege_escalation_anomaly(self, anomaly_detector, action_context):
357
+ """Test detection of privilege escalation anomaly."""
358
+ # First time performing privileged action
359
+ privilege_context = ActionContext(
360
+ action_id="privilege_action",
361
+ actor_id="test_user",
362
+ action_type="admin_access",
363
+ resource_id="admin_panel"
364
+ )
365
+
366
+ result = await anomaly_detector(privilege_context)
367
+
368
+ assert result is not None
369
+ assert result["type"] == "anomaly"
370
+ assert result["severity"] == "high"
371
+ # Should detect both new resource access and privilege escalation
372
+ assert len(result["anomalies"]) >= 1
373
+ # Check that privilege escalation is detected
374
+ privilege_anomalies = [a for a in result["anomalies"] if a["type"] == "privilege_escalation"]
375
+ assert len(privilege_anomalies) == 1
376
+ assert privilege_anomalies[0]["type"] == "privilege_escalation"
377
+
378
+ def test_update_patterns(self, anomaly_detector, action_context):
379
+ """Test pattern updating."""
380
+ # Update patterns
381
+ anomaly_detector._update_patterns(action_context)
382
+
383
+ # Check that patterns were updated
384
+ actor_data = anomaly_detector.actor_patterns[action_context.actor_id]
385
+ assert actor_data["action_counts"][action_context.action_type] == 1
386
+ assert actor_data["resource_access"][action_context.resource_id] == 1
387
+ assert len(actor_data["session_times"]) == 1
388
+ assert actor_data["last_seen"] == action_context.timestamp
389
+
390
+ # Check global patterns
391
+ assert anomaly_detector.global_patterns["action_frequency"][action_context.action_type] == 1
392
+ assert anomaly_detector.global_patterns["resource_access"][action_context.resource_id] == 1
393
+
394
+ def test_assess_anomaly_severity(self, anomaly_detector):
395
+ """Test anomaly severity assessment."""
396
+ # Test high severity
397
+ high_anomalies = [{"severity": "high"}]
398
+ assert anomaly_detector._assess_anomaly_severity(high_anomalies) == "high"
399
+
400
+ # Test medium severity
401
+ medium_anomalies = [{"severity": "medium"}]
402
+ assert anomaly_detector._assess_anomaly_severity(medium_anomalies) == "medium"
403
+
404
+ # Test low severity
405
+ low_anomalies = [{"severity": "low"}]
406
+ assert anomaly_detector._assess_anomaly_severity(low_anomalies) == "low"
407
+
408
+ # Test mixed severities
409
+ mixed_anomalies = [{"severity": "low"}, {"severity": "high"}]
410
+ assert anomaly_detector._assess_anomaly_severity(mixed_anomalies) == "high"
411
+
412
+ def test_get_anomaly_stats(self, anomaly_detector, action_context):
413
+ """Test getting anomaly statistics."""
414
+ # Update patterns for some actors
415
+ anomaly_detector._update_patterns(action_context)
416
+
417
+ another_context = ActionContext(
418
+ action_id="another_action",
419
+ actor_id="another_user",
420
+ action_type="another_type"
421
+ )
422
+ anomaly_detector._update_patterns(another_context)
423
+
424
+ stats = anomaly_detector.get_anomaly_stats()
425
+
426
+ assert stats["tracked_actors"] == 2
427
+ assert stats["global_action_types"] == 2
428
+ assert stats["global_resources"] == 1
tests/reflexive/test_reflexive_engine.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the reflexive engine."""
2
+
3
+ import pytest
4
+ from datetime import datetime
5
+ from unittest.mock import Mock, AsyncMock
6
+
7
+ from fastmcp.reflexive import ReflexiveEngine, ActionContext, DecisionType, RiskLevel
8
+ from fastmcp.reflexive.engine import ReflexiveDecision
9
+
10
+
11
+ class TestReflexiveEngine:
12
+ """Test the reflexive engine functionality."""
13
+
14
+ @pytest.fixture
15
+ def reflexive_engine(self):
16
+ """Create a reflexive engine for testing."""
17
+ return ReflexiveEngine()
18
+
19
+ @pytest.fixture
20
+ def action_context(self):
21
+ """Create an action context for testing."""
22
+ return ActionContext(
23
+ action_id="test_action_123",
24
+ actor_id="test_user",
25
+ action_type="tool_call",
26
+ resource_id="test_resource",
27
+ metadata={"test": "data"}
28
+ )
29
+
30
+ def test_engine_initialization(self, reflexive_engine):
31
+ """Test reflexive engine initialization."""
32
+ assert reflexive_engine.policy_engine is None
33
+ assert reflexive_engine.ledger is None
34
+ assert len(reflexive_engine.monitors) == 0
35
+ assert len(reflexive_engine.decision_handlers) == 4
36
+ assert not reflexive_engine.is_running
37
+
38
+ def test_engine_status(self, reflexive_engine):
39
+ """Test getting engine status."""
40
+ status = reflexive_engine.get_engine_status()
41
+
42
+ assert "is_running" in status
43
+ assert "monitor_count" in status
44
+ assert "queue_size" in status
45
+ assert "decision_handlers" in status
46
+
47
+ assert status["is_running"] is False
48
+ assert status["monitor_count"] == 0
49
+ assert len(status["decision_handlers"]) == 4
50
+
51
+ def test_add_remove_monitor(self, reflexive_engine):
52
+ """Test adding and removing monitors."""
53
+ def test_monitor(context):
54
+ return None
55
+
56
+ # Add monitor
57
+ reflexive_engine.add_monitor(test_monitor)
58
+ assert len(reflexive_engine.monitors) == 1
59
+ assert test_monitor in reflexive_engine.monitors
60
+
61
+ # Remove monitor
62
+ reflexive_engine.remove_monitor(test_monitor)
63
+ assert len(reflexive_engine.monitors) == 0
64
+ assert test_monitor not in reflexive_engine.monitors
65
+
66
+ async def test_submit_action(self, reflexive_engine, action_context):
67
+ """Test submitting an action for evaluation."""
68
+ await reflexive_engine.submit_action(action_context)
69
+
70
+ # Check that action was added to queue
71
+ assert reflexive_engine.event_queue.qsize() == 1
72
+
73
+ async def test_evaluate_action_no_violations(self, reflexive_engine, action_context):
74
+ """Test evaluating an action with no violations."""
75
+ decision = await reflexive_engine._evaluate_action(action_context)
76
+
77
+ assert decision.decision_type == DecisionType.ALLOW
78
+ assert decision.risk_level == RiskLevel.LOW
79
+ assert decision.reason == "No violations or anomalies detected"
80
+ assert decision.action_context == action_context
81
+ assert decision.proof_hash is not None
82
+
83
+ async def test_evaluate_action_with_violations(self, reflexive_engine, action_context):
84
+ """Test evaluating an action with violations."""
85
+ # Add a monitor that returns violations
86
+ def violation_monitor(context):
87
+ return {
88
+ "type": "violation",
89
+ "severity": "high",
90
+ "violations": [{"rule": "test_rule", "message": "Test violation", "severity": "high"}]
91
+ }
92
+
93
+ reflexive_engine.add_monitor(violation_monitor)
94
+
95
+ decision = await reflexive_engine._evaluate_action(action_context)
96
+
97
+ assert decision.decision_type == DecisionType.HALT
98
+ assert decision.risk_level == RiskLevel.HIGH
99
+ assert "violations" in decision.evidence
100
+ assert len(decision.evidence["violations"]) == 1
101
+
102
+ async def test_evaluate_action_with_anomalies(self, reflexive_engine, action_context):
103
+ """Test evaluating an action with anomalies."""
104
+ # Add a monitor that returns anomalies
105
+ def anomaly_monitor(context):
106
+ return {
107
+ "type": "anomaly",
108
+ "severity": "medium",
109
+ "anomalies": [{"type": "test_anomaly", "message": "Test anomaly", "severity": "medium"}]
110
+ }
111
+
112
+ reflexive_engine.add_monitor(anomaly_monitor)
113
+
114
+ decision = await reflexive_engine._evaluate_action(action_context)
115
+
116
+ assert decision.decision_type == DecisionType.ESCALATE
117
+ assert decision.risk_level == RiskLevel.MEDIUM
118
+ assert "anomalies" in decision.evidence
119
+ assert len(decision.evidence["anomalies"]) == 1
120
+
121
+ async def test_evaluate_action_critical_violation(self, reflexive_engine, action_context):
122
+ """Test evaluating an action with critical violations."""
123
+ # Add a monitor that returns critical violations
124
+ def critical_monitor(context):
125
+ return {
126
+ "type": "violation",
127
+ "severity": "critical",
128
+ "violations": [{"rule": "critical_rule", "message": "Critical violation", "severity": "critical"}]
129
+ }
130
+
131
+ reflexive_engine.add_monitor(critical_monitor)
132
+
133
+ decision = await reflexive_engine._evaluate_action(action_context)
134
+
135
+ assert decision.decision_type == DecisionType.HALT
136
+ assert decision.risk_level == RiskLevel.CRITICAL
137
+
138
+ async def test_simulate_risk(self, reflexive_engine):
139
+ """Test risk simulation."""
140
+ risk_scenario = {
141
+ "action_context": {
142
+ "action_id": "simulation_action",
143
+ "actor_id": "test_actor",
144
+ "action_type": "admin_access",
145
+ "resource_id": "sensitive_data"
146
+ },
147
+ "monitors": [
148
+ lambda ctx: {
149
+ "type": "violation",
150
+ "severity": "high",
151
+ "violations": [{"rule": "admin_restriction", "message": "Unauthorized admin access", "severity": "high"}]
152
+ }
153
+ ]
154
+ }
155
+
156
+ decision = await reflexive_engine.simulate_risk(risk_scenario)
157
+
158
+ assert decision.decision_type == DecisionType.HALT
159
+ assert decision.risk_level == RiskLevel.HIGH
160
+ assert decision.action_context.action_id == "simulation_action"
161
+
162
+ def test_assess_risk_level(self, reflexive_engine):
163
+ """Test risk level assessment."""
164
+ # Test critical risk
165
+ critical_violations = [{"severity": "critical"}]
166
+ assert reflexive_engine._assess_risk_level(critical_violations, []) == RiskLevel.CRITICAL
167
+
168
+ # Test high risk
169
+ high_violations = [{"severity": "high"}]
170
+ assert reflexive_engine._assess_risk_level(high_violations, []) == RiskLevel.HIGH
171
+
172
+ # Test medium risk
173
+ medium_violations = [{"severity": "medium"}]
174
+ assert reflexive_engine._assess_risk_level(medium_violations, []) == RiskLevel.MEDIUM
175
+
176
+ # Test low risk
177
+ low_violations = [{"severity": "low"}]
178
+ assert reflexive_engine._assess_risk_level(low_violations, []) == RiskLevel.LOW
179
+
180
+ # Test multiple issues
181
+ multiple_issues = [{"severity": "low"}, {"severity": "low"}, {"severity": "low"}]
182
+ assert reflexive_engine._assess_risk_level(multiple_issues, []) == RiskLevel.MEDIUM
183
+
184
+ def test_determine_escalation_target(self, reflexive_engine):
185
+ """Test escalation target determination."""
186
+ decision = ReflexiveDecision(
187
+ decision_type=DecisionType.ESCALATE,
188
+ risk_level=RiskLevel.CRITICAL,
189
+ action_context=ActionContext(action_id="test", actor_id="test", action_type="test"),
190
+ reason="Test"
191
+ )
192
+
193
+ target = reflexive_engine._determine_escalation_target(decision)
194
+ assert target == "security_admin"
195
+
196
+ decision.risk_level = RiskLevel.HIGH
197
+ target = reflexive_engine._determine_escalation_target(decision)
198
+ assert target == "system_admin"
199
+
200
+ decision.risk_level = RiskLevel.MEDIUM
201
+ target = reflexive_engine._determine_escalation_target(decision)
202
+ assert target == "monitoring_team"
203
+
204
+
205
+ class TestActionContext:
206
+ """Test the ActionContext class."""
207
+
208
+ def test_action_context_creation(self):
209
+ """Test creating an action context."""
210
+ context = ActionContext(
211
+ action_id="test_action",
212
+ actor_id="test_actor",
213
+ action_type="test_type"
214
+ )
215
+
216
+ assert context.action_id == "test_action"
217
+ assert context.actor_id == "test_actor"
218
+ assert context.action_type == "test_type"
219
+ assert context.resource_id is None
220
+ assert context.metadata == {}
221
+ assert isinstance(context.timestamp, datetime)
222
+
223
+ def test_action_context_hash(self):
224
+ """Test action context hash generation."""
225
+ context1 = ActionContext(
226
+ action_id="test_action",
227
+ actor_id="test_actor",
228
+ action_type="test_type",
229
+ metadata={"key": "value"}
230
+ )
231
+
232
+ context2 = ActionContext(
233
+ action_id="test_action",
234
+ actor_id="test_actor",
235
+ action_type="test_type",
236
+ metadata={"key": "value"}
237
+ )
238
+
239
+ # Same content should produce same hash
240
+ assert context1.get_context_hash() == context2.get_context_hash()
241
+
242
+ # Different content should produce different hash
243
+ context3 = ActionContext(
244
+ action_id="different_action",
245
+ actor_id="test_actor",
246
+ action_type="test_type",
247
+ metadata={"key": "value"}
248
+ )
249
+
250
+ assert context1.get_context_hash() != context3.get_context_hash()
251
+
252
+
253
+ class TestReflexiveDecision:
254
+ """Test the ReflexiveDecision class."""
255
+
256
+ def test_decision_creation(self):
257
+ """Test creating a reflexive decision."""
258
+ action_context = ActionContext(
259
+ action_id="test_action",
260
+ actor_id="test_actor",
261
+ action_type="test_type"
262
+ )
263
+
264
+ decision = ReflexiveDecision(
265
+ decision_type=DecisionType.HALT,
266
+ risk_level=RiskLevel.HIGH,
267
+ action_context=action_context,
268
+ reason="Test reason"
269
+ )
270
+
271
+ assert decision.decision_type == DecisionType.HALT
272
+ assert decision.risk_level == RiskLevel.HIGH
273
+ assert decision.action_context == action_context
274
+ assert decision.reason == "Test reason"
275
+ assert decision.evidence == {}
276
+ assert decision.escalated_to is None
277
+ assert decision.proof_hash is None
278
+ assert isinstance(decision.timestamp, datetime)
279
+
280
+ def test_decision_hash(self):
281
+ """Test decision hash generation."""
282
+ from uuid import uuid4
283
+
284
+ action_context = ActionContext(
285
+ action_id="test_action",
286
+ actor_id="test_actor",
287
+ action_type="test_type"
288
+ )
289
+
290
+ # Use the same decision_id for both decisions
291
+ decision_id = uuid4()
292
+
293
+ decision1 = ReflexiveDecision(
294
+ decision_id=decision_id,
295
+ decision_type=DecisionType.HALT,
296
+ risk_level=RiskLevel.HIGH,
297
+ action_context=action_context,
298
+ reason="Test reason"
299
+ )
300
+
301
+ decision2 = ReflexiveDecision(
302
+ decision_id=decision_id,
303
+ decision_type=DecisionType.HALT,
304
+ risk_level=RiskLevel.HIGH,
305
+ action_context=action_context,
306
+ reason="Test reason"
307
+ )
308
+
309
+ # Same content should produce same hash
310
+ assert decision1.get_decision_hash() == decision2.get_decision_hash()
311
+
312
+ # Different content should produce different hash
313
+ decision3 = ReflexiveDecision(
314
+ decision_type=DecisionType.ESCALATE,
315
+ risk_level=RiskLevel.HIGH,
316
+ action_context=action_context,
317
+ reason="Test reason"
318
+ )
319
+
320
+ assert decision1.get_decision_hash() != decision3.get_decision_hash()
tests/reflexive/test_reflexive_http.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the reflexive HTTP endpoints."""
2
+
3
+ import pytest
4
+ import httpx
5
+ from unittest.mock import Mock, AsyncMock
6
+
7
+ from fastmcp.server.server import FastMCP
8
+ from fastmcp.reflexive import ReflexiveEngine, ActionContext, DecisionType, RiskLevel
9
+ from fastmcp.server.http import create_streamable_http_app
10
+
11
+
12
+ @pytest.fixture(name="server_with_reflexive")
13
+ async def server_with_reflexive_fixture():
14
+ """Fixture for a FastMCP server with an enabled reflexive core."""
15
+ server = FastMCP("TestReflexiveServer")
16
+ reflexive_engine = server.enable_reflexive_core()
17
+ return server, reflexive_engine
18
+
19
+
20
+ @pytest.fixture
21
+ async def client(server_with_reflexive):
22
+ """Create an HTTP client for testing."""
23
+ server, reflexive_engine = server_with_reflexive
24
+ app = create_streamable_http_app(server, streamable_http_path="/")
25
+ async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
26
+ # Store server and reflexive engine in client for test access
27
+ client.server = server
28
+ client.reflexive_engine = reflexive_engine
29
+ yield client
30
+
31
+
32
+ class TestReflexiveHTTPEndpoints:
33
+ """Test the reflexive HTTP endpoints."""
34
+
35
+ async def test_simulate_risk_endpoint(self, client):
36
+ """Test the simulate risk endpoint."""
37
+ risk_scenario = {
38
+ "action_context": {
39
+ "action_id": "test_action",
40
+ "actor_id": "test_user",
41
+ "action_type": "admin_access",
42
+ "resource_id": "admin_panel"
43
+ },
44
+ "scenario_type": "policy_violation"
45
+ }
46
+
47
+ response = await client.post("/core/simulate-risk", json=risk_scenario)
48
+
49
+ assert response.status_code == 200
50
+ data = response.json()
51
+ assert "simulation_id" in data
52
+ assert "decision" in data
53
+ assert "action" in data
54
+ assert "action_context" in data
55
+
56
+ # Check decision structure
57
+ decision = data["decision"]
58
+ assert "decision_id" in decision
59
+ assert "decision_type" in decision
60
+ assert "risk_level" in decision
61
+ assert "reason" in decision
62
+ assert "proof_hash" in decision
63
+
64
+ # Check action structure
65
+ action = data["action"]
66
+ assert "action_id" in action
67
+ assert "action_type" in action
68
+ assert "status" in action
69
+ assert "result" in action
70
+
71
+ async def test_simulate_risk_invalid_data(self, client):
72
+ """Test simulate risk endpoint with invalid data."""
73
+ invalid_scenario = {
74
+ "invalid_field": "invalid_value"
75
+ }
76
+
77
+ response = await client.post("/core/simulate-risk", json=invalid_scenario)
78
+
79
+ assert response.status_code == 400
80
+ data = response.json()
81
+ assert "error" in data
82
+ assert "Missing required field: action_context" in data["error"]
83
+
84
+ async def test_get_engine_status_endpoint(self, client):
85
+ """Test the get engine status endpoint."""
86
+ response = await client.get("/core/status")
87
+
88
+ assert response.status_code == 200
89
+ data = response.json()
90
+ assert "is_running" in data
91
+ assert "monitor_count" in data
92
+ assert "queue_size" in data
93
+ assert "decision_handlers" in data
94
+
95
+ async def test_submit_action_endpoint(self, client):
96
+ """Test the submit action endpoint."""
97
+ action_data = {
98
+ "action_id": "test_action",
99
+ "actor_id": "test_user",
100
+ "action_type": "tool_call",
101
+ "resource_id": "test_resource",
102
+ "metadata": {"test": "data"}
103
+ }
104
+
105
+ response = await client.post("/core/submit-action", json=action_data)
106
+
107
+ assert response.status_code == 202
108
+ data = response.json()
109
+ assert "message" in data
110
+ assert "action_id" in data
111
+ assert "submitted_at" in data
112
+ assert data["action_id"] == "test_action"
113
+
114
+ async def test_submit_action_invalid_data(self, client):
115
+ """Test submit action endpoint with invalid data."""
116
+ invalid_action = {
117
+ "invalid_field": "invalid_value"
118
+ }
119
+
120
+ response = await client.post("/core/submit-action", json=invalid_action)
121
+
122
+ assert response.status_code == 400
123
+ data = response.json()
124
+ assert "error" in data
125
+ assert "Invalid action data" in data["error"]
126
+
127
+ async def test_get_monitor_stats_endpoint(self, client):
128
+ """Test the get monitor stats endpoint."""
129
+ response = await client.get("/core/monitor-stats")
130
+
131
+ assert response.status_code == 200
132
+ data = response.json()
133
+ # Should return empty dict if no monitors are configured
134
+ assert isinstance(data, dict)
135
+
136
+ async def test_create_risk_scenario_endpoint(self, client):
137
+ """Test the create risk scenario endpoint."""
138
+ scenario_data = {
139
+ "scenario_name": "admin_privilege_escalation",
140
+ "scenario_type": "policy_violation",
141
+ "parameters": {
142
+ "actor_type": "guest_user",
143
+ "target_resource": "admin_panel",
144
+ "severity": "high"
145
+ }
146
+ }
147
+
148
+ response = await client.post("/core/risk-scenario", json=scenario_data)
149
+
150
+ assert response.status_code == 200
151
+ data = response.json()
152
+ assert "scenario" in data
153
+ assert "message" in data
154
+ assert data["message"] == "Risk scenario 'admin_privilege_escalation' created successfully"
155
+
156
+ # Check scenario structure
157
+ scenario = data["scenario"]
158
+ assert "action_context" in scenario
159
+ assert "expected_decision" in scenario
160
+ assert "expected_risk_level" in scenario
161
+
162
+ # Check action context
163
+ action_context = scenario["action_context"]
164
+ assert action_context["action_type"] == "admin_access"
165
+ assert action_context["resource_id"] == "admin_panel"
166
+
167
+ async def test_create_risk_scenario_missing_fields(self, client):
168
+ """Test create risk scenario endpoint with missing fields."""
169
+ invalid_scenario = {
170
+ "scenario_name": "test_scenario"
171
+ # Missing scenario_type
172
+ }
173
+
174
+ response = await client.post("/core/risk-scenario", json=invalid_scenario)
175
+
176
+ assert response.status_code == 400
177
+ data = response.json()
178
+ assert "error" in data
179
+ assert "Missing required fields" in data["error"]
180
+
181
+ async def test_create_risk_scenario_unknown_type(self, client):
182
+ """Test create risk scenario endpoint with unknown scenario type."""
183
+ scenario_data = {
184
+ "scenario_name": "unknown_scenario",
185
+ "scenario_type": "unknown_type",
186
+ "parameters": {}
187
+ }
188
+
189
+ response = await client.post("/core/risk-scenario", json=scenario_data)
190
+
191
+ assert response.status_code == 200
192
+ data = response.json()
193
+ assert "scenario" in data
194
+
195
+ # Should create a custom scenario
196
+ scenario = data["scenario"]
197
+ assert scenario["action_context"]["action_id"] == "custom_unknown_scenario"
198
+ assert scenario["expected_decision"] == "monitor"
199
+ assert scenario["expected_risk_level"] == "low"
200
+
201
+ async def test_simulate_risk_with_violations(self, client):
202
+ """Test simulate risk with policy violations."""
203
+ # Add a monitor that returns violations
204
+ def violation_monitor(context):
205
+ return {
206
+ "type": "violation",
207
+ "severity": "high",
208
+ "violations": [{"rule": "admin_restriction", "message": "Unauthorized admin access", "severity": "high"}]
209
+ }
210
+
211
+ client.reflexive_engine.add_monitor(violation_monitor)
212
+
213
+ risk_scenario = {
214
+ "action_context": {
215
+ "action_id": "admin_action",
216
+ "actor_id": "guest_user",
217
+ "action_type": "admin_access",
218
+ "resource_id": "admin_panel"
219
+ }
220
+ }
221
+
222
+ response = await client.post("/core/simulate-risk", json=risk_scenario)
223
+
224
+ assert response.status_code == 200
225
+ data = response.json()
226
+
227
+ # Should result in a halt decision
228
+ decision = data["decision"]
229
+ assert decision["decision_type"] == "halt"
230
+ assert decision["risk_level"] == "high"
231
+ assert "violations" in decision["evidence"]
232
+
233
+ async def test_simulate_risk_with_anomalies(self, client):
234
+ """Test simulate risk with anomalies."""
235
+ # Add a monitor that returns anomalies
236
+ def anomaly_monitor(context):
237
+ return {
238
+ "type": "anomaly",
239
+ "severity": "medium",
240
+ "anomalies": [{"type": "unusual_timing", "message": "Action at unusual hour", "severity": "medium"}]
241
+ }
242
+
243
+ client.reflexive_engine.add_monitor(anomaly_monitor)
244
+
245
+ risk_scenario = {
246
+ "action_context": {
247
+ "action_id": "unusual_action",
248
+ "actor_id": "test_user",
249
+ "action_type": "data_access",
250
+ "resource_id": "sensitive_data"
251
+ }
252
+ }
253
+
254
+ response = await client.post("/core/simulate-risk", json=risk_scenario)
255
+
256
+ assert response.status_code == 200
257
+ data = response.json()
258
+
259
+ # Should result in an escalate decision
260
+ decision = data["decision"]
261
+ assert decision["decision_type"] == "escalate"
262
+ assert decision["risk_level"] == "medium"
263
+ assert "anomalies" in decision["evidence"]
264
+
265
+ async def test_simulate_risk_no_issues(self, client):
266
+ """Test simulate risk with no violations or anomalies."""
267
+ risk_scenario = {
268
+ "action_context": {
269
+ "action_id": "normal_action",
270
+ "actor_id": "authorized_user",
271
+ "action_type": "normal_operation",
272
+ "resource_id": "public_resource"
273
+ }
274
+ }
275
+
276
+ response = await client.post("/core/simulate-risk", json=risk_scenario)
277
+
278
+ assert response.status_code == 200
279
+ data = response.json()
280
+
281
+ # Should result in an allow decision
282
+ decision = data["decision"]
283
+ assert decision["decision_type"] == "allow"
284
+ assert decision["risk_level"] == "low"
285
+ assert decision["reason"] == "No violations or anomalies detected"
286
+
287
+ async def test_multiple_risk_scenarios(self, client):
288
+ """Test multiple predefined risk scenarios."""
289
+ scenarios = [
290
+ {
291
+ "name": "admin_privilege_escalation",
292
+ "expected_decision": "halt",
293
+ "expected_risk": "high"
294
+ },
295
+ {
296
+ "name": "suspicious_activity",
297
+ "expected_decision": "escalate",
298
+ "expected_risk": "medium"
299
+ },
300
+ {
301
+ "name": "integrity_violation",
302
+ "expected_decision": "halt",
303
+ "expected_risk": "critical"
304
+ },
305
+ {
306
+ "name": "rate_limit_exceeded",
307
+ "expected_decision": "escalate",
308
+ "expected_risk": "medium"
309
+ }
310
+ ]
311
+
312
+ for scenario in scenarios:
313
+ scenario_data = {
314
+ "scenario_name": scenario["name"],
315
+ "scenario_type": "test",
316
+ "parameters": {}
317
+ }
318
+
319
+ response = await client.post("/core/risk-scenario", json=scenario_data)
320
+ assert response.status_code == 200
321
+
322
+ data = response.json()
323
+ created_scenario = data["scenario"]
324
+ assert created_scenario["expected_decision"] == scenario["expected_decision"]
325
+ assert created_scenario["expected_risk_level"] == scenario["expected_risk"]