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.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- 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
|
+
]
|