agent_os_kernel 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. agent_control_plane/__init__.py +662 -0
  2. agent_control_plane/a2a_adapter.py +543 -0
  3. agent_control_plane/adapter.py +417 -0
  4. agent_control_plane/agent_hibernation.py +394 -0
  5. agent_control_plane/agent_kernel.py +470 -0
  6. agent_control_plane/compliance.py +720 -0
  7. agent_control_plane/constraint_graphs.py +478 -0
  8. agent_control_plane/control_plane.py +854 -0
  9. agent_control_plane/example_executors.py +195 -0
  10. agent_control_plane/execution_engine.py +231 -0
  11. agent_control_plane/flight_recorder.py +846 -0
  12. agent_control_plane/governance_layer.py +435 -0
  13. agent_control_plane/hf_utils.py +563 -0
  14. agent_control_plane/interfaces/__init__.py +55 -0
  15. agent_control_plane/interfaces/kernel_interface.py +361 -0
  16. agent_control_plane/interfaces/plugin_interface.py +497 -0
  17. agent_control_plane/interfaces/protocol_interfaces.py +387 -0
  18. agent_control_plane/kernel_space.py +1009 -0
  19. agent_control_plane/langchain_adapter.py +424 -0
  20. agent_control_plane/lifecycle.py +3113 -0
  21. agent_control_plane/mcp_adapter.py +653 -0
  22. agent_control_plane/ml_safety.py +563 -0
  23. agent_control_plane/multimodal.py +727 -0
  24. agent_control_plane/mute_agent.py +422 -0
  25. agent_control_plane/observability.py +787 -0
  26. agent_control_plane/orchestrator.py +482 -0
  27. agent_control_plane/plugin_registry.py +750 -0
  28. agent_control_plane/policy_engine.py +954 -0
  29. agent_control_plane/process_isolation.py +777 -0
  30. agent_control_plane/shadow_mode.py +310 -0
  31. agent_control_plane/signals.py +493 -0
  32. agent_control_plane/supervisor_agents.py +430 -0
  33. agent_control_plane/time_travel_debugger.py +557 -0
  34. agent_control_plane/tool_registry.py +452 -0
  35. agent_control_plane/vfs.py +697 -0
  36. agent_kernel/__init__.py +69 -0
  37. agent_kernel/analyzer.py +435 -0
  38. agent_kernel/auditor.py +36 -0
  39. agent_kernel/completeness_auditor.py +237 -0
  40. agent_kernel/detector.py +203 -0
  41. agent_kernel/kernel.py +744 -0
  42. agent_kernel/memory_manager.py +85 -0
  43. agent_kernel/models.py +374 -0
  44. agent_kernel/nudge_mechanism.py +263 -0
  45. agent_kernel/outcome_analyzer.py +338 -0
  46. agent_kernel/patcher.py +582 -0
  47. agent_kernel/semantic_analyzer.py +316 -0
  48. agent_kernel/semantic_purge.py +349 -0
  49. agent_kernel/simulator.py +449 -0
  50. agent_kernel/teacher.py +85 -0
  51. agent_kernel/triage.py +152 -0
  52. agent_os/__init__.py +409 -0
  53. agent_os/_adversarial_impl.py +200 -0
  54. agent_os/_circuit_breaker_impl.py +232 -0
  55. agent_os/_mcp_metrics.py +193 -0
  56. agent_os/adversarial.py +20 -0
  57. agent_os/agents_compat.py +490 -0
  58. agent_os/audit_logger.py +135 -0
  59. agent_os/base_agent.py +651 -0
  60. agent_os/circuit_breaker.py +34 -0
  61. agent_os/cli/__init__.py +659 -0
  62. agent_os/cli/cmd_audit.py +128 -0
  63. agent_os/cli/cmd_init.py +152 -0
  64. agent_os/cli/cmd_policy.py +41 -0
  65. agent_os/cli/cmd_policy_gen.py +180 -0
  66. agent_os/cli/cmd_validate.py +258 -0
  67. agent_os/cli/mcp_scan.py +265 -0
  68. agent_os/cli/output.py +192 -0
  69. agent_os/cli/policy_checker.py +330 -0
  70. agent_os/compat.py +74 -0
  71. agent_os/constraint_graph.py +234 -0
  72. agent_os/content_governance.py +140 -0
  73. agent_os/context_budget.py +305 -0
  74. agent_os/credential_redactor.py +224 -0
  75. agent_os/diff_policy.py +89 -0
  76. agent_os/egress_policy.py +159 -0
  77. agent_os/escalation.py +276 -0
  78. agent_os/event_bus.py +124 -0
  79. agent_os/exceptions.py +180 -0
  80. agent_os/execution_context_policy.py +141 -0
  81. agent_os/github_enterprise.py +96 -0
  82. agent_os/health.py +20 -0
  83. agent_os/integrations/__init__.py +279 -0
  84. agent_os/integrations/a2a_adapter.py +279 -0
  85. agent_os/integrations/agent_lightning/__init__.py +30 -0
  86. agent_os/integrations/anthropic_adapter.py +420 -0
  87. agent_os/integrations/autogen_adapter.py +620 -0
  88. agent_os/integrations/base.py +1137 -0
  89. agent_os/integrations/compat.py +229 -0
  90. agent_os/integrations/config.py +98 -0
  91. agent_os/integrations/conversation_guardian.py +957 -0
  92. agent_os/integrations/crewai_adapter.py +467 -0
  93. agent_os/integrations/drift_detector.py +425 -0
  94. agent_os/integrations/dry_run.py +124 -0
  95. agent_os/integrations/escalation.py +582 -0
  96. agent_os/integrations/gemini_adapter.py +364 -0
  97. agent_os/integrations/google_adk_adapter.py +633 -0
  98. agent_os/integrations/guardrails_adapter.py +394 -0
  99. agent_os/integrations/health.py +197 -0
  100. agent_os/integrations/langchain_adapter.py +654 -0
  101. agent_os/integrations/llamafirewall.py +343 -0
  102. agent_os/integrations/llamaindex_adapter.py +188 -0
  103. agent_os/integrations/logging.py +191 -0
  104. agent_os/integrations/maf_adapter.py +631 -0
  105. agent_os/integrations/mistral_adapter.py +365 -0
  106. agent_os/integrations/openai_adapter.py +816 -0
  107. agent_os/integrations/openai_agents_sdk.py +406 -0
  108. agent_os/integrations/policy_compose.py +171 -0
  109. agent_os/integrations/profiling.py +144 -0
  110. agent_os/integrations/pydantic_ai_adapter.py +420 -0
  111. agent_os/integrations/rate_limiter.py +130 -0
  112. agent_os/integrations/rbac.py +143 -0
  113. agent_os/integrations/registry.py +113 -0
  114. agent_os/integrations/scope_guard.py +303 -0
  115. agent_os/integrations/semantic_kernel_adapter.py +769 -0
  116. agent_os/integrations/smolagents_adapter.py +629 -0
  117. agent_os/integrations/templates.py +178 -0
  118. agent_os/integrations/token_budget.py +134 -0
  119. agent_os/integrations/tool_aliases.py +190 -0
  120. agent_os/integrations/webhooks.py +177 -0
  121. agent_os/lite.py +208 -0
  122. agent_os/mcp_gateway.py +385 -0
  123. agent_os/mcp_message_signer.py +273 -0
  124. agent_os/mcp_protocols.py +161 -0
  125. agent_os/mcp_response_scanner.py +232 -0
  126. agent_os/mcp_security.py +924 -0
  127. agent_os/mcp_session_auth.py +231 -0
  128. agent_os/mcp_sliding_rate_limiter.py +184 -0
  129. agent_os/memory_guard.py +409 -0
  130. agent_os/metrics.py +134 -0
  131. agent_os/mute.py +428 -0
  132. agent_os/mute_agent.py +209 -0
  133. agent_os/policies/__init__.py +77 -0
  134. agent_os/policies/async_evaluator.py +275 -0
  135. agent_os/policies/backends.py +670 -0
  136. agent_os/policies/bridge.py +169 -0
  137. agent_os/policies/budget.py +85 -0
  138. agent_os/policies/cli.py +294 -0
  139. agent_os/policies/conflict_resolution.py +270 -0
  140. agent_os/policies/data_classification.py +252 -0
  141. agent_os/policies/evaluator.py +239 -0
  142. agent_os/policies/policy_schema.json +228 -0
  143. agent_os/policies/rate_limiting.py +145 -0
  144. agent_os/policies/schema.py +115 -0
  145. agent_os/policies/shared.py +331 -0
  146. agent_os/prompt_injection.py +694 -0
  147. agent_os/providers.py +182 -0
  148. agent_os/py.typed +0 -0
  149. agent_os/retry.py +81 -0
  150. agent_os/reversibility.py +251 -0
  151. agent_os/sandbox.py +432 -0
  152. agent_os/sandbox_provider.py +140 -0
  153. agent_os/secure_codegen.py +525 -0
  154. agent_os/security_skills.py +538 -0
  155. agent_os/semantic_policy.py +422 -0
  156. agent_os/server/__init__.py +15 -0
  157. agent_os/server/__main__.py +25 -0
  158. agent_os/server/app.py +277 -0
  159. agent_os/server/models.py +104 -0
  160. agent_os/shift_left_metrics.py +130 -0
  161. agent_os/stateless.py +742 -0
  162. agent_os/supervisor.py +148 -0
  163. agent_os/task_outcome.py +148 -0
  164. agent_os/transparency.py +181 -0
  165. agent_os/trust_root.py +128 -0
  166. agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
  167. agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
  168. agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
  169. agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
  170. agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
  171. agent_os_observability/__init__.py +27 -0
  172. agent_os_observability/dashboards.py +898 -0
  173. agent_os_observability/metrics.py +398 -0
  174. agent_os_observability/server.py +223 -0
  175. agent_os_observability/tracer.py +232 -0
  176. agent_primitives/__init__.py +24 -0
  177. agent_primitives/failures.py +84 -0
  178. agent_primitives/py.typed +0 -0
  179. amb_core/__init__.py +177 -0
  180. amb_core/adapters/__init__.py +57 -0
  181. amb_core/adapters/aws_sqs_broker.py +376 -0
  182. amb_core/adapters/azure_servicebus_broker.py +340 -0
  183. amb_core/adapters/kafka_broker.py +260 -0
  184. amb_core/adapters/nats_broker.py +285 -0
  185. amb_core/adapters/rabbitmq_broker.py +235 -0
  186. amb_core/adapters/redis_broker.py +262 -0
  187. amb_core/broker.py +145 -0
  188. amb_core/bus.py +481 -0
  189. amb_core/cloudevents.py +509 -0
  190. amb_core/dlq.py +345 -0
  191. amb_core/hf_utils.py +536 -0
  192. amb_core/memory_broker.py +410 -0
  193. amb_core/models.py +141 -0
  194. amb_core/persistence.py +529 -0
  195. amb_core/schema.py +294 -0
  196. amb_core/tracing.py +358 -0
  197. atr/__init__.py +640 -0
  198. atr/access.py +348 -0
  199. atr/composition.py +645 -0
  200. atr/decorator.py +357 -0
  201. atr/executor.py +384 -0
  202. atr/health.py +557 -0
  203. atr/hf_utils.py +449 -0
  204. atr/injection.py +422 -0
  205. atr/metrics.py +440 -0
  206. atr/policies.py +403 -0
  207. atr/py.typed +2 -0
  208. atr/registry.py +452 -0
  209. atr/schema.py +480 -0
  210. atr/tools/safe/__init__.py +75 -0
  211. atr/tools/safe/calculator.py +467 -0
  212. atr/tools/safe/datetime_tool.py +443 -0
  213. atr/tools/safe/file_reader.py +402 -0
  214. atr/tools/safe/http_client.py +316 -0
  215. atr/tools/safe/json_parser.py +374 -0
  216. atr/tools/safe/text_tool.py +537 -0
  217. atr/tools/safe/toolkit.py +175 -0
  218. caas/__init__.py +162 -0
  219. caas/api/__init__.py +7 -0
  220. caas/api/server.py +1328 -0
  221. caas/caching.py +834 -0
  222. caas/cli.py +210 -0
  223. caas/conversation.py +223 -0
  224. caas/decay.py +72 -0
  225. caas/detection/__init__.py +9 -0
  226. caas/detection/detector.py +238 -0
  227. caas/enrichment.py +130 -0
  228. caas/gateway/__init__.py +27 -0
  229. caas/gateway/trust_gateway.py +474 -0
  230. caas/hf_utils.py +479 -0
  231. caas/ingestion/__init__.py +23 -0
  232. caas/ingestion/processors.py +253 -0
  233. caas/ingestion/structure_parser.py +188 -0
  234. caas/models.py +356 -0
  235. caas/pragmatic_truth.py +444 -0
  236. caas/routing/__init__.py +10 -0
  237. caas/routing/heuristic_router.py +58 -0
  238. caas/storage/__init__.py +9 -0
  239. caas/storage/store.py +389 -0
  240. caas/triad.py +213 -0
  241. caas/tuning/__init__.py +9 -0
  242. caas/tuning/tuner.py +329 -0
  243. caas/vfs/__init__.py +14 -0
  244. caas/vfs/filesystem.py +452 -0
  245. cmvk/__init__.py +218 -0
  246. cmvk/audit.py +402 -0
  247. cmvk/benchmarks.py +478 -0
  248. cmvk/constitutional.py +904 -0
  249. cmvk/hf_utils.py +301 -0
  250. cmvk/metrics.py +473 -0
  251. cmvk/profiles.py +300 -0
  252. cmvk/py.typed +0 -0
  253. cmvk/types.py +12 -0
  254. cmvk/verification.py +956 -0
  255. emk/__init__.py +89 -0
  256. emk/causal.py +352 -0
  257. emk/hf_utils.py +421 -0
  258. emk/indexer.py +83 -0
  259. emk/py.typed +0 -0
  260. emk/schema.py +204 -0
  261. emk/sleep_cycle.py +347 -0
  262. emk/store.py +281 -0
  263. iatp/__init__.py +166 -0
  264. iatp/attestation.py +461 -0
  265. iatp/cli.py +317 -0
  266. iatp/hf_utils.py +472 -0
  267. iatp/ipc_pipes.py +580 -0
  268. iatp/main.py +412 -0
  269. iatp/models/__init__.py +447 -0
  270. iatp/policy_engine.py +337 -0
  271. iatp/py.typed +2 -0
  272. iatp/recovery.py +321 -0
  273. iatp/security/__init__.py +270 -0
  274. iatp/sidecar/__init__.py +519 -0
  275. iatp/telemetry/__init__.py +164 -0
  276. iatp/tests/__init__.py +1 -0
  277. iatp/tests/test_attestation.py +370 -0
  278. iatp/tests/test_cli.py +131 -0
  279. iatp/tests/test_ed25519_attestation.py +211 -0
  280. iatp/tests/test_models.py +130 -0
  281. iatp/tests/test_policy_engine.py +347 -0
  282. iatp/tests/test_recovery.py +281 -0
  283. iatp/tests/test_security.py +222 -0
  284. iatp/tests/test_sidecar.py +167 -0
  285. iatp/tests/test_telemetry.py +175 -0
  286. mcp_kernel_server/__init__.py +28 -0
  287. mcp_kernel_server/cli.py +274 -0
  288. mcp_kernel_server/resources.py +217 -0
  289. mcp_kernel_server/server.py +564 -0
  290. mcp_kernel_server/tools.py +1174 -0
  291. mute_agent/__init__.py +68 -0
  292. mute_agent/core/__init__.py +1 -0
  293. mute_agent/core/execution_agent.py +166 -0
  294. mute_agent/core/handshake_protocol.py +201 -0
  295. mute_agent/core/reasoning_agent.py +238 -0
  296. mute_agent/knowledge_graph/__init__.py +1 -0
  297. mute_agent/knowledge_graph/graph_elements.py +65 -0
  298. mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
  299. mute_agent/knowledge_graph/subgraph.py +224 -0
  300. mute_agent/listener/__init__.py +43 -0
  301. mute_agent/listener/adapters/__init__.py +31 -0
  302. mute_agent/listener/adapters/base_adapter.py +189 -0
  303. mute_agent/listener/adapters/caas_adapter.py +344 -0
  304. mute_agent/listener/adapters/control_plane_adapter.py +436 -0
  305. mute_agent/listener/adapters/iatp_adapter.py +332 -0
  306. mute_agent/listener/adapters/scak_adapter.py +251 -0
  307. mute_agent/listener/listener.py +610 -0
  308. mute_agent/listener/state_observer.py +436 -0
  309. mute_agent/listener/threshold_config.py +313 -0
  310. mute_agent/super_system/__init__.py +1 -0
  311. mute_agent/super_system/router.py +204 -0
  312. mute_agent/visualization/__init__.py +10 -0
  313. mute_agent/visualization/graph_debugger.py +502 -0
  314. nexus/README.md +60 -0
  315. nexus/__init__.py +51 -0
  316. nexus/arbiter.py +359 -0
  317. nexus/client.py +466 -0
  318. nexus/dmz.py +444 -0
  319. nexus/escrow.py +430 -0
  320. nexus/exceptions.py +286 -0
  321. nexus/pyproject.toml +36 -0
  322. nexus/registry.py +393 -0
  323. nexus/reputation.py +425 -0
  324. nexus/schemas/__init__.py +51 -0
  325. nexus/schemas/compliance.py +276 -0
  326. nexus/schemas/escrow.py +251 -0
  327. nexus/schemas/manifest.py +225 -0
  328. nexus/schemas/receipt.py +208 -0
  329. nexus/tests/__init__.py +0 -0
  330. nexus/tests/conftest.py +146 -0
  331. nexus/tests/test_arbiter.py +192 -0
  332. nexus/tests/test_dmz.py +194 -0
  333. nexus/tests/test_escrow.py +276 -0
  334. nexus/tests/test_exceptions.py +225 -0
  335. nexus/tests/test_registry.py +232 -0
  336. nexus/tests/test_reputation.py +328 -0
  337. nexus/tests/test_schemas.py +295 -0
@@ -0,0 +1,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()