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,273 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """HMAC signing and replay protection for MCP messages."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import base64
8
+ import hashlib
9
+ import hmac
10
+ import logging
11
+ import secrets
12
+ import threading
13
+ import uuid
14
+ from dataclasses import dataclass
15
+ from datetime import datetime, timedelta, timezone
16
+ from typing import Callable
17
+
18
+ from agent_os.mcp_protocols import InMemoryNonceStore, MCPNonceStore
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class MCPSignedEnvelope:
25
+ """A signed MCP message envelope."""
26
+
27
+ payload: str
28
+ nonce: str
29
+ timestamp: datetime
30
+ signature: str
31
+ sender_id: str | None = None
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class MCPVerificationResult:
36
+ """The result of verifying a signed MCP envelope."""
37
+
38
+ is_valid: bool
39
+ payload: str | None = None
40
+ sender_id: str | None = None
41
+ failure_reason: str | None = None
42
+
43
+ @classmethod
44
+ def success(cls, payload: str, sender_id: str | None) -> "MCPVerificationResult":
45
+ return cls(is_valid=True, payload=payload, sender_id=sender_id)
46
+
47
+ @classmethod
48
+ def failed(cls, reason: str) -> "MCPVerificationResult":
49
+ return cls(is_valid=False, failure_reason=reason)
50
+
51
+
52
+ def _utcnow() -> datetime:
53
+ return datetime.now(timezone.utc)
54
+
55
+
56
+ class MCPMessageSigner:
57
+ """Sign and verify MCP messages with replay protection.
58
+
59
+ The signer produces tamper-evident envelopes and tracks nonces inside a
60
+ replay window so previously accepted messages cannot be replayed
61
+ indefinitely. Persistence and nonce generation are injectable to support
62
+ deterministic tests and external storage backends.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ signing_key: bytes,
68
+ *,
69
+ replay_window: timedelta = timedelta(minutes=5),
70
+ nonce_cache_cleanup_interval: timedelta = timedelta(minutes=10),
71
+ max_nonce_cache_size: int = 10_000,
72
+ nonce_store: MCPNonceStore | None = None,
73
+ nonce_generator: Callable[[], str] | None = None,
74
+ ) -> None:
75
+ """Initialize the signer and replay-protection settings.
76
+
77
+ Args:
78
+ signing_key: Shared secret used to compute HMAC signatures.
79
+ replay_window: Maximum permitted clock skew and replay horizon.
80
+ nonce_cache_cleanup_interval: Minimum interval between automatic
81
+ nonce cleanup passes.
82
+ max_nonce_cache_size: Maximum number of nonces retained by the
83
+ default in-memory nonce store.
84
+ nonce_store: Optional nonce persistence backend. Defaults to the
85
+ in-memory LRU nonce store.
86
+ nonce_generator: Optional nonce factory for deterministic testing
87
+ or custom nonce generation strategies.
88
+ """
89
+ if signing_key is None:
90
+ raise ValueError("signing_key must not be None")
91
+ if len(signing_key) < 32:
92
+ raise ValueError("signing_key must be at least 32 bytes")
93
+ if replay_window <= timedelta(0):
94
+ raise ValueError("replay_window must be positive")
95
+ if nonce_cache_cleanup_interval <= timedelta(0):
96
+ raise ValueError("nonce_cache_cleanup_interval must be positive")
97
+ if max_nonce_cache_size <= 0:
98
+ raise ValueError("max_nonce_cache_size must be positive")
99
+
100
+ self._signing_key = signing_key
101
+ self.replay_window = replay_window
102
+ self.nonce_cache_cleanup_interval = nonce_cache_cleanup_interval
103
+ self.max_nonce_cache_size = max_nonce_cache_size
104
+ self._lock = threading.Lock()
105
+ self._nonce_store = nonce_store or InMemoryNonceStore(
106
+ max_entries=max_nonce_cache_size,
107
+ )
108
+ self._nonce_generator = nonce_generator or (lambda: uuid.uuid4().hex)
109
+ self._last_cleanup = _utcnow()
110
+
111
+ @classmethod
112
+ def from_base64_key(cls, base64_key: str) -> "MCPMessageSigner":
113
+ """Build a signer from a base64-encoded shared secret.
114
+
115
+ Args:
116
+ base64_key: Shared secret encoded as ASCII base64 text.
117
+
118
+ Returns:
119
+ A configured ``MCPMessageSigner`` instance using the decoded key
120
+ bytes.
121
+ """
122
+ if not base64_key or not base64_key.strip():
123
+ raise ValueError("base64_key must not be empty")
124
+ decoded = base64.b64decode(base64_key.encode("ascii"), validate=True)
125
+ return cls(decoded)
126
+
127
+ @staticmethod
128
+ def generate_key() -> bytes:
129
+ """Return a new 256-bit signing key.
130
+
131
+ Returns:
132
+ A cryptographically random 32-byte signing key.
133
+ """
134
+ return secrets.token_bytes(32)
135
+
136
+ @property
137
+ def cached_nonce_count(self) -> int:
138
+ """Return the number of tracked nonces.
139
+
140
+ Returns:
141
+ The number of replay-protection nonces currently retained by the
142
+ backing store when the store exposes a ``count`` helper.
143
+ """
144
+ with self._lock:
145
+ count = getattr(self._nonce_store, "count", None)
146
+ return int(count()) if callable(count) else 0
147
+
148
+ def sign_message(self, payload: str, sender_id: str | None = None) -> MCPSignedEnvelope:
149
+ """Create a signed message envelope.
150
+
151
+ Args:
152
+ payload: Serialized MCP payload to sign.
153
+ sender_id: Optional sender identifier included in the signature.
154
+
155
+ Returns:
156
+ A signed envelope containing the payload, nonce, timestamp, and
157
+ computed signature.
158
+ """
159
+ if payload is None:
160
+ raise ValueError("payload must not be None")
161
+ if not payload.strip():
162
+ raise ValueError("payload must not be empty")
163
+
164
+ timestamp = _utcnow()
165
+ nonce = self._nonce_generator()
166
+ signature = self._compute_signature(
167
+ nonce=nonce,
168
+ timestamp=timestamp,
169
+ sender_id=sender_id,
170
+ payload=payload,
171
+ )
172
+ return MCPSignedEnvelope(
173
+ payload=payload,
174
+ nonce=nonce,
175
+ timestamp=timestamp,
176
+ sender_id=sender_id,
177
+ signature=signature,
178
+ )
179
+
180
+ def verify_message(self, envelope: MCPSignedEnvelope) -> MCPVerificationResult:
181
+ """Verify an envelope's signature and replay metadata.
182
+
183
+ Args:
184
+ envelope: Signed message envelope to validate.
185
+
186
+ Returns:
187
+ ``MCPVerificationResult`` describing whether the envelope is valid
188
+ and, on success, exposing the verified payload and sender identity.
189
+ Invalid signatures, replayed nonces, or unexpected errors fail
190
+ closed.
191
+ """
192
+ if envelope is None:
193
+ raise ValueError("envelope must not be None")
194
+
195
+ try:
196
+ now = _utcnow()
197
+ age = now - envelope.timestamp
198
+ if age > self.replay_window or age < -self.replay_window:
199
+ return MCPVerificationResult.failed("Message timestamp outside replay window.")
200
+
201
+ with self._lock:
202
+ self._maybe_cleanup_locked(now)
203
+ if self._nonce_store.has(envelope.nonce):
204
+ logger.warning("Duplicate MCP nonce detected: %s", envelope.nonce)
205
+ return MCPVerificationResult.failed("Duplicate nonce (replay detected).")
206
+ self._nonce_store.add(
207
+ envelope.nonce,
208
+ envelope.timestamp + self.replay_window,
209
+ )
210
+
211
+ expected_signature = self._compute_signature(
212
+ nonce=envelope.nonce,
213
+ timestamp=envelope.timestamp,
214
+ sender_id=envelope.sender_id,
215
+ payload=envelope.payload,
216
+ )
217
+ if not hmac.compare_digest(expected_signature, envelope.signature):
218
+ return MCPVerificationResult.failed("Invalid signature.")
219
+
220
+ return MCPVerificationResult.success(envelope.payload, envelope.sender_id)
221
+ except Exception as exc:
222
+ logger.error("MCP signature verification failed closed", exc_info=True)
223
+ return MCPVerificationResult.failed(f"Verification error (fail-closed): {exc}")
224
+
225
+ def cleanup_nonce_cache(self) -> int:
226
+ """Remove expired nonces and return the number removed.
227
+
228
+ Returns:
229
+ The number of expired nonces removed from the backing store.
230
+ """
231
+ with self._lock:
232
+ return self._cleanup_nonce_cache_locked(_utcnow())
233
+
234
+ def _compute_signature(
235
+ self,
236
+ *,
237
+ nonce: str,
238
+ timestamp: datetime,
239
+ sender_id: str | None,
240
+ payload: str,
241
+ ) -> str:
242
+ canonical = self._build_canonical_string(
243
+ nonce=nonce,
244
+ timestamp=timestamp,
245
+ sender_id=sender_id,
246
+ payload=payload,
247
+ )
248
+ digest = hmac.new(
249
+ self._signing_key,
250
+ canonical.encode("utf-8"),
251
+ hashlib.sha256,
252
+ ).digest()
253
+ return base64.b64encode(digest).decode("ascii")
254
+
255
+ @staticmethod
256
+ def _build_canonical_string(
257
+ *,
258
+ nonce: str,
259
+ timestamp: datetime,
260
+ sender_id: str | None,
261
+ payload: str,
262
+ ) -> str:
263
+ timestamp_ms = int(timestamp.timestamp() * 1000)
264
+ return f"{nonce}|{timestamp_ms}|{sender_id or ''}|{payload}"
265
+
266
+ def _maybe_cleanup_locked(self, now: datetime) -> None:
267
+ if now - self._last_cleanup >= self.nonce_cache_cleanup_interval:
268
+ self._cleanup_nonce_cache_locked(now)
269
+
270
+ def _cleanup_nonce_cache_locked(self, now: datetime) -> int:
271
+ expired = self._nonce_store.cleanup()
272
+ self._last_cleanup = now
273
+ return expired
@@ -0,0 +1,161 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """Persistence protocols and in-memory defaults for MCP governance components."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import threading
8
+ from collections import OrderedDict
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Callable, Protocol
11
+
12
+
13
+ def _utcnow() -> datetime:
14
+ return datetime.now(timezone.utc)
15
+
16
+
17
+ class MCPSessionStore(Protocol):
18
+ """Persistence contract for authenticated MCP sessions."""
19
+
20
+ def get(self, session_id: str) -> Any | None:
21
+ """Return the stored session for *session_id*, if present."""
22
+
23
+ def set(self, session: Any) -> None:
24
+ """Persist *session* keyed by its session identifier."""
25
+
26
+ def delete(self, session_id: str) -> bool:
27
+ """Delete *session_id* and return ``True`` when it existed."""
28
+
29
+
30
+ class MCPNonceStore(Protocol):
31
+ """Persistence contract for replay-protection nonces."""
32
+
33
+ def has(self, nonce: str) -> bool:
34
+ """Return ``True`` when *nonce* is still tracked."""
35
+
36
+ def add(self, nonce: str, expires_at: datetime) -> None:
37
+ """Track *nonce* until *expires_at*."""
38
+
39
+ def cleanup(self) -> int:
40
+ """Remove expired entries and return the number removed."""
41
+
42
+
43
+ class MCPRateLimitStore(Protocol):
44
+ """Persistence contract for per-agent rate-limit buckets."""
45
+
46
+ def get_bucket(self, agent_id: str) -> Any | None:
47
+ """Return the bucket state stored for *agent_id*, if present."""
48
+
49
+ def set_bucket(self, agent_id: str, bucket: Any) -> None:
50
+ """Persist *bucket* for *agent_id*."""
51
+
52
+
53
+ class MCPAuditSink(Protocol):
54
+ """Persistence contract for structured audit records."""
55
+
56
+ def record(self, entry: dict[str, Any]) -> None:
57
+ """Persist a structured audit *entry*."""
58
+
59
+
60
+ class InMemorySessionStore:
61
+ """Thread-safe in-memory session storage."""
62
+
63
+ def __init__(self) -> None:
64
+ self._sessions: dict[str, Any] = {}
65
+ self._lock = threading.Lock()
66
+
67
+ def get(self, session_id: str) -> Any | None:
68
+ with self._lock:
69
+ return self._sessions.get(session_id)
70
+
71
+ def set(self, session: Any) -> None:
72
+ session_id = getattr(session, "token", None)
73
+ if not isinstance(session_id, str) or not session_id:
74
+ raise ValueError("session must provide a non-empty string token")
75
+ with self._lock:
76
+ self._sessions[session_id] = session
77
+
78
+ def delete(self, session_id: str) -> bool:
79
+ with self._lock:
80
+ return self._sessions.pop(session_id, None) is not None
81
+
82
+
83
+ class InMemoryNonceStore:
84
+ """Thread-safe in-memory nonce storage with TTL cleanup and eviction."""
85
+
86
+ def __init__(
87
+ self,
88
+ *,
89
+ clock: Callable[[], datetime] = _utcnow,
90
+ max_entries: int = 10_000,
91
+ ) -> None:
92
+ if max_entries <= 0:
93
+ raise ValueError("max_entries must be positive")
94
+ self._clock = clock
95
+ self._max_entries = max_entries
96
+ self._nonces: OrderedDict[str, datetime] = OrderedDict()
97
+ self._lock = threading.Lock()
98
+
99
+ def has(self, nonce: str) -> bool:
100
+ with self._lock:
101
+ expires_at = self._nonces.get(nonce)
102
+ if expires_at is None:
103
+ return False
104
+ if expires_at <= self._clock():
105
+ self._nonces.pop(nonce, None)
106
+ return False
107
+ self._nonces.move_to_end(nonce)
108
+ return True
109
+
110
+ def add(self, nonce: str, expires_at: datetime) -> None:
111
+ with self._lock:
112
+ self._nonces[nonce] = expires_at
113
+ self._nonces.move_to_end(nonce)
114
+ while len(self._nonces) > self._max_entries:
115
+ self._nonces.popitem(last=False)
116
+
117
+ def cleanup(self) -> int:
118
+ removed = 0
119
+ now = self._clock()
120
+ with self._lock:
121
+ expired = [nonce for nonce, expires_at in self._nonces.items() if expires_at <= now]
122
+ for nonce in expired:
123
+ self._nonces.pop(nonce, None)
124
+ removed = len(expired)
125
+ return removed
126
+
127
+ def count(self) -> int:
128
+ with self._lock:
129
+ return len(self._nonces)
130
+
131
+
132
+ class InMemoryRateLimitStore:
133
+ """Thread-safe in-memory rate-limit bucket storage."""
134
+
135
+ def __init__(self) -> None:
136
+ self._buckets: dict[str, Any] = {}
137
+ self._lock = threading.Lock()
138
+
139
+ def get_bucket(self, agent_id: str) -> Any | None:
140
+ with self._lock:
141
+ return self._buckets.get(agent_id)
142
+
143
+ def set_bucket(self, agent_id: str, bucket: Any) -> None:
144
+ with self._lock:
145
+ self._buckets[agent_id] = bucket
146
+
147
+
148
+ class InMemoryAuditSink:
149
+ """Thread-safe in-memory audit sink for structured records."""
150
+
151
+ def __init__(self) -> None:
152
+ self._entries: list[dict[str, Any]] = []
153
+ self._lock = threading.Lock()
154
+
155
+ def record(self, entry: dict[str, Any]) -> None:
156
+ with self._lock:
157
+ self._entries.append(dict(entry))
158
+
159
+ def entries(self) -> list[dict[str, Any]]:
160
+ with self._lock:
161
+ return [dict(entry) for entry in self._entries]
@@ -0,0 +1,232 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """Scan MCP tool responses for prompt injection and data leakage."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import re
9
+ from dataclasses import dataclass, field
10
+
11
+ from agent_os.credential_redactor import CredentialRedactor
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ _INSTRUCTION_TAG_PATTERNS: tuple[re.Pattern[str], ...] = (
16
+ re.compile(
17
+ r"<(?:important|system|instruction|instructions|hidden|inject|admin|override|prompt|context|role)\b[^>]*>",
18
+ re.IGNORECASE,
19
+ ),
20
+ re.compile(r"\[(?:system|admin|instructions?)\]", re.IGNORECASE),
21
+ )
22
+ _IMPERATIVE_PATTERNS: tuple[re.Pattern[str], ...] = (
23
+ re.compile(r"ignore\s+(?:all\s+)?previous\s+(?:instructions?|context|rules?)", re.IGNORECASE),
24
+ re.compile(
25
+ r"(?:forget|disregard|override)\s+(?:all\s+)?(?:previous|above|prior|earlier)",
26
+ re.IGNORECASE,
27
+ ),
28
+ re.compile(r"\bexecute\s+this\b", re.IGNORECASE),
29
+ re.compile(r"\byou\s+are\s+now\b", re.IGNORECASE),
30
+ re.compile(r"\bnew\s+(?:role|instruction|directive|persona)\s*:", re.IGNORECASE),
31
+ re.compile(r"\bfrom\s+now\s+on\b", re.IGNORECASE),
32
+ re.compile(r"\bdo\s+not\s+(?:follow|obey|listen)\b", re.IGNORECASE),
33
+ )
34
+ _URL_PATTERN = re.compile(r"https?://[^\s<>'\"]+", re.IGNORECASE)
35
+ _EXFILTRATION_URL_PATTERN = re.compile(
36
+ r"(?i)(?:\b(?:api[_-]?key|token|secret|payload|data|dump|upload|exfil|webhook)\b|webhook\.site|requestbin|pastebin|ngrok|transfer\.sh)"
37
+ )
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class MCPResponseThreat:
42
+ """A threat detected in tool output."""
43
+
44
+ category: str
45
+ description: str
46
+ matched_pattern: str | None = None
47
+ details: dict[str, str] = field(default_factory=dict)
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class MCPResponseScanResult:
52
+ """Result of scanning an MCP tool response."""
53
+
54
+ is_safe: bool
55
+ tool_name: str
56
+ threats: list[MCPResponseThreat] = field(default_factory=list)
57
+
58
+ @classmethod
59
+ def safe(cls, tool_name: str) -> "MCPResponseScanResult":
60
+ return cls(is_safe=True, tool_name=tool_name, threats=[])
61
+
62
+ @classmethod
63
+ def unsafe(
64
+ cls,
65
+ tool_name: str,
66
+ *,
67
+ reason: str,
68
+ category: str = "error",
69
+ ) -> "MCPResponseScanResult":
70
+ return cls(
71
+ is_safe=False,
72
+ tool_name=tool_name,
73
+ threats=[MCPResponseThreat(category=category, description=reason)],
74
+ )
75
+
76
+
77
+ class MCPResponseScanner:
78
+ """Scan tool responses before they are returned to an LLM.
79
+
80
+ The scanner looks for prompt-injection markers, credential leaks, and
81
+ likely exfiltration URLs, returning structured findings that callers can
82
+ use to block or sanitize unsafe tool output.
83
+ """
84
+
85
+ def scan_response(
86
+ self,
87
+ response_content: str | None,
88
+ tool_name: str = "unknown",
89
+ ) -> MCPResponseScanResult:
90
+ """Scan tool output and return a structured safety result.
91
+
92
+ Args:
93
+ response_content: Raw tool output to inspect.
94
+ tool_name: Human-readable tool name for reporting.
95
+
96
+ Returns:
97
+ An ``MCPResponseScanResult`` marked safe when no threats are found.
98
+ Unexpected scanner failures return an unsafe result.
99
+ """
100
+ try:
101
+ if not response_content:
102
+ return MCPResponseScanResult.safe(tool_name)
103
+
104
+ threats: list[MCPResponseThreat] = []
105
+ threats.extend(
106
+ self._scan_patterns(
107
+ response_content,
108
+ patterns=_INSTRUCTION_TAG_PATTERNS,
109
+ category="instruction_injection",
110
+ description="Instruction tag detected in tool response.",
111
+ )
112
+ )
113
+ threats.extend(
114
+ self._scan_patterns(
115
+ response_content,
116
+ patterns=_IMPERATIVE_PATTERNS,
117
+ category="prompt_injection",
118
+ description="Imperative instruction detected in tool response.",
119
+ )
120
+ )
121
+ threats.extend(self._scan_credential_leaks(response_content))
122
+ threats.extend(self._scan_exfiltration_urls(response_content))
123
+
124
+ if not threats:
125
+ return MCPResponseScanResult.safe(tool_name)
126
+
127
+ logger.warning(
128
+ "MCP response scan found %s issue(s) in tool %s",
129
+ len(threats),
130
+ tool_name,
131
+ )
132
+ return MCPResponseScanResult(is_safe=False, tool_name=tool_name, threats=threats)
133
+ except Exception:
134
+ logger.error("MCP response scanning failed closed", exc_info=True)
135
+ return MCPResponseScanResult.unsafe(
136
+ tool_name,
137
+ reason="Response scanner error (fail-closed).",
138
+ )
139
+
140
+ def sanitize_response(
141
+ self,
142
+ response_content: str | None,
143
+ tool_name: str = "unknown",
144
+ ) -> tuple[str, list[MCPResponseThreat]]:
145
+ """Strip instruction tags from tool output and report stripped threats.
146
+
147
+ Args:
148
+ response_content: Raw tool output to sanitize.
149
+ tool_name: Human-readable tool name for reporting.
150
+
151
+ Returns:
152
+ A tuple of ``(sanitized_content, stripped_threats)``. On failure the
153
+ method returns an empty string and a fail-closed error finding.
154
+ """
155
+ try:
156
+ if not response_content:
157
+ return "", []
158
+
159
+ sanitized = response_content
160
+ stripped: list[MCPResponseThreat] = []
161
+ for pattern in _INSTRUCTION_TAG_PATTERNS:
162
+ for match in pattern.finditer(sanitized):
163
+ stripped.append(
164
+ MCPResponseThreat(
165
+ category="instruction_injection",
166
+ description="Instruction tag stripped from tool response.",
167
+ matched_pattern=match.group(0),
168
+ )
169
+ )
170
+ sanitized = pattern.sub("", sanitized)
171
+
172
+ return sanitized, stripped
173
+ except Exception:
174
+ logger.error("MCP response sanitization failed closed", exc_info=True)
175
+ return "", [
176
+ MCPResponseThreat(
177
+ category="error",
178
+ description=(
179
+ f"Response sanitization failed for tool '{tool_name}' (fail-closed)."
180
+ ),
181
+ )
182
+ ]
183
+
184
+ @staticmethod
185
+ def _scan_patterns(
186
+ content: str,
187
+ *,
188
+ patterns: tuple[re.Pattern[str], ...],
189
+ category: str,
190
+ description: str,
191
+ ) -> list[MCPResponseThreat]:
192
+ threats: list[MCPResponseThreat] = []
193
+ for pattern in patterns:
194
+ for match in pattern.finditer(content):
195
+ threats.append(
196
+ MCPResponseThreat(
197
+ category=category,
198
+ description=description,
199
+ matched_pattern=match.group(0),
200
+ )
201
+ )
202
+ return threats
203
+
204
+ @staticmethod
205
+ def _scan_credential_leaks(content: str) -> list[MCPResponseThreat]:
206
+ threats: list[MCPResponseThreat] = []
207
+ for credential_match in CredentialRedactor.find_matches(content):
208
+ threats.append(
209
+ MCPResponseThreat(
210
+ category="credential_leak",
211
+ description=f"{credential_match.name} detected in tool response.",
212
+ matched_pattern=credential_match.name,
213
+ details={"credential_type": credential_match.name},
214
+ )
215
+ )
216
+ return threats
217
+
218
+ @staticmethod
219
+ def _scan_exfiltration_urls(content: str) -> list[MCPResponseThreat]:
220
+ threats: list[MCPResponseThreat] = []
221
+ for match in _URL_PATTERN.finditer(content):
222
+ url = match.group(0)
223
+ if not _EXFILTRATION_URL_PATTERN.search(url):
224
+ continue
225
+ threats.append(
226
+ MCPResponseThreat(
227
+ category="data_exfiltration",
228
+ description="Potential data exfiltration URL detected in tool response.",
229
+ matched_pattern=url,
230
+ )
231
+ )
232
+ return threats