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,538 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """Security Skills — static analysis checks for agent code.
4
+
5
+ Codifies patterns discovered during security audits into reusable
6
+ scan functions that can run in CI, pre-commit hooks, or agent-based
7
+ code review workflows.
8
+
9
+ Each skill returns a list of :class:`SecurityFinding` objects with
10
+ severity, description, and line number information.
11
+
12
+ Architecture:
13
+ scan_file(path)
14
+ ├─ check_stub_security() — verify/validate stubs returning True
15
+ ├─ check_unsafe_pickle() — pickle.loads without HMAC
16
+ ├─ check_hardcoded_denylist() — inline security pattern lists
17
+ ├─ check_unbounded_collections() — dicts/lists without size cap
18
+ ├─ check_ssrf_urls() — URLs from input without guard
19
+ ├─ check_missing_circuit_breaker() — external calls without backoff
20
+ ├─ check_redos_patterns() — regex without complexity limits
21
+ ├─ check_hardcoded_secrets() — API keys, tokens in source
22
+ ├─ check_trust_without_crypto() — trust decisions without signatures
23
+ └─ check_error_info_leak() — exceptions leaking internals
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from dataclasses import dataclass
30
+ from enum import Enum
31
+ from pathlib import Path
32
+ from typing import Callable, Sequence
33
+
34
+
35
+ class Severity(str, Enum):
36
+ CRITICAL = "critical"
37
+ HIGH = "high"
38
+ MEDIUM = "medium"
39
+ LOW = "low"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class SecurityFinding:
44
+ """A single security issue found during scanning."""
45
+
46
+ rule_id: str
47
+ title: str
48
+ severity: Severity
49
+ description: str
50
+ file_path: str = ""
51
+ line_number: int = 0
52
+ suggestion: str = ""
53
+ owasp_risks: tuple[str, ...] = ()
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Individual skill checks
58
+ # ---------------------------------------------------------------------------
59
+
60
+ def _find_line(source: str, pattern: re.Pattern[str]) -> int:
61
+ """Return 1-based line number of first match, or 0."""
62
+ for i, line in enumerate(source.splitlines(), 1):
63
+ if pattern.search(line):
64
+ return i
65
+ return 0
66
+
67
+
68
+ _STUB_PATTERN = re.compile(
69
+ r"def\s+(verify|validate|authenticate|check_permission|"
70
+ r"is_authorized|is_trusted|authorize)\s*\([^)]*\)\s*"
71
+ r"(?:->.*?)?:\s*\n\s+(?:#[^\n]*)?\s*return\s+True"
72
+ )
73
+
74
+
75
+ def check_stub_security(source: str, path: str = "") -> list[SecurityFinding]:
76
+ """Detect security functions that unconditionally return True."""
77
+ findings: list[SecurityFinding] = []
78
+ for m in _STUB_PATTERN.finditer(source):
79
+ fn_name = m.group(1)
80
+ line = source[:m.start()].count("\n") + 1
81
+ findings.append(SecurityFinding(
82
+ rule_id="SKILL-001",
83
+ title=f"Stub security function: {fn_name}()",
84
+ severity=Severity.CRITICAL,
85
+ description=(
86
+ f"Function '{fn_name}()' unconditionally returns True. "
87
+ "An attacker can bypass this security boundary entirely. "
88
+ "This was the root cause of a real-world identity fabrication "
89
+ "vulnerability in agent trust handshakes."
90
+ ),
91
+ file_path=path,
92
+ line_number=line,
93
+ suggestion=(
94
+ "Implement actual verification logic with cryptographic "
95
+ "challenge-response or registry lookup."
96
+ ),
97
+ owasp_risks=("AT02", "AT07"),
98
+ ))
99
+ return findings
100
+
101
+
102
+ _PICKLE_LOAD = re.compile(r"pickle\.loads?\s*\(")
103
+ _HMAC_CHECK = re.compile(
104
+ r"hmac\.(new|compare_digest)|verify.*signature|verify.*integrity",
105
+ re.IGNORECASE,
106
+ )
107
+
108
+
109
+ def check_unsafe_pickle(source: str, path: str = "") -> list[SecurityFinding]:
110
+ """Detect pickle.loads without HMAC verification."""
111
+ if not _PICKLE_LOAD.search(source):
112
+ return []
113
+ if _HMAC_CHECK.search(source):
114
+ return []
115
+ line = _find_line(source, _PICKLE_LOAD)
116
+ return [SecurityFinding(
117
+ rule_id="SKILL-002",
118
+ title="pickle.loads() without integrity verification",
119
+ severity=Severity.CRITICAL,
120
+ description=(
121
+ "pickle.loads() is called without HMAC or signature verification. "
122
+ "Tampered pickle data enables arbitrary code execution."
123
+ ),
124
+ file_path=path,
125
+ line_number=line,
126
+ suggestion="Sign data with HMAC-SHA256 and verify before deserializing.",
127
+ owasp_risks=("AT02", "AT07"),
128
+ )]
129
+
130
+
131
+ _DENYLIST_PATTERN = re.compile(
132
+ r"(?:dangerous_patterns|blocked_patterns|destructive_patterns|"
133
+ r"sensitive_keywords|HARM_PATTERNS|ILLEGAL_PATTERNS|"
134
+ r"MALWARE_PATTERNS|BLOCKED_)\s*=\s*\["
135
+ )
136
+
137
+
138
+ def check_hardcoded_denylist(source: str, path: str = "") -> list[SecurityFinding]:
139
+ """Detect hardcoded security deny-lists in source."""
140
+ findings: list[SecurityFinding] = []
141
+ for m in _DENYLIST_PATTERN.finditer(source):
142
+ line = source[:m.start()].count("\n") + 1
143
+ findings.append(SecurityFinding(
144
+ rule_id="SKILL-003",
145
+ title="Hardcoded security deny-list",
146
+ severity=Severity.HIGH,
147
+ description=(
148
+ "Security patterns are hardcoded in source. Attackers with "
149
+ "read access can reverse-engineer bypass strategies."
150
+ ),
151
+ file_path=path,
152
+ line_number=line,
153
+ suggestion=(
154
+ "Externalize into YAML config loaded at runtime. Keep "
155
+ "built-in defaults in a dataclass but warn when used."
156
+ ),
157
+ owasp_risks=("AT01", "AT08"),
158
+ ))
159
+ return findings
160
+
161
+
162
+ _CACHE_DICT = re.compile(
163
+ r"(?:self\.)?_(?:cache|sessions|pending|peers|clients|buckets|"
164
+ r"tokens|challenges|nonces|attempts)\s*[=:]\s*(?:\{\}|dict\(\)|defaultdict)"
165
+ )
166
+ _EVICTION = re.compile(
167
+ r"\.pop\(|\.popitem\(|max_size|maxsize|_MAX_|_evict|_cleanup|"
168
+ r"LRUCache|lru_cache|OrderedDict|_max_\w+\s*=",
169
+ re.IGNORECASE,
170
+ )
171
+
172
+
173
+ def check_unbounded_collections(
174
+ source: str, path: str = ""
175
+ ) -> list[SecurityFinding]:
176
+ """Detect security-sensitive dicts/lists without size limits."""
177
+ if not _CACHE_DICT.search(source):
178
+ return []
179
+ if _EVICTION.search(source):
180
+ return []
181
+ line = _find_line(source, _CACHE_DICT)
182
+ return [SecurityFinding(
183
+ rule_id="SKILL-004",
184
+ title="Unbounded security-sensitive collection",
185
+ severity=Severity.MEDIUM,
186
+ description=(
187
+ "A dict/list used for caching or session tracking grows "
188
+ "without size limit. An attacker can exhaust memory."
189
+ ),
190
+ file_path=path,
191
+ line_number=line,
192
+ suggestion="Add _MAX_ENTRIES and evict oldest entries when full.",
193
+ owasp_risks=("AT05",),
194
+ )]
195
+
196
+
197
+ _URL_FROM_INPUT = re.compile(
198
+ r"(?:server_url|endpoint|url|base_url)\s*[:=].*"
199
+ r"(?:args|params|request|input|config)",
200
+ re.IGNORECASE,
201
+ )
202
+ _SSRF_GUARD = re.compile(
203
+ r"(?:localhost|127\.0\.0\.1|169\.254|::1|0\.0\.0\.0).*block|"
204
+ r"ssrf|_BLOCKED_HOSTS|validate_url|_is_safe_url",
205
+ re.IGNORECASE,
206
+ )
207
+
208
+
209
+ def check_ssrf_urls(source: str, path: str = "") -> list[SecurityFinding]:
210
+ """Detect URLs from input used without SSRF validation."""
211
+ if not _URL_FROM_INPUT.search(source):
212
+ return []
213
+ if _SSRF_GUARD.search(source):
214
+ return []
215
+ line = _find_line(source, _URL_FROM_INPUT)
216
+ return [SecurityFinding(
217
+ rule_id="SKILL-005",
218
+ title="SSRF-vulnerable URL handling",
219
+ severity=Severity.HIGH,
220
+ description=(
221
+ "A URL derived from user/agent input is used without SSRF "
222
+ "validation. Attackers can reach internal services (cloud "
223
+ "metadata endpoints, localhost admin panels)."
224
+ ),
225
+ file_path=path,
226
+ line_number=line,
227
+ suggestion="Block reserved/internal addresses before making requests.",
228
+ owasp_risks=("AT02", "AT07"),
229
+ )]
230
+
231
+
232
+ _EXTERNAL_CALL = re.compile(
233
+ r"httpx\.|aiohttp\.|requests\.|fetch\(|urllib|invoke_tool|call_tool",
234
+ re.IGNORECASE,
235
+ )
236
+ _CIRCUIT_BREAKER = re.compile(
237
+ r"circuit.?breaker|CircuitBreaker|_failures.*threshold|"
238
+ r"backoff|retry.*max|tenacity",
239
+ re.IGNORECASE,
240
+ )
241
+
242
+
243
+ def check_missing_circuit_breaker(
244
+ source: str, path: str = ""
245
+ ) -> list[SecurityFinding]:
246
+ """Detect external calls without circuit breaker."""
247
+ if not _EXTERNAL_CALL.search(source):
248
+ return []
249
+ if _CIRCUIT_BREAKER.search(source):
250
+ return []
251
+ line = _find_line(source, _EXTERNAL_CALL)
252
+ return [SecurityFinding(
253
+ rule_id="SKILL-006",
254
+ title="External calls without circuit breaker",
255
+ severity=Severity.MEDIUM,
256
+ description=(
257
+ "External service calls lack circuit breaker pattern. "
258
+ "Failing downstream services cause cascading failures."
259
+ ),
260
+ file_path=path,
261
+ line_number=line,
262
+ suggestion=(
263
+ "Track consecutive failures per endpoint and stop calling "
264
+ "after threshold is exceeded."
265
+ ),
266
+ owasp_risks=("AT05", "AT10"),
267
+ )]
268
+
269
+
270
+ _REGEX_COMPILE = re.compile(r"re\.compile\(\s*['\"](.+?)['\"]\s*\)")
271
+ _REDOS_INDICATORS = re.compile(r"(\.\+|\.\*)\1|(\([^)]+\))\+\+|\(\?!.*\)\+")
272
+
273
+
274
+ def check_redos_patterns(source: str, path: str = "") -> list[SecurityFinding]:
275
+ """Detect regex patterns susceptible to catastrophic backtracking."""
276
+ findings: list[SecurityFinding] = []
277
+ for m in _REGEX_COMPILE.finditer(source):
278
+ pattern_str = m.group(1)
279
+ if _REDOS_INDICATORS.search(pattern_str):
280
+ line = source[:m.start()].count("\n") + 1
281
+ findings.append(SecurityFinding(
282
+ rule_id="SKILL-007",
283
+ title="Potential ReDoS pattern",
284
+ severity=Severity.MEDIUM,
285
+ description=(
286
+ f"Regex '{pattern_str[:60]}...' has nested quantifiers "
287
+ "that may cause catastrophic backtracking (ReDoS)."
288
+ ),
289
+ file_path=path,
290
+ line_number=line,
291
+ suggestion=(
292
+ "Use atomic groups, possessive quantifiers, or "
293
+ "re2/regex library with backtracking limits."
294
+ ),
295
+ owasp_risks=("AT05",),
296
+ ))
297
+ return findings
298
+
299
+
300
+ _SECRET_PATTERNS = [
301
+ (re.compile(r"""(?:api[_-]?key|secret[_-]?key|password|token)\s*=\s*['"][A-Za-z0-9+/=_-]{16,}['"]""", re.IGNORECASE), "Hardcoded secret/API key"),
302
+ (re.compile(r"AKIA[0-9A-Z]{16}"), "AWS access key ID"),
303
+ (re.compile(r"gh[ps]_[A-Za-z0-9]{36}"), "GitHub token"),
304
+ (re.compile(r"sk-[A-Za-z0-9]{32,}"), "OpenAI API key"),
305
+ ]
306
+
307
+
308
+ def check_hardcoded_secrets(source: str, path: str = "") -> list[SecurityFinding]:
309
+ """Detect hardcoded API keys, tokens, and secrets."""
310
+ findings: list[SecurityFinding] = []
311
+ for pattern, label in _SECRET_PATTERNS:
312
+ for m in pattern.finditer(source):
313
+ # Skip test files and example placeholders
314
+ if "test" in path.lower() or "example" in path.lower():
315
+ continue
316
+ if "YOUR_" in m.group(0) or "PLACEHOLDER" in m.group(0):
317
+ continue
318
+ line = source[:m.start()].count("\n") + 1
319
+ findings.append(SecurityFinding(
320
+ rule_id="SKILL-008",
321
+ title=f"Hardcoded secret: {label}",
322
+ severity=Severity.CRITICAL,
323
+ description=(
324
+ f"Possible hardcoded credential ({label}) in source. "
325
+ "Secrets in code are exposed through version control."
326
+ ),
327
+ file_path=path,
328
+ line_number=line,
329
+ suggestion="Use environment variables or Azure Key Vault.",
330
+ owasp_risks=("AT02",),
331
+ ))
332
+ return findings
333
+
334
+
335
+ _TRUST_DECISION = re.compile(
336
+ r"(?:trust_score|trust_level|verified|is_trusted)\s*=\s*"
337
+ r"(?:True|['\"]trusted['\"]|\d{3,})",
338
+ re.IGNORECASE,
339
+ )
340
+ _CRYPTO_VERIFY = re.compile(
341
+ r"ed25519|nacl|verify_key|signature.*verify|"
342
+ r"cryptograph|verify_signature|_verify_ed25519",
343
+ re.IGNORECASE,
344
+ )
345
+
346
+
347
+ def check_trust_without_crypto(
348
+ source: str, path: str = ""
349
+ ) -> list[SecurityFinding]:
350
+ """Detect trust decisions made without cryptographic verification."""
351
+ if not _TRUST_DECISION.search(source):
352
+ return []
353
+ if _CRYPTO_VERIFY.search(source):
354
+ return []
355
+ line = _find_line(source, _TRUST_DECISION)
356
+ return [SecurityFinding(
357
+ rule_id="SKILL-009",
358
+ title="Trust decision without cryptographic verification",
359
+ severity=Severity.HIGH,
360
+ description=(
361
+ "Trust score, level, or verification status is assigned without "
362
+ "cryptographic proof (Ed25519, HMAC). Attackers can fabricate "
363
+ "trusted identities."
364
+ ),
365
+ file_path=path,
366
+ line_number=line,
367
+ suggestion=(
368
+ "Require Ed25519 signature verification before assigning "
369
+ "trust scores. Use challenge-response for peer verification."
370
+ ),
371
+ owasp_risks=("AT02", "AT07"),
372
+ )]
373
+
374
+
375
+ _EXCEPTION_EXPOSE = re.compile(
376
+ r"""(?:str\((?:e|err|exc|exception)\)|(?:e|err|exc)\.(?:args|message)|"""
377
+ r"""repr\((?:e|err|exc)\))"""
378
+ )
379
+ _SANITIZE_ERROR = re.compile(
380
+ r"sanitize|truncat|strip|redact|type\(.*\)\.__name__",
381
+ re.IGNORECASE,
382
+ )
383
+
384
+
385
+ def check_error_info_leak(source: str, path: str = "") -> list[SecurityFinding]:
386
+ """Detect exception details leaked to callers."""
387
+ if not _EXCEPTION_EXPOSE.search(source):
388
+ return []
389
+ if _SANITIZE_ERROR.search(source):
390
+ return []
391
+ line = _find_line(source, _EXCEPTION_EXPOSE)
392
+ return [SecurityFinding(
393
+ rule_id="SKILL-010",
394
+ title="Exception details exposed to caller",
395
+ severity=Severity.MEDIUM,
396
+ description=(
397
+ "Full exception details (str(e), repr(e)) are returned or "
398
+ "logged at user-visible level. Internal paths, stack frames, "
399
+ "and SQL queries may leak to attackers."
400
+ ),
401
+ file_path=path,
402
+ line_number=line,
403
+ suggestion=(
404
+ "Return only type(e).__name__ to callers. Log full details "
405
+ "at DEBUG level only."
406
+ ),
407
+ owasp_risks=("AT02",),
408
+ )]
409
+
410
+
411
+ # ---------------------------------------------------------------------------
412
+ # Aggregate scanner
413
+ # ---------------------------------------------------------------------------
414
+
415
+ ALL_CHECKS: list[Callable[[str, str], list[SecurityFinding]]] = [
416
+ check_stub_security,
417
+ check_unsafe_pickle,
418
+ check_hardcoded_denylist,
419
+ check_unbounded_collections,
420
+ check_ssrf_urls,
421
+ check_missing_circuit_breaker,
422
+ check_redos_patterns,
423
+ check_hardcoded_secrets,
424
+ check_trust_without_crypto,
425
+ check_error_info_leak,
426
+ ]
427
+
428
+
429
+ def scan_source(source: str, path: str = "") -> list[SecurityFinding]:
430
+ """Run all security skills against source code.
431
+
432
+ Args:
433
+ source: Raw Python source text.
434
+ path: Optional file path for findings metadata.
435
+
436
+ Returns:
437
+ List of all findings across all checks.
438
+ """
439
+ findings: list[SecurityFinding] = []
440
+ for check in ALL_CHECKS:
441
+ findings.extend(check(source, path))
442
+ return findings
443
+
444
+
445
+ def scan_file(file_path: str | Path) -> list[SecurityFinding]:
446
+ """Scan a single Python file.
447
+
448
+ Args:
449
+ file_path: Path to the .py file to scan.
450
+
451
+ Returns:
452
+ List of findings. Empty list if file cannot be read.
453
+ """
454
+ p = Path(file_path)
455
+ if not p.exists() or not p.suffix == ".py":
456
+ return []
457
+ try:
458
+ source = p.read_text(encoding="utf-8", errors="replace")
459
+ except OSError:
460
+ return []
461
+ return scan_source(source, str(p))
462
+
463
+
464
+ def scan_directory(
465
+ directory: str | Path,
466
+ *,
467
+ exclude_tests: bool = False,
468
+ exclude_patterns: Sequence[str] = ("__pycache__", ".git", "node_modules"),
469
+ ) -> list[SecurityFinding]:
470
+ """Recursively scan all Python files in a directory.
471
+
472
+ Args:
473
+ directory: Root directory to scan.
474
+ exclude_tests: Skip files with 'test' in the name.
475
+ exclude_patterns: Directory name patterns to skip.
476
+
477
+ Returns:
478
+ Aggregated findings across all scanned files.
479
+ """
480
+ root = Path(directory)
481
+ findings: list[SecurityFinding] = []
482
+ for py_file in root.rglob("*.py"):
483
+ if any(pat in str(py_file) for pat in exclude_patterns):
484
+ continue
485
+ if exclude_tests and "test" in py_file.name.lower():
486
+ continue
487
+ findings.extend(scan_file(py_file))
488
+ return findings
489
+
490
+
491
+ def format_findings(findings: list[SecurityFinding]) -> str:
492
+ """Format findings as a human-readable report.
493
+
494
+ Args:
495
+ findings: List of SecurityFinding objects.
496
+
497
+ Returns:
498
+ Markdown-formatted report string.
499
+ """
500
+ if not findings:
501
+ return "✅ **Security scan passed.** No issues found."
502
+
503
+ by_severity = {s: [] for s in Severity}
504
+ for f in findings:
505
+ by_severity[f.severity].append(f)
506
+
507
+ counts = {s.value: len(fs) for s, fs in by_severity.items() if fs}
508
+ summary_parts = [f"{v} {k}" for k, v in counts.items()]
509
+ header = (
510
+ f"❌ **Security scan found {len(findings)} issue(s)**: "
511
+ + ", ".join(summary_parts) + ".\n"
512
+ )
513
+
514
+ lines = [header]
515
+ icons = {
516
+ Severity.CRITICAL: "🔴",
517
+ Severity.HIGH: "🟠",
518
+ Severity.MEDIUM: "🟡",
519
+ Severity.LOW: "🔵",
520
+ }
521
+
522
+ for f in sorted(findings, key=lambda x: list(Severity).index(x.severity)):
523
+ icon = icons.get(f.severity, "⚪")
524
+ loc = f"{f.file_path}:{f.line_number}" if f.line_number else f.file_path
525
+ lines.append(f"### {icon} [{f.rule_id}] {f.title}")
526
+ if loc:
527
+ lines.append(f"**Location:** `{loc}`")
528
+ lines.append(f.description)
529
+ if f.suggestion:
530
+ lines.append(f"\n**Suggested fix:** {f.suggestion}")
531
+ if f.owasp_risks:
532
+ lines.append(
533
+ "**OWASP Agentic Top-10:** "
534
+ + ", ".join(f"`{r}`" for r in f.owasp_risks)
535
+ )
536
+ lines.append("---")
537
+
538
+ return "\n".join(lines)