agent_os_kernel 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. agent_control_plane/__init__.py +662 -0
  2. agent_control_plane/a2a_adapter.py +543 -0
  3. agent_control_plane/adapter.py +417 -0
  4. agent_control_plane/agent_hibernation.py +394 -0
  5. agent_control_plane/agent_kernel.py +470 -0
  6. agent_control_plane/compliance.py +720 -0
  7. agent_control_plane/constraint_graphs.py +478 -0
  8. agent_control_plane/control_plane.py +854 -0
  9. agent_control_plane/example_executors.py +195 -0
  10. agent_control_plane/execution_engine.py +231 -0
  11. agent_control_plane/flight_recorder.py +846 -0
  12. agent_control_plane/governance_layer.py +435 -0
  13. agent_control_plane/hf_utils.py +563 -0
  14. agent_control_plane/interfaces/__init__.py +55 -0
  15. agent_control_plane/interfaces/kernel_interface.py +361 -0
  16. agent_control_plane/interfaces/plugin_interface.py +497 -0
  17. agent_control_plane/interfaces/protocol_interfaces.py +387 -0
  18. agent_control_plane/kernel_space.py +1009 -0
  19. agent_control_plane/langchain_adapter.py +424 -0
  20. agent_control_plane/lifecycle.py +3113 -0
  21. agent_control_plane/mcp_adapter.py +653 -0
  22. agent_control_plane/ml_safety.py +563 -0
  23. agent_control_plane/multimodal.py +727 -0
  24. agent_control_plane/mute_agent.py +422 -0
  25. agent_control_plane/observability.py +787 -0
  26. agent_control_plane/orchestrator.py +482 -0
  27. agent_control_plane/plugin_registry.py +750 -0
  28. agent_control_plane/policy_engine.py +954 -0
  29. agent_control_plane/process_isolation.py +777 -0
  30. agent_control_plane/shadow_mode.py +310 -0
  31. agent_control_plane/signals.py +493 -0
  32. agent_control_plane/supervisor_agents.py +430 -0
  33. agent_control_plane/time_travel_debugger.py +557 -0
  34. agent_control_plane/tool_registry.py +452 -0
  35. agent_control_plane/vfs.py +697 -0
  36. agent_kernel/__init__.py +69 -0
  37. agent_kernel/analyzer.py +435 -0
  38. agent_kernel/auditor.py +36 -0
  39. agent_kernel/completeness_auditor.py +237 -0
  40. agent_kernel/detector.py +203 -0
  41. agent_kernel/kernel.py +744 -0
  42. agent_kernel/memory_manager.py +85 -0
  43. agent_kernel/models.py +374 -0
  44. agent_kernel/nudge_mechanism.py +263 -0
  45. agent_kernel/outcome_analyzer.py +338 -0
  46. agent_kernel/patcher.py +582 -0
  47. agent_kernel/semantic_analyzer.py +316 -0
  48. agent_kernel/semantic_purge.py +349 -0
  49. agent_kernel/simulator.py +449 -0
  50. agent_kernel/teacher.py +85 -0
  51. agent_kernel/triage.py +152 -0
  52. agent_os/__init__.py +409 -0
  53. agent_os/_adversarial_impl.py +200 -0
  54. agent_os/_circuit_breaker_impl.py +232 -0
  55. agent_os/_mcp_metrics.py +193 -0
  56. agent_os/adversarial.py +20 -0
  57. agent_os/agents_compat.py +490 -0
  58. agent_os/audit_logger.py +135 -0
  59. agent_os/base_agent.py +651 -0
  60. agent_os/circuit_breaker.py +34 -0
  61. agent_os/cli/__init__.py +659 -0
  62. agent_os/cli/cmd_audit.py +128 -0
  63. agent_os/cli/cmd_init.py +152 -0
  64. agent_os/cli/cmd_policy.py +41 -0
  65. agent_os/cli/cmd_policy_gen.py +180 -0
  66. agent_os/cli/cmd_validate.py +258 -0
  67. agent_os/cli/mcp_scan.py +265 -0
  68. agent_os/cli/output.py +192 -0
  69. agent_os/cli/policy_checker.py +330 -0
  70. agent_os/compat.py +74 -0
  71. agent_os/constraint_graph.py +234 -0
  72. agent_os/content_governance.py +140 -0
  73. agent_os/context_budget.py +305 -0
  74. agent_os/credential_redactor.py +224 -0
  75. agent_os/diff_policy.py +89 -0
  76. agent_os/egress_policy.py +159 -0
  77. agent_os/escalation.py +276 -0
  78. agent_os/event_bus.py +124 -0
  79. agent_os/exceptions.py +180 -0
  80. agent_os/execution_context_policy.py +141 -0
  81. agent_os/github_enterprise.py +96 -0
  82. agent_os/health.py +20 -0
  83. agent_os/integrations/__init__.py +279 -0
  84. agent_os/integrations/a2a_adapter.py +279 -0
  85. agent_os/integrations/agent_lightning/__init__.py +30 -0
  86. agent_os/integrations/anthropic_adapter.py +420 -0
  87. agent_os/integrations/autogen_adapter.py +620 -0
  88. agent_os/integrations/base.py +1137 -0
  89. agent_os/integrations/compat.py +229 -0
  90. agent_os/integrations/config.py +98 -0
  91. agent_os/integrations/conversation_guardian.py +957 -0
  92. agent_os/integrations/crewai_adapter.py +467 -0
  93. agent_os/integrations/drift_detector.py +425 -0
  94. agent_os/integrations/dry_run.py +124 -0
  95. agent_os/integrations/escalation.py +582 -0
  96. agent_os/integrations/gemini_adapter.py +364 -0
  97. agent_os/integrations/google_adk_adapter.py +633 -0
  98. agent_os/integrations/guardrails_adapter.py +394 -0
  99. agent_os/integrations/health.py +197 -0
  100. agent_os/integrations/langchain_adapter.py +654 -0
  101. agent_os/integrations/llamafirewall.py +343 -0
  102. agent_os/integrations/llamaindex_adapter.py +188 -0
  103. agent_os/integrations/logging.py +191 -0
  104. agent_os/integrations/maf_adapter.py +631 -0
  105. agent_os/integrations/mistral_adapter.py +365 -0
  106. agent_os/integrations/openai_adapter.py +816 -0
  107. agent_os/integrations/openai_agents_sdk.py +406 -0
  108. agent_os/integrations/policy_compose.py +171 -0
  109. agent_os/integrations/profiling.py +144 -0
  110. agent_os/integrations/pydantic_ai_adapter.py +420 -0
  111. agent_os/integrations/rate_limiter.py +130 -0
  112. agent_os/integrations/rbac.py +143 -0
  113. agent_os/integrations/registry.py +113 -0
  114. agent_os/integrations/scope_guard.py +303 -0
  115. agent_os/integrations/semantic_kernel_adapter.py +769 -0
  116. agent_os/integrations/smolagents_adapter.py +629 -0
  117. agent_os/integrations/templates.py +178 -0
  118. agent_os/integrations/token_budget.py +134 -0
  119. agent_os/integrations/tool_aliases.py +190 -0
  120. agent_os/integrations/webhooks.py +177 -0
  121. agent_os/lite.py +208 -0
  122. agent_os/mcp_gateway.py +385 -0
  123. agent_os/mcp_message_signer.py +273 -0
  124. agent_os/mcp_protocols.py +161 -0
  125. agent_os/mcp_response_scanner.py +232 -0
  126. agent_os/mcp_security.py +924 -0
  127. agent_os/mcp_session_auth.py +231 -0
  128. agent_os/mcp_sliding_rate_limiter.py +184 -0
  129. agent_os/memory_guard.py +409 -0
  130. agent_os/metrics.py +134 -0
  131. agent_os/mute.py +428 -0
  132. agent_os/mute_agent.py +209 -0
  133. agent_os/policies/__init__.py +77 -0
  134. agent_os/policies/async_evaluator.py +275 -0
  135. agent_os/policies/backends.py +670 -0
  136. agent_os/policies/bridge.py +169 -0
  137. agent_os/policies/budget.py +85 -0
  138. agent_os/policies/cli.py +294 -0
  139. agent_os/policies/conflict_resolution.py +270 -0
  140. agent_os/policies/data_classification.py +252 -0
  141. agent_os/policies/evaluator.py +239 -0
  142. agent_os/policies/policy_schema.json +228 -0
  143. agent_os/policies/rate_limiting.py +145 -0
  144. agent_os/policies/schema.py +115 -0
  145. agent_os/policies/shared.py +331 -0
  146. agent_os/prompt_injection.py +694 -0
  147. agent_os/providers.py +182 -0
  148. agent_os/py.typed +0 -0
  149. agent_os/retry.py +81 -0
  150. agent_os/reversibility.py +251 -0
  151. agent_os/sandbox.py +432 -0
  152. agent_os/sandbox_provider.py +140 -0
  153. agent_os/secure_codegen.py +525 -0
  154. agent_os/security_skills.py +538 -0
  155. agent_os/semantic_policy.py +422 -0
  156. agent_os/server/__init__.py +15 -0
  157. agent_os/server/__main__.py +25 -0
  158. agent_os/server/app.py +277 -0
  159. agent_os/server/models.py +104 -0
  160. agent_os/shift_left_metrics.py +130 -0
  161. agent_os/stateless.py +742 -0
  162. agent_os/supervisor.py +148 -0
  163. agent_os/task_outcome.py +148 -0
  164. agent_os/transparency.py +181 -0
  165. agent_os/trust_root.py +128 -0
  166. agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
  167. agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
  168. agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
  169. agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
  170. agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
  171. agent_os_observability/__init__.py +27 -0
  172. agent_os_observability/dashboards.py +898 -0
  173. agent_os_observability/metrics.py +398 -0
  174. agent_os_observability/server.py +223 -0
  175. agent_os_observability/tracer.py +232 -0
  176. agent_primitives/__init__.py +24 -0
  177. agent_primitives/failures.py +84 -0
  178. agent_primitives/py.typed +0 -0
  179. amb_core/__init__.py +177 -0
  180. amb_core/adapters/__init__.py +57 -0
  181. amb_core/adapters/aws_sqs_broker.py +376 -0
  182. amb_core/adapters/azure_servicebus_broker.py +340 -0
  183. amb_core/adapters/kafka_broker.py +260 -0
  184. amb_core/adapters/nats_broker.py +285 -0
  185. amb_core/adapters/rabbitmq_broker.py +235 -0
  186. amb_core/adapters/redis_broker.py +262 -0
  187. amb_core/broker.py +145 -0
  188. amb_core/bus.py +481 -0
  189. amb_core/cloudevents.py +509 -0
  190. amb_core/dlq.py +345 -0
  191. amb_core/hf_utils.py +536 -0
  192. amb_core/memory_broker.py +410 -0
  193. amb_core/models.py +141 -0
  194. amb_core/persistence.py +529 -0
  195. amb_core/schema.py +294 -0
  196. amb_core/tracing.py +358 -0
  197. atr/__init__.py +640 -0
  198. atr/access.py +348 -0
  199. atr/composition.py +645 -0
  200. atr/decorator.py +357 -0
  201. atr/executor.py +384 -0
  202. atr/health.py +557 -0
  203. atr/hf_utils.py +449 -0
  204. atr/injection.py +422 -0
  205. atr/metrics.py +440 -0
  206. atr/policies.py +403 -0
  207. atr/py.typed +2 -0
  208. atr/registry.py +452 -0
  209. atr/schema.py +480 -0
  210. atr/tools/safe/__init__.py +75 -0
  211. atr/tools/safe/calculator.py +467 -0
  212. atr/tools/safe/datetime_tool.py +443 -0
  213. atr/tools/safe/file_reader.py +402 -0
  214. atr/tools/safe/http_client.py +316 -0
  215. atr/tools/safe/json_parser.py +374 -0
  216. atr/tools/safe/text_tool.py +537 -0
  217. atr/tools/safe/toolkit.py +175 -0
  218. caas/__init__.py +162 -0
  219. caas/api/__init__.py +7 -0
  220. caas/api/server.py +1328 -0
  221. caas/caching.py +834 -0
  222. caas/cli.py +210 -0
  223. caas/conversation.py +223 -0
  224. caas/decay.py +72 -0
  225. caas/detection/__init__.py +9 -0
  226. caas/detection/detector.py +238 -0
  227. caas/enrichment.py +130 -0
  228. caas/gateway/__init__.py +27 -0
  229. caas/gateway/trust_gateway.py +474 -0
  230. caas/hf_utils.py +479 -0
  231. caas/ingestion/__init__.py +23 -0
  232. caas/ingestion/processors.py +253 -0
  233. caas/ingestion/structure_parser.py +188 -0
  234. caas/models.py +356 -0
  235. caas/pragmatic_truth.py +444 -0
  236. caas/routing/__init__.py +10 -0
  237. caas/routing/heuristic_router.py +58 -0
  238. caas/storage/__init__.py +9 -0
  239. caas/storage/store.py +389 -0
  240. caas/triad.py +213 -0
  241. caas/tuning/__init__.py +9 -0
  242. caas/tuning/tuner.py +329 -0
  243. caas/vfs/__init__.py +14 -0
  244. caas/vfs/filesystem.py +452 -0
  245. cmvk/__init__.py +218 -0
  246. cmvk/audit.py +402 -0
  247. cmvk/benchmarks.py +478 -0
  248. cmvk/constitutional.py +904 -0
  249. cmvk/hf_utils.py +301 -0
  250. cmvk/metrics.py +473 -0
  251. cmvk/profiles.py +300 -0
  252. cmvk/py.typed +0 -0
  253. cmvk/types.py +12 -0
  254. cmvk/verification.py +956 -0
  255. emk/__init__.py +89 -0
  256. emk/causal.py +352 -0
  257. emk/hf_utils.py +421 -0
  258. emk/indexer.py +83 -0
  259. emk/py.typed +0 -0
  260. emk/schema.py +204 -0
  261. emk/sleep_cycle.py +347 -0
  262. emk/store.py +281 -0
  263. iatp/__init__.py +166 -0
  264. iatp/attestation.py +461 -0
  265. iatp/cli.py +317 -0
  266. iatp/hf_utils.py +472 -0
  267. iatp/ipc_pipes.py +580 -0
  268. iatp/main.py +412 -0
  269. iatp/models/__init__.py +447 -0
  270. iatp/policy_engine.py +337 -0
  271. iatp/py.typed +2 -0
  272. iatp/recovery.py +321 -0
  273. iatp/security/__init__.py +270 -0
  274. iatp/sidecar/__init__.py +519 -0
  275. iatp/telemetry/__init__.py +164 -0
  276. iatp/tests/__init__.py +1 -0
  277. iatp/tests/test_attestation.py +370 -0
  278. iatp/tests/test_cli.py +131 -0
  279. iatp/tests/test_ed25519_attestation.py +211 -0
  280. iatp/tests/test_models.py +130 -0
  281. iatp/tests/test_policy_engine.py +347 -0
  282. iatp/tests/test_recovery.py +281 -0
  283. iatp/tests/test_security.py +222 -0
  284. iatp/tests/test_sidecar.py +167 -0
  285. iatp/tests/test_telemetry.py +175 -0
  286. mcp_kernel_server/__init__.py +28 -0
  287. mcp_kernel_server/cli.py +274 -0
  288. mcp_kernel_server/resources.py +217 -0
  289. mcp_kernel_server/server.py +564 -0
  290. mcp_kernel_server/tools.py +1174 -0
  291. mute_agent/__init__.py +68 -0
  292. mute_agent/core/__init__.py +1 -0
  293. mute_agent/core/execution_agent.py +166 -0
  294. mute_agent/core/handshake_protocol.py +201 -0
  295. mute_agent/core/reasoning_agent.py +238 -0
  296. mute_agent/knowledge_graph/__init__.py +1 -0
  297. mute_agent/knowledge_graph/graph_elements.py +65 -0
  298. mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
  299. mute_agent/knowledge_graph/subgraph.py +224 -0
  300. mute_agent/listener/__init__.py +43 -0
  301. mute_agent/listener/adapters/__init__.py +31 -0
  302. mute_agent/listener/adapters/base_adapter.py +189 -0
  303. mute_agent/listener/adapters/caas_adapter.py +344 -0
  304. mute_agent/listener/adapters/control_plane_adapter.py +436 -0
  305. mute_agent/listener/adapters/iatp_adapter.py +332 -0
  306. mute_agent/listener/adapters/scak_adapter.py +251 -0
  307. mute_agent/listener/listener.py +610 -0
  308. mute_agent/listener/state_observer.py +436 -0
  309. mute_agent/listener/threshold_config.py +313 -0
  310. mute_agent/super_system/__init__.py +1 -0
  311. mute_agent/super_system/router.py +204 -0
  312. mute_agent/visualization/__init__.py +10 -0
  313. mute_agent/visualization/graph_debugger.py +502 -0
  314. nexus/README.md +60 -0
  315. nexus/__init__.py +51 -0
  316. nexus/arbiter.py +359 -0
  317. nexus/client.py +466 -0
  318. nexus/dmz.py +444 -0
  319. nexus/escrow.py +430 -0
  320. nexus/exceptions.py +286 -0
  321. nexus/pyproject.toml +36 -0
  322. nexus/registry.py +393 -0
  323. nexus/reputation.py +425 -0
  324. nexus/schemas/__init__.py +51 -0
  325. nexus/schemas/compliance.py +276 -0
  326. nexus/schemas/escrow.py +251 -0
  327. nexus/schemas/manifest.py +225 -0
  328. nexus/schemas/receipt.py +208 -0
  329. nexus/tests/__init__.py +0 -0
  330. nexus/tests/conftest.py +146 -0
  331. nexus/tests/test_arbiter.py +192 -0
  332. nexus/tests/test_dmz.py +194 -0
  333. nexus/tests/test_escrow.py +276 -0
  334. nexus/tests/test_exceptions.py +225 -0
  335. nexus/tests/test_registry.py +232 -0
  336. nexus/tests/test_reputation.py +328 -0
  337. nexus/tests/test_schemas.py +295 -0
