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,231 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Cryptographic session authentication for MCP agents."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import logging
|
|
9
|
+
import secrets
|
|
10
|
+
import threading
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
|
|
14
|
+
from agent_os.mcp_protocols import InMemorySessionStore, MCPSessionStore
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class MCPSession:
|
|
21
|
+
"""An authenticated MCP session bound to an agent identity."""
|
|
22
|
+
|
|
23
|
+
token: str
|
|
24
|
+
agent_id: str
|
|
25
|
+
user_id: str | None
|
|
26
|
+
created_at: datetime
|
|
27
|
+
expires_at: datetime
|
|
28
|
+
rate_limit_key: str
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def is_expired(self) -> bool:
|
|
32
|
+
return _utcnow() >= self.expires_at
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _utcnow() -> datetime:
|
|
36
|
+
return datetime.now(timezone.utc)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MCPSessionAuthenticator:
|
|
40
|
+
"""Authenticate MCP sessions with cryptographic tokens and expiry checks.
|
|
41
|
+
|
|
42
|
+
The authenticator creates short-lived session tokens, validates that a
|
|
43
|
+
token belongs to a specific agent, and keeps internal indices synchronized
|
|
44
|
+
with the injected session store under a thread-safe lock.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
session_ttl: timedelta = timedelta(hours=1),
|
|
51
|
+
max_concurrent_sessions: int = 10,
|
|
52
|
+
session_store: MCPSessionStore | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Initialize the session authenticator.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
session_ttl: Lifetime applied to newly issued session tokens.
|
|
58
|
+
max_concurrent_sessions: Maximum number of live sessions allowed
|
|
59
|
+
per agent.
|
|
60
|
+
session_store: Optional persistence backend for storing sessions.
|
|
61
|
+
Defaults to the in-memory session store.
|
|
62
|
+
"""
|
|
63
|
+
if session_ttl <= timedelta(0):
|
|
64
|
+
raise ValueError("session_ttl must be positive")
|
|
65
|
+
if max_concurrent_sessions <= 0:
|
|
66
|
+
raise ValueError("max_concurrent_sessions must be positive")
|
|
67
|
+
|
|
68
|
+
self.session_ttl = session_ttl
|
|
69
|
+
self.max_concurrent_sessions = max_concurrent_sessions
|
|
70
|
+
self._lock = threading.Lock()
|
|
71
|
+
self._session_store = session_store or InMemorySessionStore()
|
|
72
|
+
self._agent_sessions: dict[str, set[str]] = {}
|
|
73
|
+
self._session_expirations: dict[str, datetime] = {}
|
|
74
|
+
|
|
75
|
+
def create_session(self, agent_id: str, user_id: str | None = None) -> str:
|
|
76
|
+
"""Create a session token bound to an agent and optional user context.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
agent_id: Agent identifier that will own the session.
|
|
80
|
+
user_id: Optional user or tenant identifier associated with the
|
|
81
|
+
session.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
A newly generated opaque session token.
|
|
85
|
+
"""
|
|
86
|
+
if not agent_id or not agent_id.strip():
|
|
87
|
+
raise ValueError("agent_id must not be empty")
|
|
88
|
+
|
|
89
|
+
now = _utcnow()
|
|
90
|
+
with self._lock:
|
|
91
|
+
self._cleanup_expired_locked(now)
|
|
92
|
+
active_for_agent = sum(
|
|
93
|
+
1
|
|
94
|
+
for token in self._agent_sessions.get(agent_id, set())
|
|
95
|
+
if (
|
|
96
|
+
(session := self._session_store.get(token)) is not None
|
|
97
|
+
and session.expires_at > now
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
if active_for_agent >= self.max_concurrent_sessions:
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
f"Agent '{agent_id}' exceeded maximum concurrent sessions "
|
|
103
|
+
f"({self.max_concurrent_sessions})."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
token = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("ascii")
|
|
107
|
+
session = MCPSession(
|
|
108
|
+
token=token,
|
|
109
|
+
agent_id=agent_id,
|
|
110
|
+
user_id=user_id,
|
|
111
|
+
created_at=now,
|
|
112
|
+
expires_at=now + self.session_ttl,
|
|
113
|
+
rate_limit_key=f"{user_id}:{agent_id}" if user_id else agent_id,
|
|
114
|
+
)
|
|
115
|
+
self._store_session_locked(session)
|
|
116
|
+
|
|
117
|
+
logger.info("Created MCP session for agent %s", agent_id)
|
|
118
|
+
return token
|
|
119
|
+
|
|
120
|
+
def validate_session(self, agent_id: str, session_token: str) -> MCPSession | None:
|
|
121
|
+
"""Validate a session token for a specific agent.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
agent_id: Agent identifier expected to own the session.
|
|
125
|
+
session_token: Opaque session token previously issued by this
|
|
126
|
+
authenticator.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The active ``MCPSession`` when the token is valid for the supplied
|
|
130
|
+
agent, otherwise ``None``. Errors fail closed.
|
|
131
|
+
"""
|
|
132
|
+
if not agent_id or not agent_id.strip() or not session_token or not session_token.strip():
|
|
133
|
+
logger.warning("MCP session validation failed due to missing agent_id or session token")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
with self._lock:
|
|
138
|
+
session = self._session_store.get(session_token)
|
|
139
|
+
if session is None:
|
|
140
|
+
self._delete_session_locked(session_token)
|
|
141
|
+
return None
|
|
142
|
+
if session.agent_id != agent_id:
|
|
143
|
+
return None
|
|
144
|
+
if session.is_expired:
|
|
145
|
+
self._delete_session_locked(session_token, session)
|
|
146
|
+
return None
|
|
147
|
+
return session
|
|
148
|
+
except Exception:
|
|
149
|
+
logger.error("MCP session validation failed closed", exc_info=True)
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def revoke_session(self, session_token: str) -> bool:
|
|
153
|
+
"""Revoke a single session token.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
session_token: Session token to revoke.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
``True`` when the session existed and was removed, otherwise
|
|
160
|
+
``False``.
|
|
161
|
+
"""
|
|
162
|
+
with self._lock:
|
|
163
|
+
return self._delete_session_locked(session_token)
|
|
164
|
+
|
|
165
|
+
def revoke_all_sessions(self, agent_id: str) -> int:
|
|
166
|
+
"""Revoke every session belonging to an agent.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
agent_id: Agent identifier whose sessions should be revoked.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The number of sessions removed for the agent.
|
|
173
|
+
"""
|
|
174
|
+
with self._lock:
|
|
175
|
+
tokens = list(self._agent_sessions.get(agent_id, set()))
|
|
176
|
+
for token in tokens:
|
|
177
|
+
session = self._session_store.get(token)
|
|
178
|
+
if session is None or session.agent_id == agent_id:
|
|
179
|
+
self._delete_session_locked(token, session)
|
|
180
|
+
return len(tokens)
|
|
181
|
+
|
|
182
|
+
def cleanup_expired_sessions(self) -> int:
|
|
183
|
+
"""Remove expired sessions and return the number removed.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
The number of expired sessions removed from the store and internal
|
|
187
|
+
indices.
|
|
188
|
+
"""
|
|
189
|
+
with self._lock:
|
|
190
|
+
return self._cleanup_expired_locked(_utcnow())
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def active_session_count(self) -> int:
|
|
194
|
+
"""Return the number of active sessions.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
The number of currently active, non-expired sessions.
|
|
198
|
+
"""
|
|
199
|
+
with self._lock:
|
|
200
|
+
self._cleanup_expired_locked(_utcnow())
|
|
201
|
+
return len(self._session_expirations)
|
|
202
|
+
|
|
203
|
+
def _cleanup_expired_locked(self, now: datetime) -> int:
|
|
204
|
+
expired_tokens = [
|
|
205
|
+
token for token, expires_at in self._session_expirations.items() if expires_at <= now
|
|
206
|
+
]
|
|
207
|
+
for token in expired_tokens:
|
|
208
|
+
session = self._session_store.get(token)
|
|
209
|
+
self._delete_session_locked(token, session)
|
|
210
|
+
return len(expired_tokens)
|
|
211
|
+
|
|
212
|
+
def _store_session_locked(self, session: MCPSession) -> None:
|
|
213
|
+
self._session_store.set(session)
|
|
214
|
+
self._session_expirations[session.token] = session.expires_at
|
|
215
|
+
self._agent_sessions.setdefault(session.agent_id, set()).add(session.token)
|
|
216
|
+
|
|
217
|
+
def _delete_session_locked(
|
|
218
|
+
self,
|
|
219
|
+
session_token: str,
|
|
220
|
+
session: MCPSession | None = None,
|
|
221
|
+
) -> bool:
|
|
222
|
+
stored_session = session or self._session_store.get(session_token)
|
|
223
|
+
deleted = self._session_store.delete(session_token)
|
|
224
|
+
self._session_expirations.pop(session_token, None)
|
|
225
|
+
if stored_session is not None:
|
|
226
|
+
tokens = self._agent_sessions.get(stored_session.agent_id)
|
|
227
|
+
if tokens is not None:
|
|
228
|
+
tokens.discard(session_token)
|
|
229
|
+
if not tokens:
|
|
230
|
+
self._agent_sessions.pop(stored_session.agent_id, None)
|
|
231
|
+
return deleted or stored_session is not None
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Sliding-window rate limiting for MCP tool invocations."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
from agent_os.mcp_protocols import InMemoryRateLimitStore, MCPRateLimitStore
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MCPSlidingRateLimiter:
|
|
18
|
+
"""Thread-safe per-agent sliding-window rate limiter.
|
|
19
|
+
|
|
20
|
+
The limiter tracks recent call timestamps per normalized agent identifier
|
|
21
|
+
and enforces a bounded number of calls within a moving time window. Bucket
|
|
22
|
+
persistence and the clock are injectable so callers can externalize state
|
|
23
|
+
or drive the limiter deterministically in tests.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
max_calls_per_window: int = 100,
|
|
30
|
+
window_size: float = 300.0,
|
|
31
|
+
rate_limit_store: MCPRateLimitStore | None = None,
|
|
32
|
+
clock: Callable[[], float] = time.monotonic,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize the sliding-window limiter.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
max_calls_per_window: Maximum calls allowed within a single
|
|
38
|
+
sliding window.
|
|
39
|
+
window_size: Size of the sliding window in seconds.
|
|
40
|
+
rate_limit_store: Optional persistence backend for per-agent
|
|
41
|
+
buckets. Defaults to the in-memory bucket store.
|
|
42
|
+
clock: Monotonic clock used to timestamp calls.
|
|
43
|
+
"""
|
|
44
|
+
if max_calls_per_window <= 0:
|
|
45
|
+
raise ValueError("max_calls_per_window must be positive")
|
|
46
|
+
if window_size <= 0:
|
|
47
|
+
raise ValueError("window_size must be positive")
|
|
48
|
+
|
|
49
|
+
self.max_calls_per_window = max_calls_per_window
|
|
50
|
+
self.window_size = float(window_size)
|
|
51
|
+
self._rate_limit_store = rate_limit_store or InMemoryRateLimitStore()
|
|
52
|
+
self._clock = clock
|
|
53
|
+
self._state_lock = threading.Lock()
|
|
54
|
+
self._bucket_locks: dict[str, threading.Lock] = {}
|
|
55
|
+
self._tracked_agents: set[str] = set()
|
|
56
|
+
|
|
57
|
+
def try_acquire(self, agent_id: str) -> bool:
|
|
58
|
+
"""Try to acquire budget for an agent.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
agent_id: Agent identifier whose budget should be decremented.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
``True`` when a call can proceed inside the active window,
|
|
65
|
+
otherwise ``False``.
|
|
66
|
+
"""
|
|
67
|
+
key = self._normalize_agent_id(agent_id)
|
|
68
|
+
bucket_lock = self._get_bucket_lock(key)
|
|
69
|
+
now = self._clock()
|
|
70
|
+
cutoff = now - self.window_size
|
|
71
|
+
with bucket_lock:
|
|
72
|
+
bucket = self._load_bucket(key)
|
|
73
|
+
self._prune_expired(bucket, cutoff)
|
|
74
|
+
if len(bucket) >= self.max_calls_per_window:
|
|
75
|
+
logger.warning("MCP sliding window exceeded for agent %s", key)
|
|
76
|
+
return False
|
|
77
|
+
bucket.append(now)
|
|
78
|
+
self._rate_limit_store.set_bucket(key, bucket)
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def get_remaining_budget(self, agent_id: str) -> int:
|
|
82
|
+
"""Return remaining calls in the active window.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
agent_id: Agent identifier to inspect.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The number of additional calls the agent may make before the
|
|
89
|
+
current window is exhausted.
|
|
90
|
+
"""
|
|
91
|
+
key = self._normalize_agent_id(agent_id)
|
|
92
|
+
bucket_lock = self._get_bucket_lock(key)
|
|
93
|
+
now = self._clock()
|
|
94
|
+
cutoff = now - self.window_size
|
|
95
|
+
with bucket_lock:
|
|
96
|
+
bucket = self._load_bucket(key)
|
|
97
|
+
self._prune_expired(bucket, cutoff)
|
|
98
|
+
self._rate_limit_store.set_bucket(key, bucket)
|
|
99
|
+
return max(0, self.max_calls_per_window - len(bucket))
|
|
100
|
+
|
|
101
|
+
def get_call_count(self, agent_id: str) -> int:
|
|
102
|
+
"""Return the number of calls inside the active window.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
agent_id: Agent identifier to inspect.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The number of calls retained inside the current sliding window.
|
|
109
|
+
"""
|
|
110
|
+
key = self._normalize_agent_id(agent_id)
|
|
111
|
+
bucket_lock = self._get_bucket_lock(key)
|
|
112
|
+
now = self._clock()
|
|
113
|
+
cutoff = now - self.window_size
|
|
114
|
+
with bucket_lock:
|
|
115
|
+
bucket = self._load_bucket(key)
|
|
116
|
+
self._prune_expired(bucket, cutoff)
|
|
117
|
+
self._rate_limit_store.set_bucket(key, bucket)
|
|
118
|
+
return len(bucket)
|
|
119
|
+
|
|
120
|
+
def reset(self, agent_id: str) -> None:
|
|
121
|
+
"""Clear state for a single agent.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
agent_id: Agent identifier whose bucket should be reset.
|
|
125
|
+
"""
|
|
126
|
+
key = self._normalize_agent_id(agent_id)
|
|
127
|
+
with self._get_bucket_lock(key):
|
|
128
|
+
self._rate_limit_store.set_bucket(key, [])
|
|
129
|
+
|
|
130
|
+
def reset_all(self) -> None:
|
|
131
|
+
"""Clear state for every agent.
|
|
132
|
+
|
|
133
|
+
This removes all retained timestamps from every tracked bucket.
|
|
134
|
+
"""
|
|
135
|
+
for key in self._tracked_agent_ids():
|
|
136
|
+
with self._get_bucket_lock(key):
|
|
137
|
+
self._rate_limit_store.set_bucket(key, [])
|
|
138
|
+
|
|
139
|
+
def cleanup_expired(self) -> int:
|
|
140
|
+
"""Prune expired entries from all agents and return the number removed.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The total number of expired timestamps removed across all tracked
|
|
144
|
+
agent buckets.
|
|
145
|
+
"""
|
|
146
|
+
now = self._clock()
|
|
147
|
+
cutoff = now - self.window_size
|
|
148
|
+
removed = 0
|
|
149
|
+
for key in self._tracked_agent_ids():
|
|
150
|
+
with self._get_bucket_lock(key):
|
|
151
|
+
bucket = self._load_bucket(key)
|
|
152
|
+
before = len(bucket)
|
|
153
|
+
self._prune_expired(bucket, cutoff)
|
|
154
|
+
removed += before - len(bucket)
|
|
155
|
+
self._rate_limit_store.set_bucket(key, bucket)
|
|
156
|
+
return removed
|
|
157
|
+
|
|
158
|
+
def _get_bucket_lock(self, agent_id: str) -> threading.Lock:
|
|
159
|
+
with self._state_lock:
|
|
160
|
+
self._tracked_agents.add(agent_id)
|
|
161
|
+
return self._bucket_locks.setdefault(agent_id, threading.Lock())
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _prune_expired(timestamps: list[float], cutoff: float) -> None:
|
|
165
|
+
while timestamps and timestamps[0] <= cutoff:
|
|
166
|
+
timestamps.pop(0)
|
|
167
|
+
|
|
168
|
+
def _load_bucket(self, agent_id: str) -> list[float]:
|
|
169
|
+
bucket = self._rate_limit_store.get_bucket(agent_id)
|
|
170
|
+
if bucket is None:
|
|
171
|
+
return []
|
|
172
|
+
if isinstance(bucket, list):
|
|
173
|
+
return list(bucket)
|
|
174
|
+
return [float(timestamp) for timestamp in bucket]
|
|
175
|
+
|
|
176
|
+
def _tracked_agent_ids(self) -> list[str]:
|
|
177
|
+
with self._state_lock:
|
|
178
|
+
return list(self._tracked_agents)
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _normalize_agent_id(agent_id: str) -> str:
|
|
182
|
+
if not agent_id or not agent_id.strip():
|
|
183
|
+
raise ValueError("agent_id must not be empty")
|
|
184
|
+
return agent_id.casefold()
|