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
agent_os/lite.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""AGT Lite — Zero-config governance in 3 lines.
|
|
4
|
+
|
|
5
|
+
The full Agent OS is powerful but heavy (530 files, 42s import).
|
|
6
|
+
AGT Lite is the lightweight alternative: single import, inline rules,
|
|
7
|
+
no YAML, no external deps beyond pydantic. Designed for the developer
|
|
8
|
+
who just wants to add basic governance without learning the full stack.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from agent_os.lite import govern
|
|
12
|
+
|
|
13
|
+
# One line to create a governance function
|
|
14
|
+
check = govern(allow=["read_file", "web_search"], deny=["execute_code", "delete_file"])
|
|
15
|
+
|
|
16
|
+
# One line to check any action
|
|
17
|
+
check("read_file") # returns True
|
|
18
|
+
check("execute_code") # raises GovernanceViolation
|
|
19
|
+
|
|
20
|
+
# Or use the non-raising version
|
|
21
|
+
check.is_allowed("execute_code") # returns False
|
|
22
|
+
|
|
23
|
+
That's it. No YAML, no PolicyEvaluator, no 42-second import.
|
|
24
|
+
Upgrade to the full stack when you need trust mesh, SRE, or compliance.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
import time
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GovernanceViolation(Exception):
|
|
36
|
+
"""Raised when an action is blocked by governance policy."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, action: str, reason: str) -> None:
|
|
39
|
+
self.action = action
|
|
40
|
+
self.reason = reason
|
|
41
|
+
super().__init__(f"Governance violation: '{action}' — {reason}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GovernanceDecision:
|
|
45
|
+
"""Result of a governance check."""
|
|
46
|
+
|
|
47
|
+
__slots__ = ("action", "allowed", "reason", "timestamp", "latency_ms")
|
|
48
|
+
|
|
49
|
+
def __init__(self, action: str, allowed: bool, reason: str, latency_ms: float) -> None:
|
|
50
|
+
self.action = action
|
|
51
|
+
self.allowed = allowed
|
|
52
|
+
self.reason = reason
|
|
53
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
54
|
+
self.latency_ms = latency_ms
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LiteGovernor:
|
|
58
|
+
"""Lightweight, zero-config governance gate.
|
|
59
|
+
|
|
60
|
+
Rules:
|
|
61
|
+
1. If action is in `deny` list → BLOCKED
|
|
62
|
+
2. If action matches a `deny_patterns` regex → BLOCKED
|
|
63
|
+
3. If `allow` list is set and action is NOT in it → BLOCKED
|
|
64
|
+
4. If content matches `blocked_content` patterns → BLOCKED
|
|
65
|
+
5. Otherwise → ALLOWED
|
|
66
|
+
|
|
67
|
+
Deny takes priority over allow (fail-secure).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
allow: list[str] | None = None,
|
|
73
|
+
deny: list[str] | None = None,
|
|
74
|
+
deny_patterns: list[str] | None = None,
|
|
75
|
+
blocked_content: list[str] | None = None,
|
|
76
|
+
escalate: list[str] | None = None,
|
|
77
|
+
max_calls: int = 0,
|
|
78
|
+
log: bool = True,
|
|
79
|
+
) -> None:
|
|
80
|
+
self._allow = set(allow) if allow else None
|
|
81
|
+
self._deny = set(deny or [])
|
|
82
|
+
self._deny_patterns = [re.compile(p) for p in (deny_patterns or [])]
|
|
83
|
+
self._blocked_content = [re.compile(p) for p in (blocked_content or [])]
|
|
84
|
+
self._escalate = set(escalate or [])
|
|
85
|
+
self._max_calls = max_calls
|
|
86
|
+
self._log = log
|
|
87
|
+
self._call_count = 0
|
|
88
|
+
self._audit: list[GovernanceDecision] = []
|
|
89
|
+
|
|
90
|
+
def __call__(self, action: str, content: str = "", **context: Any) -> bool:
|
|
91
|
+
"""Check if action is allowed. Raises GovernanceViolation if not."""
|
|
92
|
+
decision = self.evaluate(action, content, **context)
|
|
93
|
+
if not decision.allowed:
|
|
94
|
+
raise GovernanceViolation(action, decision.reason)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def is_allowed(self, action: str, content: str = "", **context: Any) -> bool:
|
|
98
|
+
"""Check if action is allowed. Returns bool (non-raising)."""
|
|
99
|
+
return self.evaluate(action, content, **context).allowed
|
|
100
|
+
|
|
101
|
+
def evaluate(self, action: str, content: str = "", **context: Any) -> GovernanceDecision:
|
|
102
|
+
"""Evaluate an action against policy. Returns GovernanceDecision."""
|
|
103
|
+
start = time.perf_counter()
|
|
104
|
+
|
|
105
|
+
# Rate limit check
|
|
106
|
+
if self._max_calls > 0:
|
|
107
|
+
self._call_count += 1
|
|
108
|
+
if self._call_count > self._max_calls:
|
|
109
|
+
return self._decide(action, False, f"Rate limit exceeded ({self._max_calls} max)", start)
|
|
110
|
+
|
|
111
|
+
# Deny list (highest priority)
|
|
112
|
+
if action in self._deny:
|
|
113
|
+
return self._decide(action, False, f"Action '{action}' is explicitly denied", start)
|
|
114
|
+
|
|
115
|
+
# Deny patterns
|
|
116
|
+
for pattern in self._deny_patterns:
|
|
117
|
+
if pattern.search(action):
|
|
118
|
+
return self._decide(action, False, f"Action '{action}' matches deny pattern", start)
|
|
119
|
+
|
|
120
|
+
# Content check
|
|
121
|
+
if content:
|
|
122
|
+
for pattern in self._blocked_content:
|
|
123
|
+
if pattern.search(content):
|
|
124
|
+
return self._decide(action, False, "Content matches blocked pattern", start)
|
|
125
|
+
|
|
126
|
+
# Allow list (if set, only listed actions are allowed)
|
|
127
|
+
if self._allow is not None and action not in self._allow:
|
|
128
|
+
return self._decide(action, False, f"Action '{action}' not in allow list", start)
|
|
129
|
+
|
|
130
|
+
return self._decide(action, True, "Allowed by policy", start)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def audit_trail(self) -> list[GovernanceDecision]:
|
|
134
|
+
"""Get all governance decisions made."""
|
|
135
|
+
return list(self._audit)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def stats(self) -> dict[str, Any]:
|
|
139
|
+
"""Get governance statistics."""
|
|
140
|
+
total = len(self._audit)
|
|
141
|
+
allowed = sum(1 for d in self._audit if d.allowed)
|
|
142
|
+
denied = total - allowed
|
|
143
|
+
avg_latency = (
|
|
144
|
+
sum(d.latency_ms for d in self._audit) / total if total else 0
|
|
145
|
+
)
|
|
146
|
+
return {
|
|
147
|
+
"total": total,
|
|
148
|
+
"allowed": allowed,
|
|
149
|
+
"denied": denied,
|
|
150
|
+
"violation_rate": f"{denied/total*100:.1f}%" if total else "0%",
|
|
151
|
+
"avg_latency_ms": f"{avg_latency:.3f}",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def _decide(
|
|
155
|
+
self, action: str, allowed: bool, reason: str, start: float
|
|
156
|
+
) -> GovernanceDecision:
|
|
157
|
+
latency_ms = (time.perf_counter() - start) * 1000
|
|
158
|
+
decision = GovernanceDecision(action, allowed, reason, latency_ms)
|
|
159
|
+
if self._log:
|
|
160
|
+
self._audit.append(decision)
|
|
161
|
+
return decision
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def govern(
|
|
165
|
+
allow: list[str] | None = None,
|
|
166
|
+
deny: list[str] | None = None,
|
|
167
|
+
deny_patterns: list[str] | None = None,
|
|
168
|
+
blocked_content: list[str] | None = None,
|
|
169
|
+
escalate: list[str] | None = None,
|
|
170
|
+
max_calls: int = 0,
|
|
171
|
+
log: bool = True,
|
|
172
|
+
) -> LiteGovernor:
|
|
173
|
+
"""Create a lightweight governance gate.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
allow: Actions to allow (allowlist). If set, only these actions pass.
|
|
177
|
+
deny: Actions to explicitly deny (takes priority over allow).
|
|
178
|
+
deny_patterns: Regex patterns to deny.
|
|
179
|
+
blocked_content: Regex patterns to block in content.
|
|
180
|
+
escalate: Actions that require human approval (logged as denied).
|
|
181
|
+
max_calls: Max total calls before rate limiting (0 = unlimited).
|
|
182
|
+
log: Whether to keep audit trail.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
A LiteGovernor callable. Use as: `check("action_name")`
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
# Minimal — block dangerous, allow everything else
|
|
189
|
+
check = govern(deny=["execute_code", "delete_file", "ssh_connect"])
|
|
190
|
+
|
|
191
|
+
# Allowlist — only permit specific actions
|
|
192
|
+
check = govern(allow=["read_file", "web_search", "api_call"])
|
|
193
|
+
|
|
194
|
+
# With content filtering
|
|
195
|
+
check = govern(
|
|
196
|
+
allow=["read_file", "web_search"],
|
|
197
|
+
blocked_content=[r'\\b\\d{3}-\\d{2}-\\d{4}\\b'], # SSN
|
|
198
|
+
)
|
|
199
|
+
"""
|
|
200
|
+
return LiteGovernor(
|
|
201
|
+
allow=allow,
|
|
202
|
+
deny=deny,
|
|
203
|
+
deny_patterns=deny_patterns,
|
|
204
|
+
blocked_content=blocked_content,
|
|
205
|
+
escalate=escalate,
|
|
206
|
+
max_calls=max_calls,
|
|
207
|
+
log=log,
|
|
208
|
+
)
|
agent_os/mcp_gateway.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
MCP Security Gateway — Public Preview
|
|
5
|
+
|
|
6
|
+
A governance layer that sits between MCP clients and MCP servers,
|
|
7
|
+
enforcing policy-based controls on all tool calls passing through.
|
|
8
|
+
|
|
9
|
+
Addresses OWASP ASI02 (Tool Misuse & Exploitation) by providing:
|
|
10
|
+
- Tool allow/deny list filtering
|
|
11
|
+
- Parameter sanitization against dangerous patterns
|
|
12
|
+
- Per-agent rate limiting / call budget enforcement
|
|
13
|
+
- Structured audit logging of every tool invocation
|
|
14
|
+
- Human-in-the-loop approval for sensitive tools
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from typing import Any, Callable
|
|
27
|
+
|
|
28
|
+
from agent_os._mcp_metrics import MCPMetrics, MCPMetricsRecorder
|
|
29
|
+
from agent_os.credential_redactor import CredentialRedactor
|
|
30
|
+
from agent_os.integrations.base import GovernancePolicy, PatternType
|
|
31
|
+
from agent_os.mcp_protocols import (
|
|
32
|
+
InMemoryAuditSink,
|
|
33
|
+
InMemoryRateLimitStore,
|
|
34
|
+
MCPAuditSink,
|
|
35
|
+
MCPRateLimitStore,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Built-in dangerous parameter patterns (CE defaults) ─────────────────────
|
|
42
|
+
|
|
43
|
+
_BUILTIN_DANGEROUS_PATTERNS: list[tuple[str, PatternType]] = [
|
|
44
|
+
# PII / sensitive data
|
|
45
|
+
(r"\b\d{3}-\d{2}-\d{4}\b", PatternType.REGEX), # SSN
|
|
46
|
+
(r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b", PatternType.REGEX), # credit card
|
|
47
|
+
# Shell injection
|
|
48
|
+
(r";\s*(rm|del|format|mkfs)\b", PatternType.REGEX), # destructive cmds
|
|
49
|
+
(r"\$\(.*\)", PatternType.REGEX), # command substitution
|
|
50
|
+
(r"`[^`]+`", PatternType.REGEX), # backtick execution
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ApprovalStatus(Enum):
|
|
55
|
+
"""Result of a human-approval check."""
|
|
56
|
+
|
|
57
|
+
PENDING = "pending"
|
|
58
|
+
APPROVED = "approved"
|
|
59
|
+
DENIED = "denied"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class AuditEntry:
|
|
64
|
+
"""A single audit-log record for a tool call."""
|
|
65
|
+
|
|
66
|
+
timestamp: float
|
|
67
|
+
agent_id: str
|
|
68
|
+
tool_name: str
|
|
69
|
+
parameters: dict[str, Any]
|
|
70
|
+
allowed: bool
|
|
71
|
+
reason: str
|
|
72
|
+
approval_status: ApprovalStatus | None = None
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"timestamp": self.timestamp,
|
|
77
|
+
"agent_id": self.agent_id,
|
|
78
|
+
"tool_name": self.tool_name,
|
|
79
|
+
"parameters": self.parameters,
|
|
80
|
+
"allowed": self.allowed,
|
|
81
|
+
"reason": self.reason,
|
|
82
|
+
"approval_status": self.approval_status.value if self.approval_status else None,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class GatewayConfig:
|
|
88
|
+
"""Configuration returned by ``wrap_mcp_server``."""
|
|
89
|
+
|
|
90
|
+
server_config: dict[str, Any]
|
|
91
|
+
policy_name: str
|
|
92
|
+
allowed_tools: list[str]
|
|
93
|
+
denied_tools: list[str]
|
|
94
|
+
sensitive_tools: list[str]
|
|
95
|
+
rate_limit: int
|
|
96
|
+
builtin_sanitization: bool
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class MCPGateway:
|
|
100
|
+
"""Security gateway that sits between MCP clients and servers.
|
|
101
|
+
|
|
102
|
+
Enforces governance policies on all tool calls passing through,
|
|
103
|
+
providing defense against tool misuse, data exfiltration, and
|
|
104
|
+
unauthorized access (OWASP ASI02). The gateway redacts persisted audit
|
|
105
|
+
payloads, enforces a per-agent call budget, and fails closed whenever
|
|
106
|
+
policy evaluation or approval hooks raise unexpected errors.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
policy: GovernancePolicy,
|
|
112
|
+
*,
|
|
113
|
+
denied_tools: list[str] | None = None,
|
|
114
|
+
sensitive_tools: list[str] | None = None,
|
|
115
|
+
approval_callback: Callable[[str, str, dict[str, Any]], ApprovalStatus] | None = None,
|
|
116
|
+
enable_builtin_sanitization: bool = True,
|
|
117
|
+
metrics: MCPMetricsRecorder | None = None,
|
|
118
|
+
rate_limit_store: MCPRateLimitStore | None = None,
|
|
119
|
+
audit_sink: MCPAuditSink | None = None,
|
|
120
|
+
clock: Callable[[], float] = time.time,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Args:
|
|
124
|
+
policy: Governance policy defining constraints and thresholds.
|
|
125
|
+
denied_tools: Explicit deny-list — these tools are never exposed.
|
|
126
|
+
sensitive_tools: Tools that require human approval before execution.
|
|
127
|
+
approval_callback: Sync callback invoked for sensitive-tool approval.
|
|
128
|
+
Signature: ``(agent_id, tool_name, params) -> ApprovalStatus``.
|
|
129
|
+
enable_builtin_sanitization: When True, apply built-in dangerous-
|
|
130
|
+
parameter patterns in addition to the policy's blocked_patterns.
|
|
131
|
+
metrics: Optional metrics recorder for gateway events.
|
|
132
|
+
rate_limit_store: Optional persistence backend for per-agent call
|
|
133
|
+
counts.
|
|
134
|
+
audit_sink: Optional sink for persisted audit records.
|
|
135
|
+
clock: Clock used when recording audit timestamps.
|
|
136
|
+
"""
|
|
137
|
+
self.policy = policy
|
|
138
|
+
self.denied_tools: list[str] = denied_tools or []
|
|
139
|
+
self.sensitive_tools: list[str] = sensitive_tools or []
|
|
140
|
+
self.approval_callback = approval_callback
|
|
141
|
+
self.enable_builtin_sanitization = enable_builtin_sanitization
|
|
142
|
+
self._metrics = metrics or MCPMetrics()
|
|
143
|
+
self._rate_limit_store = rate_limit_store or InMemoryRateLimitStore()
|
|
144
|
+
self._audit_sink = audit_sink or InMemoryAuditSink()
|
|
145
|
+
self._clock = clock
|
|
146
|
+
|
|
147
|
+
# Per-agent call counters for rate limiting
|
|
148
|
+
self._tracked_agents: set[str] = set()
|
|
149
|
+
self._rate_limit_lock = threading.Lock()
|
|
150
|
+
# Audit log
|
|
151
|
+
self._audit_log: list[AuditEntry] = []
|
|
152
|
+
# Pre-compile built-in patterns
|
|
153
|
+
self._builtin_compiled: list[tuple[str, re.Pattern]] = []
|
|
154
|
+
if enable_builtin_sanitization:
|
|
155
|
+
for pat_str, _ in _BUILTIN_DANGEROUS_PATTERNS:
|
|
156
|
+
self._builtin_compiled.append((pat_str, re.compile(pat_str, re.IGNORECASE)))
|
|
157
|
+
|
|
158
|
+
# ── Core interception ────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
def intercept_tool_call(
|
|
161
|
+
self,
|
|
162
|
+
agent_id: str,
|
|
163
|
+
tool_name: str,
|
|
164
|
+
params: dict[str, Any],
|
|
165
|
+
) -> tuple[bool, str]:
|
|
166
|
+
"""Evaluate a tool call against the gateway's policy stack.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
agent_id: Agent identity attempting the tool invocation.
|
|
170
|
+
tool_name: Tool being requested.
|
|
171
|
+
params: Structured tool parameters to evaluate.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
``(allowed, reason)`` — *allowed* is True when the call may
|
|
175
|
+
proceed; *reason* explains the decision.
|
|
176
|
+
"""
|
|
177
|
+
stage = "error"
|
|
178
|
+
try:
|
|
179
|
+
allowed, reason, approval, stage = self._evaluate(agent_id, tool_name, params)
|
|
180
|
+
except Exception:
|
|
181
|
+
# Fail closed: deny access on unexpected evaluation errors
|
|
182
|
+
logger.error(
|
|
183
|
+
"MCP Gateway evaluation error — failing closed | agent=%s tool=%s",
|
|
184
|
+
agent_id,
|
|
185
|
+
tool_name,
|
|
186
|
+
exc_info=True,
|
|
187
|
+
)
|
|
188
|
+
allowed, reason, approval = (
|
|
189
|
+
False,
|
|
190
|
+
"Internal gateway error — access denied (fail closed)",
|
|
191
|
+
None,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Record audit entry
|
|
195
|
+
redacted_parameters = CredentialRedactor.redact_data_structure(params)
|
|
196
|
+
entry = AuditEntry(
|
|
197
|
+
timestamp=self._clock(),
|
|
198
|
+
agent_id=agent_id,
|
|
199
|
+
tool_name=tool_name,
|
|
200
|
+
parameters=redacted_parameters,
|
|
201
|
+
allowed=allowed,
|
|
202
|
+
reason=reason,
|
|
203
|
+
approval_status=approval,
|
|
204
|
+
)
|
|
205
|
+
self._audit_log.append(entry)
|
|
206
|
+
self._audit_sink.record(entry.to_dict())
|
|
207
|
+
|
|
208
|
+
if self.policy.log_all_calls:
|
|
209
|
+
logger.info(
|
|
210
|
+
"MCP Gateway audit | agent=%s tool=%s allowed=%s reason=%s",
|
|
211
|
+
agent_id,
|
|
212
|
+
tool_name,
|
|
213
|
+
allowed,
|
|
214
|
+
reason,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
self._metrics.record_decision(
|
|
218
|
+
allowed=allowed,
|
|
219
|
+
agent_id=agent_id,
|
|
220
|
+
tool_name=tool_name,
|
|
221
|
+
stage=stage,
|
|
222
|
+
)
|
|
223
|
+
if stage == "rate_limit":
|
|
224
|
+
self._metrics.record_rate_limit_hit(agent_id=agent_id, tool_name=tool_name)
|
|
225
|
+
|
|
226
|
+
return allowed, reason
|
|
227
|
+
|
|
228
|
+
# ── Policy evaluation pipeline ───────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
def _evaluate(
|
|
231
|
+
self,
|
|
232
|
+
agent_id: str,
|
|
233
|
+
tool_name: str,
|
|
234
|
+
params: dict[str, Any],
|
|
235
|
+
) -> tuple[bool, str, ApprovalStatus | None, str]:
|
|
236
|
+
# 1. Deny-list check
|
|
237
|
+
if tool_name in self.denied_tools:
|
|
238
|
+
return False, f"Tool '{tool_name}' is on the deny list", None, "deny_list"
|
|
239
|
+
|
|
240
|
+
# 2. Allow-list check (empty list means all tools allowed)
|
|
241
|
+
if self.policy.allowed_tools and tool_name not in self.policy.allowed_tools:
|
|
242
|
+
return False, f"Tool '{tool_name}' is not on the allow list", None, "allow_list"
|
|
243
|
+
|
|
244
|
+
# 3. Parameter sanitization
|
|
245
|
+
param_text = json.dumps(params, default=str)
|
|
246
|
+
|
|
247
|
+
# 3a. Policy blocked patterns
|
|
248
|
+
matches = self.policy.matches_pattern(param_text)
|
|
249
|
+
if matches:
|
|
250
|
+
return (
|
|
251
|
+
False,
|
|
252
|
+
f"Parameters matched blocked pattern(s): {matches}",
|
|
253
|
+
None,
|
|
254
|
+
"policy_pattern",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# 3b. Built-in dangerous patterns
|
|
258
|
+
if self.enable_builtin_sanitization:
|
|
259
|
+
for pat_str, compiled in self._builtin_compiled:
|
|
260
|
+
if compiled.search(param_text):
|
|
261
|
+
return (
|
|
262
|
+
False,
|
|
263
|
+
f"Parameters matched dangerous pattern: {pat_str}",
|
|
264
|
+
None,
|
|
265
|
+
"builtin_pattern",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# 4. Rate limiting
|
|
269
|
+
with self._rate_limit_lock:
|
|
270
|
+
count = int(self._rate_limit_store.get_bucket(agent_id) or 0)
|
|
271
|
+
if count >= self.policy.max_tool_calls:
|
|
272
|
+
return (
|
|
273
|
+
False,
|
|
274
|
+
f"Agent '{agent_id}' exceeded call budget ({self.policy.max_tool_calls})",
|
|
275
|
+
None,
|
|
276
|
+
"rate_limit",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Increment call counter (only on successful evaluation past this point)
|
|
280
|
+
self._tracked_agents.add(agent_id)
|
|
281
|
+
self._rate_limit_store.set_bucket(agent_id, count + 1)
|
|
282
|
+
|
|
283
|
+
# 5. Human approval
|
|
284
|
+
if self.policy.require_human_approval or tool_name in self.sensitive_tools:
|
|
285
|
+
if self.approval_callback is not None:
|
|
286
|
+
try:
|
|
287
|
+
status = self.approval_callback(agent_id, tool_name, params)
|
|
288
|
+
except Exception:
|
|
289
|
+
logger.error(
|
|
290
|
+
"Approval callback error — denying access | agent=%s tool=%s",
|
|
291
|
+
agent_id,
|
|
292
|
+
tool_name,
|
|
293
|
+
exc_info=True,
|
|
294
|
+
)
|
|
295
|
+
return (
|
|
296
|
+
False,
|
|
297
|
+
"Approval callback error — access denied (fail closed)",
|
|
298
|
+
None,
|
|
299
|
+
"approval_error",
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
status = ApprovalStatus.PENDING
|
|
303
|
+
|
|
304
|
+
if status == ApprovalStatus.DENIED:
|
|
305
|
+
return False, "Human approval denied", status, "approval_denied"
|
|
306
|
+
if status == ApprovalStatus.PENDING:
|
|
307
|
+
return False, "Awaiting human approval", status, "approval_pending"
|
|
308
|
+
# APPROVED — fall through
|
|
309
|
+
return True, "Approved by human reviewer", status, "approval_granted"
|
|
310
|
+
|
|
311
|
+
return True, "Allowed by policy", None, "allowed"
|
|
312
|
+
|
|
313
|
+
# ── Server wrapping ──────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def wrap_mcp_server(
|
|
317
|
+
server_config: dict[str, Any],
|
|
318
|
+
policy: GovernancePolicy,
|
|
319
|
+
*,
|
|
320
|
+
denied_tools: list[str] | None = None,
|
|
321
|
+
sensitive_tools: list[str] | None = None,
|
|
322
|
+
) -> GatewayConfig:
|
|
323
|
+
"""Produce a ``GatewayConfig`` that layers governance on a server.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
server_config: Raw MCP server configuration to wrap.
|
|
327
|
+
policy: Governance policy to apply to the wrapped server.
|
|
328
|
+
denied_tools: Optional explicit deny-list.
|
|
329
|
+
sensitive_tools: Optional list of tools that require approval.
|
|
330
|
+
|
|
331
|
+
This does not mutate the original *server_config*; it returns a
|
|
332
|
+
new ``GatewayConfig`` object that downstream code can use to
|
|
333
|
+
instantiate a governed MCP proxy.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
A ``GatewayConfig`` describing the governed server surface.
|
|
337
|
+
"""
|
|
338
|
+
return GatewayConfig(
|
|
339
|
+
server_config=dict(server_config),
|
|
340
|
+
policy_name=policy.name,
|
|
341
|
+
allowed_tools=list(policy.allowed_tools),
|
|
342
|
+
denied_tools=list(denied_tools or []),
|
|
343
|
+
sensitive_tools=list(sensitive_tools or []),
|
|
344
|
+
rate_limit=policy.max_tool_calls,
|
|
345
|
+
builtin_sanitization=True,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# ── Audit helpers ────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def audit_log(self) -> list[AuditEntry]:
|
|
352
|
+
"""Return a copy of the audit log.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
A shallow copy of the in-memory ``AuditEntry`` list.
|
|
356
|
+
"""
|
|
357
|
+
return list(self._audit_log)
|
|
358
|
+
|
|
359
|
+
def get_agent_call_count(self, agent_id: str) -> int:
|
|
360
|
+
"""Return the number of calls made by *agent_id*.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
agent_id: Agent identifier to inspect.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
The number of accepted tool calls recorded for the agent.
|
|
367
|
+
"""
|
|
368
|
+
return int(self._rate_limit_store.get_bucket(agent_id) or 0)
|
|
369
|
+
|
|
370
|
+
def reset_agent_budget(self, agent_id: str) -> None:
|
|
371
|
+
"""Reset the call counter for *agent_id*.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
agent_id: Agent identifier whose budget should be reset.
|
|
375
|
+
"""
|
|
376
|
+
self._tracked_agents.add(agent_id)
|
|
377
|
+
self._rate_limit_store.set_bucket(agent_id, 0)
|
|
378
|
+
|
|
379
|
+
def reset_all_budgets(self) -> None:
|
|
380
|
+
"""Reset call counters for every agent.
|
|
381
|
+
|
|
382
|
+
This clears the recorded call count for each tracked agent.
|
|
383
|
+
"""
|
|
384
|
+
for agent_id in self._tracked_agents:
|
|
385
|
+
self._rate_limit_store.set_bucket(agent_id, 0)
|