@@ -0,0 +1,954 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """
5
+ Policy Engine - Governance and compliance rules for agent execution
6
+
7
+ The Policy Engine enforces rules and constraints on agent behavior,
8
+ including resource quotas, access controls, and risk management.
9
+
10
+ Research Foundations:
11
+ - ABAC model based on NIST SP 800-162 (Attribute-Based Access Control)
12
+ - Risk scoring informed by "A Safety Framework for Real-World Agentic Systems"
13
+ (arXiv:2511.21990, 2024) - contextual risk management
14
+ - Governance patterns from "Practices for Governing Agentic AI Systems"
15
+ (OpenAI, 2023) - pre/post-deployment checks
16
+ - Rate limiting patterns from "Fault-Tolerant Multi-Agent Systems"
17
+ (IEEE Trans. SMC, 2024) - circuit breaker patterns
18
+
19
+ See docs/RESEARCH_FOUNDATION.md for complete references.
20
+ """
21
+
22
+ from typing import Dict, List, Optional, Callable, Any, Tuple
23
+ from dataclasses import dataclass, field
24
+ from datetime import datetime, timedelta
25
+ from types import MappingProxyType # noqa: F401 — reserved for future immutable dict enforcement
26
+ from .agent_kernel import ExecutionRequest, ActionType, PolicyRule
27
+ import logging
28
+ import uuid
29
+ import os
30
+ import re
31
+ import warnings
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ @dataclass
39
+ class Condition:
40
+ """
41
+ A condition for ABAC (Attribute-Based Access Control).
42
+
43
+ Allows policies like: "Agent can call tool X IF condition Y is true"
44
+ Example: "refund_user" allowed IF user_status == "verified"
45
+ """
46
+
47
+ attribute_path: str # e.g., "user_status", "args.amount", "context.time_of_day"
48
+ operator: str # eq, ne, gt, lt, gte, lte, in, not_in, contains
49
+ value: Any # The value to compare against
50
+
51
+ def evaluate(self, context: Dict[str, Any]) -> bool:
52
+ """
53
+ Evaluate the condition against a context.
54
+
55
+ Args:
56
+ context: Dictionary containing the evaluation context
57
+ (e.g., {"user_status": "verified", "args": {...}, "context": {...}})
58
+
59
+ Returns:
60
+ True if condition is met, False otherwise
61
+ """
62
+ # Extract the value from the context using the attribute path
63
+ actual_value = self._get_nested_value(context, self.attribute_path)
64
+
65
+ if actual_value is None:
66
+ return False
67
+
68
+ # Evaluate based on operator
69
+ if self.operator == "eq":
70
+ return actual_value == self.value
71
+ elif self.operator == "ne":
72
+ return actual_value != self.value
73
+ elif self.operator == "gt":
74
+ return actual_value > self.value
75
+ elif self.operator == "lt":
76
+ return actual_value < self.value
77
+ elif self.operator == "gte":
78
+ return actual_value >= self.value
79
+ elif self.operator == "lte":
80
+ return actual_value <= self.value
81
+ elif self.operator == "in":
82
+ return actual_value in self.value
83
+ elif self.operator == "not_in":
84
+ return actual_value not in self.value
85
+ elif self.operator == "contains":
86
+ return self.value in actual_value
87
+ else:
88
+ return False
89
+
90
+ def _get_nested_value(self, data: Dict[str, Any], path: str) -> Any:
91
+ """
92
+ Get a nested value from a dictionary using dot notation.
93
+
94
+ Args:
95
+ data: The dictionary to search
96
+ path: Dot-separated path (e.g., "args.amount")
97
+
98
+ Returns:
99
+ The value at the path, or None if not found
100
+ """
101
+ keys = path.split(".")
102
+ value = data
103
+
104
+ for key in keys:
105
+ if isinstance(value, dict) and key in value:
106
+ value = value[key]
107
+ else:
108
+ return None
109
+
110
+ return value
111
+
112
+
113
+ @dataclass
114
+ class ConditionalPermission:
115
+ """
116
+ A permission that requires conditions to be met.
117
+
118
+ Example: "refund_user" allowed IF user_status == "verified" AND amount < 1000
119
+ """
120
+
121
+ tool_name: str
122
+ conditions: List[Condition]
123
+ require_all: bool = (
124
+ True # If True, all conditions must be met (AND). If False, any condition (OR).
125
+ )
126
+
127
+ def is_allowed(self, context: Dict[str, Any]) -> bool:
128
+ """
129
+ Check if the permission is allowed given the context.
130
+
131
+ Args:
132
+ context: The evaluation context
133
+
134
+ Returns:
135
+ True if allowed, False otherwise
136
+ """
137
+ if self.require_all:
138
+ # All conditions must be true (AND)
139
+ return all(cond.evaluate(context) for cond in self.conditions)
140
+ else:
141
+ # Any condition must be true (OR)
142
+ return any(cond.evaluate(context) for cond in self.conditions)
143
+
144
+
145
+ @dataclass
146
+ class ResourceQuota:
147
+ """Resource quota for an agent or tenant"""
148
+
149
+ agent_id: str
150
+ max_requests_per_minute: int = 60
151
+ max_requests_per_hour: int = 1000
152
+ max_execution_time_seconds: float = 300.0
153
+ max_concurrent_executions: int = 5
154
+ allowed_action_types: List[ActionType] = field(default_factory=list)
155
+
156
+ # Usage tracking
157
+ requests_this_minute: int = 0
158
+ requests_this_hour: int = 0
159
+ current_executions: int = 0
160
+ last_reset_minute: datetime = field(default_factory=datetime.now)
161
+ last_reset_hour: datetime = field(default_factory=datetime.now)
162
+
163
+
164
+ @dataclass
165
+ class RiskPolicy:
166
+ """Risk-based policy for agent actions"""
167
+
168
+ max_risk_score: float = 0.5
169
+ require_approval_above: float = 0.7
170
+ deny_above: float = 0.9
171
+
172
+ # Risk factors
173
+ high_risk_patterns: List[str] = field(default_factory=list)
174
+ allowed_domains: List[str] = field(default_factory=list)
175
+ blocked_domains: List[str] = field(default_factory=list)
176
+
177
+
178
+ class PolicyEngine:
179
+ """
180
+ Policy Engine - Enforces governance rules for agent execution
181
+
182
+ Provides:
183
+ - Rate limiting and quotas
184
+ - Risk assessment
185
+ - Access control policies
186
+ - Compliance rules
187
+ """
188
+
189
+ def __init__(self):
190
+ self.quotas: Dict[str, ResourceQuota] = {}
191
+ self.risk_policies: Dict[str, RiskPolicy] = {}
192
+ self.custom_rules: List[PolicyRule] = []
193
+ self.blocked_patterns: List[str] = []
194
+
195
+ # Graph-based allow-list approach (Scale by Subtraction)
196
+ # By default, EVERYTHING is blocked unless explicitly allowed
197
+ self.allowed_transitions: set = set()
198
+ self.state_permissions: Dict[str, set] = {}
199
+
200
+ # ABAC: Conditional permissions (Context-Aware Graph)
201
+ # Maps agent_role -> list of conditional permissions
202
+ self.conditional_permissions: Dict[str, List[ConditionalPermission]] = {}
203
+ # Context data for ABAC evaluation (e.g., user_status, time_of_day, etc.)
204
+ self.agent_contexts: Dict[str, Dict[str, Any]] = {}
205
+
206
+ # Configurable dangerous patterns for code/command execution
207
+ # Uses regex patterns for better detection
208
+ self.dangerous_code_patterns: List[re.Pattern] = [
209
+ re.compile(r"\brm\s+-rf\b", re.IGNORECASE),
210
+ re.compile(r"\bdel\s+/f\b", re.IGNORECASE),
211
+ re.compile(r"\bformat\s+", re.IGNORECASE),
212
+ re.compile(r"\bdrop\s+table\b", re.IGNORECASE),
213
+ re.compile(r"\bdrop\s+database\b", re.IGNORECASE),
214
+ re.compile(r"\btruncate\s+table\b", re.IGNORECASE),
215
+ re.compile(r"\bdelete\s+from\b", re.IGNORECASE),
216
+ ]
217
+
218
+ # Configurable system paths to protect
219
+ self.protected_paths: List[str] = [
220
+ "/etc/",
221
+ "/sys/",
222
+ "/proc/",
223
+ "/dev/",
224
+ "C:\\Windows\\System32",
225
+ ]
226
+
227
+ # Immutability controls — call freeze() after initial configuration
228
+ self._frozen: bool = False
229
+ self._mutation_log: List[Dict[str, Any]] = []
230
+
231
+ # ── Immutability ────────────────────────────────────────────
232
+
233
+ def freeze(self) -> None:
234
+ """Freeze the policy engine, preventing further mutations.
235
+
236
+ After calling ``freeze()``, any attempt to call ``add_constraint()``,
237
+ ``set_agent_context()``, ``update_agent_context()``, or
238
+ ``add_conditional_permission()`` will raise ``RuntimeError``.
239
+
240
+ In addition to the boolean guard, the underlying data structures
241
+ are replaced with immutable proxies (``MappingProxyType``) so that
242
+ direct attribute access (bypassing the setter methods) will also
243
+ raise ``TypeError``.
244
+
245
+ This addresses the self-modification attack vector where an agent
246
+ could call mutation methods to weaken its own policy at runtime.
247
+ """
248
+ self._frozen = True
249
+ # Replace mutable dicts with read-only proxies to harden against
250
+ # direct attribute manipulation (e.g. engine.state_permissions["x"] = ...)
251
+ self.state_permissions = MappingProxyType(
252
+ {k: frozenset(v) for k, v in self.state_permissions.items()}
253
+ )
254
+ self.agent_contexts = MappingProxyType(
255
+ {k: MappingProxyType(v) if isinstance(v, dict) else v
256
+ for k, v in self.agent_contexts.items()}
257
+ )
258
+ self.conditional_permissions = MappingProxyType(
259
+ {k: tuple(v) for k, v in self.conditional_permissions.items()}
260
+ )
261
+ self._log_mutation("freeze", {})
262
+ logger.info("PolicyEngine frozen — data structures converted to immutable proxies")
263
+
264
+ @property
265
+ def is_frozen(self) -> bool:
266
+ """Whether the policy engine is currently frozen."""
267
+ return self._frozen
268
+
269
+ @property
270
+ def mutation_log(self) -> List[Dict[str, Any]]:
271
+ """Read-only copy of the mutation audit trail."""
272
+ return list(self._mutation_log)
273
+
274
+ def _assert_mutable(self, operation: str) -> None:
275
+ """Raise RuntimeError if the engine is frozen."""
276
+ if self._frozen:
277
+ violation = {
278
+ "operation": operation,
279
+ "timestamp": datetime.now().isoformat(),
280
+ "blocked": True,
281
+ }
282
+ self._mutation_log.append(violation)
283
+ logger.warning(
284
+ "Blocked mutation '%s' on frozen PolicyEngine", operation
285
+ )
286
+ raise RuntimeError(
287
+ f"PolicyEngine is frozen — cannot perform '{operation}'. "
288
+ "Call freeze() is irreversible to prevent runtime self-modification."
289
+ )
290
+
291
+ def _log_mutation(self, operation: str, details: Dict[str, Any]) -> None:
292
+ """Record a mutation in the audit log."""
293
+ self._mutation_log.append({
294
+ "operation": operation,
295
+ "details": details,
296
+ "timestamp": datetime.now().isoformat(),
297
+ "blocked": False,
298
+ })
299
+
300
+ def set_quota(self, agent_id: str, quota: ResourceQuota):
301
+ """Set resource quota for an agent"""
302
+ self.quotas[agent_id] = quota
303
+
304
+ def set_risk_policy(self, policy_id: str, policy: RiskPolicy):
305
+ """Set a risk policy"""
306
+ self.risk_policies[policy_id] = policy
307
+
308
+ def add_custom_rule(self, rule: PolicyRule):
309
+ """Add a custom policy rule"""
310
+ self.custom_rules.append(rule)
311
+ self.custom_rules.sort(key=lambda r: r.priority, reverse=True)
312
+
313
+ def add_constraint(self, role: str, allowed_tools: List[str]):
314
+ """
315
+ Define the 'Physics' of the agent using allow-list approach.
316
+
317
+ This implements "Scale by Subtraction" - by defining what IS allowed,
318
+ everything else is implicitly blocked.
319
+
320
+ Args:
321
+ role: The agent role/ID
322
+ allowed_tools: List of tool names this role can use
323
+
324
+ Raises:
325
+ RuntimeError: If the engine has been frozen.
326
+ """
327
+ self._assert_mutable("add_constraint")
328
+ self.state_permissions[role] = set(allowed_tools)
329
+ self._log_mutation("add_constraint", {"role": role, "tools": allowed_tools})
330
+
331
+ def add_conditional_permission(self, agent_role: str, permission: ConditionalPermission):
332
+ """
333
+ Add a conditional permission for ABAC (Attribute-Based Access Control).
334
+
335
+ This moves from RBAC to ABAC, allowing context-aware policies like:
336
+ "Agent can call refund_user IF AND ONLY IF user_status == 'verified'"
337
+
338
+ Args:
339
+ agent_role: The agent role/ID
340
+ permission: The conditional permission to add
341
+
342
+ Raises:
343
+ RuntimeError: If the engine has been frozen.
344
+ """
345
+ self._assert_mutable("add_conditional_permission")
346
+
347
+ if agent_role not in self.conditional_permissions:
348
+ self.conditional_permissions[agent_role] = []
349
+
350
+ self.conditional_permissions[agent_role].append(permission)
351
+
352
+ # Also add the tool to the basic allow-list so it passes the first check
353
+ # The conditional check will happen later
354
+ if agent_role not in self.state_permissions:
355
+ self.state_permissions[agent_role] = set()
356
+ self.state_permissions[agent_role].add(permission.tool_name)
357
+ self._log_mutation(
358
+ "add_conditional_permission",
359
+ {"role": agent_role, "tool": permission.tool_name},
360
+ )
361
+
362
+ def set_agent_context(self, agent_role: str, context: Dict[str, Any]):
363
+ """
364
+ Set the context data for an agent for ABAC evaluation.
365
+
366
+ Args:
367
+ agent_role: The agent role/ID
368
+ context: Dictionary of context attributes (e.g., {"user_status": "verified", "time_of_day": "business_hours"})
369
+
370
+ Raises:
371
+ RuntimeError: If the engine has been frozen.
372
+ """
373
+ self._assert_mutable("set_agent_context")
374
+ self.agent_contexts[agent_role] = context
375
+ self._log_mutation("set_agent_context", {"role": agent_role})
376
+
377
+ def update_agent_context(self, agent_role: str, updates: Dict[str, Any]):
378
+ """
379
+ Update specific context attributes for an agent.
380
+
381
+ Args:
382
+ agent_role: The agent role/ID
383
+ updates: Dictionary of attributes to update
384
+
385
+ Raises:
386
+ RuntimeError: If the engine has been frozen.
387
+ """
388
+ self._assert_mutable("update_agent_context")
389
+
390
+ if agent_role not in self.agent_contexts:
391
+ self.agent_contexts[agent_role] = {}
392
+
393
+ self.agent_contexts[agent_role].update(updates)
394
+ self._log_mutation(
395
+ "update_agent_context",
396
+ {"role": agent_role, "keys": list(updates.keys())},
397
+ )
398
+
399
+ def is_shadow_mode(self, agent_role: str) -> bool:
400
+ """
401
+ Check if an agent is in shadow mode.
402
+
403
+ Args:
404
+ agent_role: The agent role/ID
405
+
406
+ Returns:
407
+ True if agent is in shadow mode, False otherwise
408
+ """
409
+ context = self.agent_contexts.get(agent_role, {})
410
+ return context.get("shadow_mode", False)
411
+
412
+ def check_violation(
413
+ self, agent_role: str, tool_name: str, args: Dict[str, Any]
414
+ ) -> Optional[str]:
415
+ """
416
+ Check if an action violates the constraint graph.
417
+
418
+ Uses a three-level check:
419
+ 1. Role-Based Check: Is this tool allowed for this role?
420
+ 2. Condition-Based Check (ABAC): Are the conditions met?
421
+ 3. Argument-Based Check: Are the arguments safe?
422
+
423
+ Returns:
424
+ None if no violation, or a string describing the violation
425
+ """
426
+ # 1. Role-Based Check (Allow-list approach)
427
+ allowed = self.state_permissions.get(agent_role, set())
428
+ if tool_name not in allowed:
429
+ return f"Role {agent_role} cannot use tool {tool_name}"
430
+
431
+ # 2. Condition-Based Check (ABAC)
432
+ # Check if there are conditional permissions for this agent/tool
433
+ if agent_role in self.conditional_permissions:
434
+ for cond_perm in self.conditional_permissions[agent_role]:
435
+ if cond_perm.tool_name == tool_name:
436
+ # Build evaluation context
437
+ eval_context = {
438
+ "args": args,
439
+ "context": self.agent_contexts.get(agent_role, {}),
440
+ }
441
+ # Merge top-level context attributes
442
+ eval_context.update(self.agent_contexts.get(agent_role, {}))
443
+
444
+ # Check if conditions are met
445
+ if not cond_perm.is_allowed(eval_context):
446
+ return f"Conditional permission denied for {tool_name}: Conditions not met"
447
+
448
+ # 3. Argument-Based Check
449
+
450
+ # 3a. Path validation with normalization to prevent traversal attacks
451
+ if tool_name in ["write_file", "read_file", "delete_file"] and "path" in args:
452
+ path = args.get("path", "")
453
+
454
+ # Reject paths with control characters (newlines, etc.) — prompt injection vector
455
+ if any(c in path for c in ["\n", "\r", "\x00"]):
456
+ return "Path Validation Error: Control characters in path"
457
+
458
+ # Check raw path against protected paths (cross-platform)
459
+ for protected in self.protected_paths:
460
+ if path.startswith(protected):
461
+ return f"Path Violation: Cannot access protected directory {protected}"
462
+
463
+ # Normalize path to resolve '..' and symbolic links
464
+ try:
465
+ normalized_path = os.path.normpath(os.path.abspath(path))
466
+ except (ValueError, OSError):
467
+ return "Path Validation Error: Invalid path format"
468
+
469
+ # Check normalized path against protected paths
470
+ for protected in self.protected_paths:
471
+ if normalized_path.startswith(os.path.normpath(protected)):
472
+ return f"Path Violation: Cannot access protected directory {protected}"
473
+
474
+ # 3b. Code execution validation using regex patterns
475
+ if tool_name in ["execute_code", "run_command"]:
476
+ code_or_cmd = args.get("code", args.get("command", ""))
477
+
478
+ # Check against dangerous patterns using regex
479
+ for pattern in self.dangerous_code_patterns:
480
+ if pattern.search(code_or_cmd):
481
+ return f"Dangerous pattern detected: {pattern.pattern}"
482
+
483
+ # 3c. SQL injection / destructive query validation
484
+ if tool_name in ["database_query", "database_write"]:
485
+ query = args.get("query", "")
486
+ destructive_patterns = [
487
+ r"\bDROP\s+", r"\bDELETE\s+FROM\b", r"\bTRUNCATE\s+",
488
+ r"\bALTER\s+TABLE\b.*\bDROP\b", r"\bUPDATE\s+.*\bSET\b.*\bWHERE\s+1\s*=\s*1",
489
+ ]
490
+ import re as _re
491
+ for pat in destructive_patterns:
492
+ if _re.search(pat, query, _re.IGNORECASE):
493
+ return f"Destructive SQL blocked: {pat}"
494
+
495
+ # 3d. Internal endpoint protection
496
+ if tool_name == "api_call":
497
+ endpoint = args.get("endpoint", "")
498
+ if endpoint.startswith("internal://"):
499
+ return f"Internal endpoint blocked: {endpoint}"
500
+
501
+ return None
502
+
503
+ def check_rate_limit(self, request: ExecutionRequest) -> bool:
504
+ """Check if request is within rate limits"""
505
+ agent_id = request.agent_context.agent_id
506
+
507
+ if agent_id not in self.quotas:
508
+ # No quota set, allow by default (or could deny by default)
509
+ return True
510
+
511
+ quota = self.quotas[agent_id]
512
+ now = datetime.now()
513
+
514
+ # Reset counters if needed
515
+ if (now - quota.last_reset_minute).total_seconds() >= 60:
516
+ quota.requests_this_minute = 0
517
+ quota.last_reset_minute = now
518
+
519
+ if (now - quota.last_reset_hour).total_seconds() >= 3600:
520
+ quota.requests_this_hour = 0
521
+ quota.last_reset_hour = now
522
+
523
+ # Check limits
524
+ if quota.requests_this_minute >= quota.max_requests_per_minute:
525
+ return False
526
+
527
+ if quota.requests_this_hour >= quota.max_requests_per_hour:
528
+ return False
529
+
530
+ if quota.current_executions >= quota.max_concurrent_executions:
531
+ return False
532
+
533
+ # Check action type allowed
534
+ if quota.allowed_action_types and request.action_type not in quota.allowed_action_types:
535
+ return False
536
+
537
+ # Update counters
538
+ quota.requests_this_minute += 1
539
+ quota.requests_this_hour += 1
540
+
541
+ return True
542
+
543
+ def validate_risk(self, request: ExecutionRequest, risk_score: float) -> bool:
544
+ """Validate request against risk policies"""
545
+ # Check against all risk policies
546
+ for policy_id, policy in self.risk_policies.items():
547
+ # Check if risk score exceeds limits
548
+ if risk_score >= policy.deny_above:
549
+ return False
550
+
551
+ # Check parameters for high-risk patterns
552
+ params_str = str(request.parameters)
553
+ for pattern in policy.high_risk_patterns:
554
+ if pattern.lower() in params_str.lower():
555
+ return False
556
+
557
+ # Check domain restrictions if applicable
558
+ if "url" in request.parameters or "domain" in request.parameters:
559
+ url = request.parameters.get("url", request.parameters.get("domain", ""))
560
+
561
+ # Check blocked domains
562
+ for blocked in policy.blocked_domains:
563
+ if blocked in url:
564
+ return False
565
+
566
+ # Check allowed domains (if list is not empty, only allow listed domains)
567
+ if policy.allowed_domains:
568
+ allowed = False
569
+ for allowed_domain in policy.allowed_domains:
570
+ if allowed_domain in url:
571
+ allowed = True
572
+ break
573
+ if not allowed:
574
+ return False
575
+
576
+ return True
577
+
578
+ def validate_request(self, request: ExecutionRequest) -> Tuple[bool, Optional[str]]:
579
+ """
580
+ Comprehensive validation of a request
581
+ Returns (is_valid, reason_if_invalid)
582
+ """
583
+ # Check rate limits
584
+ if not self.check_rate_limit(request):
585
+ return False, "rate_limit_exceeded"
586
+
587
+ # Check custom rules
588
+ for rule in self.custom_rules:
589
+ if request.action_type in rule.action_types:
590
+ if not rule.validator(request):
591
+ return False, f"policy_violation: {rule.name}"
592
+
593
+ return True, None
594
+
595
+ def get_quota_status(self, agent_id: str) -> Dict[str, Any]:
596
+ """Get current quota usage for an agent"""
597
+ if agent_id not in self.quotas:
598
+ return {"error": "No quota set for agent"}
599
+
600
+ quota = self.quotas[agent_id]
601
+ return {
602
+ "agent_id": agent_id,
603
+ "requests_this_minute": quota.requests_this_minute,
604
+ "max_requests_per_minute": quota.max_requests_per_minute,
605
+ "requests_this_hour": quota.requests_this_hour,
606
+ "max_requests_per_hour": quota.max_requests_per_hour,
607
+ "current_executions": quota.current_executions,
608
+ "max_concurrent_executions": quota.max_concurrent_executions,
609
+ }
610
+
611
+
612
+ @dataclass
613
+ class SQLPolicyConfig:
614
+ """Configuration for SQL policy rules, loadable from YAML.
615
+
616
+ Attributes:
617
+ blocked_statements: SQL statement types to block (e.g., DROP, GRANT).
618
+ require_where_clause: Statements blocked only when missing WHERE.
619
+ blocked_create_types: CREATE subtypes to block (e.g., USER, ROLE).
620
+ blocked_patterns: Regex patterns for vendor-specific blocking.
621
+ disclaimer: Disclaimer text shown in logs.
622
+ """
623
+ blocked_statements: List[str] = field(default_factory=lambda: [
624
+ "DROP", "TRUNCATE", "ALTER", "GRANT", "REVOKE", "MERGE",
625
+ ])
626
+ require_where_clause: List[str] = field(default_factory=lambda: [
627
+ "DELETE", "UPDATE",
628
+ ])
629
+ blocked_create_types: List[str] = field(default_factory=lambda: [
630
+ "USER", "ROLE", "LOGIN",
631
+ ])
632
+ blocked_patterns: List[str] = field(default_factory=lambda: [
633
+ r'\bEXEC(UTE)?\s+XP_CMDSHELL\b',
634
+ r'\bEXEC(UTE)?\s+SP_CONFIGURE\b',
635
+ r'\bEXEC(UTE)?\s+SP_ADDROLEMEMBER\b',
636
+ r'\bLOAD_FILE\s*\(',
637
+ r'\bINTO\s+(OUT|DUMP)FILE\b',
638
+ r'\bLOAD\s+DATA\b',
639
+ r'\bMERGE\s+INTO\b',
640
+ ])
641
+ disclaimer: str = ""
642
+
643
+
644
+ def load_sql_policy_config(path: str) -> SQLPolicyConfig:
645
+ """Load SQL policy configuration from a YAML file.
646
+
647
+ Args:
648
+ path: Path to a YAML file with ``sql_policy`` section.
649
+
650
+ Returns:
651
+ SQLPolicyConfig populated from the YAML data.
652
+
653
+ Raises:
654
+ FileNotFoundError: If the config file does not exist.
655
+ ValueError: If the YAML is missing the ``sql_policy`` section.
656
+
657
+ Example::
658
+
659
+ config = load_sql_policy_config("examples/policies/sql-safety.yaml")
660
+ rules = create_sql_policy_from_config(config)
661
+ """
662
+ import yaml
663
+
664
+ if not os.path.exists(path):
665
+ raise FileNotFoundError(f"SQL policy config not found: {path}")
666
+
667
+ with open(path, "r", encoding="utf-8") as f:
668
+ data = yaml.safe_load(f.read())
669
+
670
+ if not isinstance(data, dict) or "sql_policy" not in data:
671
+ raise ValueError(
672
+ f"YAML file must contain a 'sql_policy' section: {path}"
673
+ )
674
+
675
+ sp = data["sql_policy"]
676
+ return SQLPolicyConfig(
677
+ blocked_statements=[s.upper() for s in sp.get("blocked_statements", [])],
678
+ require_where_clause=[s.upper() for s in sp.get("require_where_clause", [])],
679
+ blocked_create_types=[s.upper() for s in sp.get("blocked_create_types", [])],
680
+ blocked_patterns=sp.get("blocked_patterns", []),
681
+ disclaimer=data.get("disclaimer", ""),
682
+ )
683
+
684
+
685
+ def _fallback_sql_check(query: str, config: Optional[SQLPolicyConfig] = None) -> bool:
686
+ """
687
+ Fallback SQL check when sqlglot is not available.
688
+
689
+ Uses regex pattern matching. Rules are driven by *config*; when
690
+ *config* is ``None`` a built-in default set is used.
691
+ """
692
+ if config is None:
693
+ config = SQLPolicyConfig()
694
+
695
+ query_upper = query.upper()
696
+ # Remove comments to prevent bypass
697
+ query_clean = re.sub(r'/\*.*?\*/', '', query_upper, flags=re.DOTALL)
698
+ query_clean = re.sub(r'--.*$', '', query_clean, flags=re.MULTILINE)
699
+
700
+ # Build patterns dynamically from config
701
+ patterns: List[str] = []
702
+
703
+ for stmt in config.blocked_statements:
704
+ if stmt == "DROP":
705
+ patterns.append(r'\bDROP\s+(TABLE|DATABASE|INDEX|VIEW|SCHEMA|PROCEDURE|FUNCTION|TRIGGER|MATERIALIZED\s+VIEW|ROLE|USER)\b')
706
+ elif stmt == "TRUNCATE":
707
+ patterns.append(r'\bTRUNCATE\s+(TABLE\s+)?\w+')
708
+ elif stmt == "ALTER":
709
+ patterns.append(r'\bALTER\s+(TABLE|DATABASE|SCHEMA|ROLE|USER)\b')
710
+ elif stmt == "GRANT":
711
+ patterns.append(r'\bGRANT\b')
712
+ elif stmt == "REVOKE":
713
+ patterns.append(r'\bREVOKE\b')
714
+ elif stmt == "MERGE":
715
+ patterns.append(r'\bMERGE\s+INTO\b')
716
+ elif stmt == "INSERT":
717
+ patterns.append(r'\bINSERT\s+INTO\b')
718
+ elif stmt in ("UPDATE", "DELETE"):
719
+ patterns.append(rf'\b{stmt}\b')
720
+
721
+ for stmt in config.require_where_clause:
722
+ if stmt == "DELETE":
723
+ patterns.append(r'\bDELETE\s+FROM\s+\w+\s*(;|$)')
724
+ elif stmt == "UPDATE":
725
+ patterns.append(r'\bUPDATE\s+\w+\s+SET\b(?!.*\bWHERE\b)')
726
+
727
+ for ct in config.blocked_create_types:
728
+ patterns.append(rf'\bCREATE\s+{ct}\b')
729
+ patterns.append(rf'\bALTER\s+{ct}\b')
730
+ patterns.append(rf'\bDROP\s+{ct}\b')
731
+
732
+ patterns.extend(config.blocked_patterns)
733
+
734
+ for pattern in patterns:
735
+ if re.search(pattern, query_clean):
736
+ return False
737
+ return True
738
+
739
+
740
+ def create_policies_from_config(
741
+ sql_config_path: Optional[str] = None,
742
+ sql_config: Optional[SQLPolicyConfig] = None,
743
+ ) -> List[PolicyRule]:
744
+ """Create security policies with SQL rules driven by external config.
745
+
746
+ Load SQL policy rules from a YAML config file or a pre-built
747
+ ``SQLPolicyConfig`` object. Non-SQL policies (file access, credential
748
+ exposure) use built-in defaults.
749
+
750
+ Args:
751
+ sql_config_path: Path to a YAML file with ``sql_policy`` section.
752
+ sql_config: Pre-built config object (takes precedence over path).
753
+
754
+ Returns:
755
+ List of PolicyRule instances.
756
+
757
+ Example::
758
+
759
+ # From YAML file
760
+ rules = create_policies_from_config("examples/policies/sql-safety.yaml")
761
+
762
+ # From explicit config
763
+ cfg = SQLPolicyConfig(blocked_statements=["DROP", "GRANT"])
764
+ rules = create_policies_from_config(sql_config=cfg)
765
+ """
766
+ if sql_config is None and sql_config_path is not None:
767
+ sql_config = load_sql_policy_config(sql_config_path)
768
+ if sql_config is None:
769
+ sql_config = SQLPolicyConfig()
770
+
771
+ return _build_policy_rules(sql_config)
772
+
773
+
774
+ def create_default_policies() -> List[PolicyRule]:
775
+ """Create a set of default security policies.
776
+
777
+ .. deprecated::
778
+ The built-in rules are **samples** and are not guaranteed to be
779
+ exhaustive. Use :func:`create_policies_from_config` with an
780
+ explicit YAML config file for production deployments.
781
+ See ``examples/policies/`` for sample configurations.
782
+ """
783
+ warnings.warn(
784
+ "create_default_policies() uses built-in sample rules that may not "
785
+ "cover all destructive SQL operations. For production use, load an "
786
+ "explicit policy config with create_policies_from_config(). "
787
+ "See examples/policies/sql-safety.yaml for a sample configuration.",
788
+ stacklevel=2,
789
+ )
790
+ return _build_policy_rules(SQLPolicyConfig())
791
+
792
+
793
+ def _build_policy_rules(sql_config: SQLPolicyConfig) -> List[PolicyRule]:
794
+
795
+ def no_system_file_access(request: ExecutionRequest) -> bool:
796
+ """Prevent access to system files"""
797
+ if request.action_type in [ActionType.FILE_READ, ActionType.FILE_WRITE]:
798
+ path = request.parameters.get("path", "")
799
+ dangerous_paths = ["/etc/", "/sys/", "/proc/", "/dev/", "C:\\Windows\\System32"]
800
+ return not any(dp in path for dp in dangerous_paths)
801
+ return True
802
+
803
+ def no_credential_exposure(request: ExecutionRequest) -> bool:
804
+ """Prevent exposure of credentials"""
805
+ params_str = str(request.parameters).lower()
806
+ sensitive_keywords = ["password", "secret", "api_key", "token", "credential"]
807
+ # This is a simple check; real implementation would be more sophisticated
808
+ return not any(keyword in params_str for keyword in sensitive_keywords)
809
+
810
+ def no_destructive_sql(request: ExecutionRequest) -> bool:
811
+ """
812
+ Prevent destructive SQL operations using AST-level parsing.
813
+
814
+ Uses sqlglot for proper SQL parsing to detect:
815
+ - DROP TABLE/DATABASE/INDEX/VIEW/USER/ROLE statements
816
+ - TRUNCATE statements
817
+ - DELETE without WHERE clause
818
+ - UPDATE without WHERE clause
819
+ - ALTER TABLE/USER/ROLE statements
820
+ - GRANT / REVOKE privilege statements
821
+ - CREATE USER/ROLE/LOGIN statements
822
+ - EXEC/EXECUTE xp_cmdshell and other dangerous procedures
823
+ - MERGE INTO statements
824
+ - Dangerous file functions (LOAD_FILE, INTO OUTFILE)
825
+
826
+ This prevents bypass attempts like:
827
+ - Keywords in comments: /* DROP */ SELECT ...
828
+ - Keywords in strings: SELECT 'DROP TABLE'
829
+ - Obfuscated queries
830
+ """
831
+ if request.action_type not in (ActionType.DATABASE_QUERY, ActionType.DATABASE_WRITE):
832
+ return True
833
+
834
+ query = request.parameters.get("query", "")
835
+ if not query.strip():
836
+ return True
837
+
838
+ try:
839
+ # Try to import sqlglot for AST-level parsing
840
+ import sqlglot
841
+ from sqlglot import exp
842
+
843
+ # Parse the SQL query into AST
844
+ try:
845
+ statements = sqlglot.parse(query)
846
+ except Exception:
847
+ # Fail-closed: deny when SQL parsing fails
848
+ logger.warning("SQL parsing failed — denying query as fail-safe.")
849
+ return False
850
+
851
+ for statement in statements:
852
+ if statement is None:
853
+ continue
854
+
855
+ # Check for DROP statements (tables, databases, users, roles, etc.)
856
+ if isinstance(statement, exp.Drop):
857
+ return False
858
+
859
+ # Check for TRUNCATE statements
860
+ if isinstance(statement, exp.Command) and statement.this.upper() == "TRUNCATE":
861
+ return False
862
+
863
+ # Check for DELETE without WHERE clause
864
+ if isinstance(statement, exp.Delete):
865
+ if statement.find(exp.Where) is None:
866
+ return False
867
+
868
+ # Check for UPDATE without WHERE clause
869
+ if isinstance(statement, exp.Update):
870
+ if statement.find(exp.Where) is None:
871
+ return False
872
+
873
+ # Check for ALTER statements
874
+ if isinstance(statement, exp.AlterTable):
875
+ return False
876
+
877
+ # Check for GRANT / REVOKE statements
878
+ if isinstance(statement, exp.Grant):
879
+ return False
880
+
881
+ # Check for MERGE statements (can do INSERT/UPDATE/DELETE)
882
+ if isinstance(statement, exp.Merge):
883
+ return False
884
+
885
+ # Check for CREATE USER/ROLE and ALTER USER/ROLE
886
+ if isinstance(statement, exp.Create):
887
+ kind = statement.args.get("kind", "")
888
+ if isinstance(kind, str) and kind.upper() in ("USER", "ROLE", "LOGIN"):
889
+ return False
890
+
891
+ # Catch GRANT, REVOKE, EXEC, CREATE USER via Command nodes
892
+ # (sqlglot may parse some vendor-specific SQL as Command)
893
+ if isinstance(statement, exp.Command):
894
+ cmd = statement.this.upper() if statement.this else ""
895
+ if cmd in ("GRANT", "REVOKE", "EXEC", "EXECUTE", "MERGE"):
896
+ return False
897
+ # Block CREATE USER/ROLE/LOGIN parsed as Command
898
+ if cmd == "CREATE":
899
+ expr_text = statement.sql().upper()
900
+ if any(kw in expr_text for kw in ("USER", "ROLE", "LOGIN")):
901
+ return False
902
+
903
+ # Check for dangerous functions in any statement
904
+ for func in statement.find_all(exp.Func):
905
+ func_name = func.name.upper() if func.name else ""
906
+ if func_name in ("LOAD_FILE", "INTO OUTFILE", "INTO DUMPFILE"):
907
+ return False
908
+
909
+ # Check for EXEC xp_cmdshell and other dangerous procs
910
+ # in the full SQL text of the statement
911
+ stmt_sql = statement.sql().upper()
912
+ if re.search(r'\bEXEC(UTE)?\s+XP_CMDSHELL\b', stmt_sql):
913
+ return False
914
+ if re.search(r'\bEXEC(UTE)?\s+SP_CONFIGURE\b', stmt_sql):
915
+ return False
916
+ if re.search(r'\bEXEC(UTE)?\s+SP_ADDROLEMEMBER\b', stmt_sql):
917
+ return False
918
+
919
+ return True
920
+
921
+ except ImportError:
922
+ # Fail-closed: deny when sqlglot is not available
923
+ logger.warning(
924
+ "sqlglot not installed — denying SQL query as fail-safe. "
925
+ "Install sqlglot for proper SQL validation."
926
+ )
927
+ return False
928
+
929
+ return [
930
+ PolicyRule(
931
+ rule_id=str(uuid.uuid4()),
932
+ name="no_system_file_access",
933
+ description="Prevent access to system files",
934
+ action_types=[ActionType.FILE_READ, ActionType.FILE_WRITE],
935
+ validator=no_system_file_access,
936
+ priority=10,
937
+ ),
938
+ PolicyRule(
939
+ rule_id=str(uuid.uuid4()),
940
+ name="no_credential_exposure",
941
+ description="Prevent exposure of credentials",
942
+ action_types=[ActionType.CODE_EXECUTION, ActionType.FILE_READ, ActionType.API_CALL],
943
+ validator=no_credential_exposure,
944
+ priority=10,
945
+ ),
946
+ PolicyRule(
947
+ rule_id=str(uuid.uuid4()),
948
+ name="no_destructive_sql",
949
+ description="Prevent destructive SQL operations",
950
+ action_types=[ActionType.DATABASE_QUERY, ActionType.DATABASE_WRITE],
951
+ validator=no_destructive_sql,
952
+ priority=9,
953
+ ),
954
+ ]