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,258 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """``agentos validate`` command implementation.
4
+
5
+ Validates policy YAML files against the bundled JSON Schema and performs
6
+ structural checks with human-readable error reporting.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ from pathlib import Path
15
+
16
+ from .output import Colors
17
+
18
+
19
+ # ============================================================================
20
+ # Schema & Structural Validation Helpers
21
+ # ============================================================================
22
+
23
+
24
+ def _load_json_schema() -> "dict | None":
25
+ """Load the bundled policy JSON schema, returning None if unavailable."""
26
+ schema_path = Path(__file__).parent.parent / "policies" / "policy_schema.json"
27
+ if schema_path.exists():
28
+ return json.loads(schema_path.read_text(encoding="utf-8"))
29
+ return None
30
+
31
+
32
+ def _validate_yaml_with_line_numbers(filepath: Path, content: dict, strict: bool) -> "tuple[list, list]":
33
+ """Validate a parsed YAML policy dict and return (errors, warnings).
34
+
35
+ Performs three validation passes in order:
36
+ 1. JSON Schema validation via ``jsonschema`` (best-effort, skipped if not installed).
37
+ 2. Required-field checks (``version``, ``name``).
38
+ 3. Rule structure checks and strict-mode unknown-field warnings.
39
+
40
+ Args:
41
+ filepath: Path to the source YAML file (used in error messages).
42
+ content: Parsed YAML content as a plain dict.
43
+ strict: When True, unknown top-level fields are reported as warnings.
44
+
45
+ Returns:
46
+ A tuple of (errors, warnings) where each element is a list of
47
+ human-readable strings prefixed with the filepath and location.
48
+ """
49
+ errors: list[str] = []
50
+ warnings: list[str] = []
51
+
52
+ # ── Pass 1: JSON Schema validation (best-effort) ──────────────────────
53
+ schema = _load_json_schema()
54
+ if schema is not None:
55
+ try:
56
+ import jsonschema # type: ignore[import-untyped]
57
+
58
+ validator = jsonschema.Draft7Validator(schema)
59
+ for ve in sorted(validator.iter_errors(content), key=lambda e: list(e.absolute_path)):
60
+ # Build a human-readable location string from the JSON path
61
+ location = " -> ".join(str(p) for p in ve.absolute_path) or "<root>"
62
+ error_msg = f"{filepath}: [{location}] {ve.message}"
63
+ # Downgrade rule-level schema errors to warnings for legacy rules with 'type'
64
+ path_parts = list(ve.absolute_path)
65
+ rules_list = content.get('rules')
66
+ if (len(path_parts) >= 2 and path_parts[0] == 'rules'
67
+ and isinstance(path_parts[1], int)
68
+ and isinstance(rules_list, list)
69
+ and path_parts[1] < len(rules_list)
70
+ and isinstance(rules_list[path_parts[1]], dict)
71
+ and 'type' in rules_list[path_parts[1]]):
72
+ warnings.append(error_msg)
73
+ else:
74
+ errors.append(error_msg)
75
+ except ImportError:
76
+ pass # jsonschema not installed — fall through to manual checks
77
+
78
+ # ── Pass 2: Required field checks ────────────────────────────────────
79
+ REQUIRED_FIELDS = ["version", "name"]
80
+ for field in REQUIRED_FIELDS:
81
+ if field not in content:
82
+ errors.append(f"{filepath}: Missing required field: '{field}'")
83
+
84
+ # Validate version format
85
+ if "version" in content:
86
+ version = str(content["version"])
87
+ if not re.match(r"^\d+(\.\d+)*$", version):
88
+ warnings.append(
89
+ f"{filepath}: Version '{version}' should be numeric (e.g., '1.0')"
90
+ )
91
+
92
+ # ── Pass 3: Rule structure checks ────────────────────────────────────
93
+ VALID_RULE_TYPES = ["allow", "deny", "audit", "require"]
94
+ VALID_ACTIONS = ["allow", "deny", "audit", "block"]
95
+
96
+ if "rules" in content:
97
+ rules = content["rules"]
98
+ if not isinstance(rules, list):
99
+ errors.append(f"{filepath}: 'rules' must be a list, got {type(rules).__name__}")
100
+ else:
101
+ for i, rule in enumerate(rules):
102
+ rule_ref = f"rules[{i + 1}]"
103
+ if not isinstance(rule, dict):
104
+ errors.append(f"{filepath}: {rule_ref} must be a mapping, got {type(rule).__name__}")
105
+ continue
106
+ # action must be a valid value
107
+ if "action" in rule and rule["action"] not in VALID_ACTIONS:
108
+ errors.append(
109
+ f"{filepath}: {rule_ref} invalid action '{rule['action']}' "
110
+ f"(valid: {VALID_ACTIONS})"
111
+ )
112
+ # legacy 'type' field warning
113
+ if "type" in rule and rule["type"] not in VALID_RULE_TYPES:
114
+ warnings.append(
115
+ f"{filepath}: {rule_ref} unknown type '{rule['type']}' "
116
+ f"(valid: {VALID_RULE_TYPES})"
117
+ )
118
+
119
+ # ── Pass 4: Strict mode — unknown top-level fields ───────────────────
120
+ if strict:
121
+ KNOWN_FIELDS = [
122
+ "version", "name", "description", "rules", "defaults",
123
+ "constraints", "signals", "allowed_actions", "blocked_actions",
124
+ "a2a_conversation_policy",
125
+ ]
126
+ for field in content.keys():
127
+ if field not in KNOWN_FIELDS:
128
+ warnings.append(f"{filepath}: Unknown top-level field '{field}'")
129
+
130
+ return errors, warnings
131
+
132
+
133
+ # ============================================================================
134
+ # Command
135
+ # ============================================================================
136
+
137
+
138
+ def cmd_validate(args: argparse.Namespace) -> int:
139
+ """Validate policy YAML files against the policy schema.
140
+
141
+ Parses each file, runs JSON Schema and structural validation, and
142
+ reports errors with field locations. Exits with a non-zero code when
143
+ any file fails validation (CI-friendly).
144
+
145
+ Args:
146
+ args: Parsed CLI arguments. Expects ``args.files`` (list of paths)
147
+ and ``args.strict`` (bool).
148
+
149
+ Returns:
150
+ 0 if all files are valid, 1 if any errors were found.
151
+ """
152
+ import yaml
153
+
154
+ print(f"\n{Colors.BOLD}Validating Policy Files{Colors.RESET}\n")
155
+
156
+ # ── Discover files ────────────────────────────────────────────────────
157
+ files_to_check: list[Path] = []
158
+ if args.files:
159
+ # Support both direct file paths and glob-style patterns
160
+ for f in args.files:
161
+ p = Path(f)
162
+ if "*" in f or "?" in f:
163
+ files_to_check.extend(sorted(Path(".").glob(f)))
164
+ else:
165
+ files_to_check.append(p)
166
+ else:
167
+ # Default: validate all YAML files in .agents/
168
+ agents_dir = Path(".agents")
169
+ if agents_dir.exists():
170
+ files_to_check = (
171
+ sorted(agents_dir.glob("*.yaml")) + sorted(agents_dir.glob("*.yml"))
172
+ )
173
+ if not files_to_check:
174
+ print(f"{Colors.YELLOW}No policy files found.{Colors.RESET}")
175
+ print("Run 'agentos init' to create default policies, or specify files directly.")
176
+ return 0
177
+
178
+ all_errors: list[str] = []
179
+ all_warnings: list[str] = []
180
+ valid_count = 0
181
+
182
+ for filepath in files_to_check:
183
+ if not filepath.exists():
184
+ all_errors.append(f"{filepath}: File not found")
185
+ print(f" {Colors.RED}✗{Colors.RESET} {filepath} — not found")
186
+ continue
187
+
188
+ print(f" Checking {filepath}...", end=" ", flush=True)
189
+
190
+ try:
191
+ # ── Step 1: Parse YAML (captures syntax errors with line numbers)
192
+ with open(filepath, encoding="utf-8") as f:
193
+ raw_text = f.read()
194
+
195
+ try:
196
+ content = yaml.safe_load(raw_text)
197
+ except yaml.YAMLError as exc:
198
+ # yaml.YAMLError includes line/column info in its string repr
199
+ msg = f"{filepath}: YAML syntax error — {exc}"
200
+ all_errors.append(msg)
201
+ print(f"{Colors.RED}PARSE ERROR{Colors.RESET}")
202
+ continue
203
+
204
+ if content is None:
205
+ all_errors.append(f"{filepath}: File is empty")
206
+ print(f"{Colors.RED}EMPTY{Colors.RESET}")
207
+ continue
208
+
209
+ if not isinstance(content, dict):
210
+ all_errors.append(
211
+ f"{filepath}: Top-level value must be a mapping, got {type(content).__name__}"
212
+ )
213
+ print(f"{Colors.RED}INVALID{Colors.RESET}")
214
+ continue
215
+
216
+ # ── Step 2: Schema + structural validation ─────────────────────
217
+ file_errors, file_warnings = _validate_yaml_with_line_numbers(
218
+ filepath, content, strict=getattr(args, "strict", False)
219
+ )
220
+
221
+ if file_errors:
222
+ all_errors.extend(file_errors)
223
+ print(f"{Colors.RED}INVALID{Colors.RESET}")
224
+ elif file_warnings:
225
+ all_warnings.extend(file_warnings)
226
+ print(f"{Colors.YELLOW}OK (warnings){Colors.RESET}")
227
+ valid_count += 1
228
+ else:
229
+ print(f"{Colors.GREEN}OK{Colors.RESET}")
230
+ valid_count += 1
231
+
232
+ except Exception as exc:
233
+ all_errors.append(f"{filepath}: Unexpected error — {exc}")
234
+ print(f"{Colors.RED}ERROR{Colors.RESET}")
235
+
236
+ print()
237
+
238
+ # ── Summary output ────────────────────────────────────────────────────
239
+ if all_warnings:
240
+ print(f"{Colors.YELLOW}Warnings:{Colors.RESET}")
241
+ for w in all_warnings:
242
+ print(f" [!] {w}")
243
+ print()
244
+
245
+ if all_errors:
246
+ print(f"{Colors.RED}Errors:{Colors.RESET}")
247
+ for e in all_errors:
248
+ print(f" [x] {e}")
249
+ print()
250
+ print(
251
+ f"{Colors.RED}Validation failed.{Colors.RESET} "
252
+ f"{valid_count}/{len(files_to_check)} file(s) valid."
253
+ )
254
+ return 1
255
+
256
+
257
+ print(f"{Colors.GREEN}All {valid_count} policy file(s) valid.{Colors.RESET}")
258
+ return 0
@@ -0,0 +1,265 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """
4
+ Agent OS MCP Security Scanner
5
+
6
+ Analyzes MCP server configurations for potential security risks,
7
+ capability exposure, and fingerprint violations.
8
+ """
9
+
10
+ import argparse
11
+ import json
12
+ import logging
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional
16
+
17
+ import yaml
18
+ from rich.console import Console
19
+ from rich.table import Table
20
+
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.WARNING)
23
+ logger = logging.getLogger("mcp-scan")
24
+
25
+ console = Console()
26
+
27
+
28
+ class SecurityFinding:
29
+ """Represents a security risk or discovery during scan."""
30
+ def __init__(self, server: str, severity: str, message: str, category: str):
31
+ self.server = server
32
+ self.severity = severity
33
+ self.message = message
34
+ self.category = category
35
+
36
+ def to_dict(self) -> Dict[str, str]:
37
+ return {
38
+ "server": self.server,
39
+ "severity": self.severity,
40
+ "message": self.message,
41
+ "category": self.category
42
+ }
43
+
44
+
45
+ def scan_config(config_path: Path, single_server: Optional[str] = None) -> List[SecurityFinding]:
46
+ """Scan MCP configuration for potential security risks."""
47
+ findings = []
48
+
49
+ try:
50
+ if config_path.suffix in [".yaml", ".yml"]:
51
+ with open(config_path) as f:
52
+ config = yaml.safe_load(f)
53
+ else:
54
+ with open(config_path) as f:
55
+ config = json.load(f)
56
+ except Exception as e:
57
+ findings.append(SecurityFinding("system", "critical", f"Failed to load config: {e}", "configuration"))
58
+ return findings
59
+
60
+ mcp_servers = config.get("mcpServers", {})
61
+
62
+ for name, server in mcp_servers.items():
63
+ if single_server and name != single_server:
64
+ continue
65
+
66
+ # 1. Environment Variable Check
67
+ env = server.get("env", {})
68
+ for key in env.keys():
69
+ if "KEY" in key.upper() or "SECRET" in key.upper() or "TOKEN" in key.upper():
70
+ findings.append(SecurityFinding(name, "warning", f"Sensitive key '{key}' exposed in environment", "leakage"))
71
+
72
+ # 2. Command Check
73
+ cmd = server.get("command", "")
74
+ if "sudo" in cmd.lower():
75
+ findings.append(SecurityFinding(name, "critical", "Server runs with sudo privileges", "privilege"))
76
+ if "/tmp/" in cmd.lower():
77
+ findings.append(SecurityFinding(name, "warning", "Server binary path in /tmp is risky", "execution"))
78
+
79
+ # 3. Arguments Check
80
+ args = server.get("args", [])
81
+ for arg in args:
82
+ if "/" in arg and Path(arg).is_absolute() and not arg.startswith(("/usr/", "/bin/", "/opt/")):
83
+ findings.append(SecurityFinding(name, "warning", f"Absolute path '{arg}' exposed in arguments", "leakage"))
84
+
85
+ return findings
86
+
87
+
88
+ def get_fingerprints(config_path: Path) -> Dict[str, str]:
89
+ """Generate fingerprints for all tools in the config."""
90
+ # Simulated fingerprinting
91
+ import hashlib
92
+
93
+ try:
94
+ with open(config_path) as f:
95
+ if config_path.suffix in [".yaml", ".yml"]:
96
+ config = yaml.safe_load(f)
97
+ else:
98
+ config = json.load(f)
99
+ except (json.JSONDecodeError, yaml.YAMLError, OSError, ValueError):
100
+ return {}
101
+
102
+ fingerprints = {}
103
+ mcp_servers = config.get("mcpServers", {})
104
+ for name, server in mcp_servers.items():
105
+ cmd = str(server.get("command", ""))
106
+ args = str(server.get("args", []))
107
+ h = hashlib.sha256(f"{cmd}{args}".encode()).hexdigest()[:16]
108
+ fingerprints[name] = h
109
+
110
+ return fingerprints
111
+
112
+
113
+ def build_parser() -> argparse.ArgumentParser:
114
+ """Build the CLI argument parser."""
115
+ parser = argparse.ArgumentParser(
116
+ prog="mcp-scan",
117
+ description="Agent OS MCP Security Scanner - Analyze MCP configs for risks"
118
+ )
119
+
120
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
121
+
122
+ # -- scan ---------------------------------------------------------------
123
+ scan_parser = subparsers.add_parser("scan", help="Scan MCP config for threats")
124
+ scan_parser.add_argument("config", help="Path to MCP config file (JSON/YAML)")
125
+ scan_parser.add_argument("--server", default=None, help="Scan only this server")
126
+ scan_parser.add_argument("--format", choices=["json", "table", "markdown"], default="table", help="Output format")
127
+ scan_parser.add_argument("--severity", choices=["warning", "critical"], default=None, help="Min severity")
128
+ scan_parser.add_argument("--json", action="store_true", help="Output in JSON format")
129
+
130
+ # -- fingerprint --------------------------------------------------------
131
+ fp_parser = subparsers.add_parser("fingerprint", help="Register/compare tool fingerprints")
132
+ fp_parser.add_argument("config", help="Path to MCP config file (JSON/YAML)")
133
+ fp_parser.add_argument("--output", default=None, help="Save fingerprints to file")
134
+ fp_parser.add_argument("--compare", default=None, help="Compare against saved file")
135
+ fp_parser.add_argument("--json", action="store_true", help="Output in JSON format")
136
+
137
+ # -- report -------------------------------------------------------------
138
+ report_parser = subparsers.add_parser("report", help="Generate a full security report")
139
+ report_parser.add_argument("config", help="Path to MCP config file (JSON/YAML)")
140
+ report_parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Report format")
141
+ report_parser.add_argument("--json", action="store_true", help="Output in JSON format")
142
+
143
+ return parser
144
+
145
+
146
+ def main(argv: List[str] | None = None) -> int:
147
+ """CLI entry point."""
148
+ parser = build_parser()
149
+ args = parser.parse_args(argv)
150
+
151
+ if not args.command:
152
+ parser.print_help()
153
+ return 0
154
+
155
+ config_path = Path(args.config)
156
+ if not config_path.exists():
157
+ print(f"Error: Config file not found: {args.config}")
158
+ return 1
159
+
160
+ output_format = "json" if getattr(args, "json", False) or getattr(args, "format", "table") == "json" else "table"
161
+
162
+ try:
163
+ if args.command == "scan":
164
+ findings = scan_config(config_path, args.server)
165
+
166
+ if args.severity:
167
+ findings = [f for f in findings if f.severity == args.severity or f.severity == "critical"]
168
+
169
+ if output_format == "json":
170
+ print(json.dumps([f.to_dict() for f in findings], indent=2))
171
+ elif output_format == "table":
172
+ table = Table(title=f"Security Scan: {args.config}")
173
+ table.add_column("Server", style="cyan")
174
+ table.add_column("Severity", style="bold")
175
+ table.add_column("Category", style="dim")
176
+ table.add_column("Finding")
177
+
178
+ for f in findings:
179
+ sev_color = "red" if f.severity == "critical" else "yellow"
180
+ table.add_row(f.server, f"[{sev_color}]{f.severity.upper()}[/{sev_color}]", f.category, f.message)
181
+
182
+ console.print(table)
183
+
184
+ return 1 if any(f.severity == "critical" for f in findings) else 0
185
+
186
+ elif args.command == "fingerprint":
187
+ fingerprints = get_fingerprints(config_path)
188
+
189
+ if args.compare:
190
+ with open(args.compare) as f:
191
+ saved = json.load(f)
192
+
193
+ diffs = {}
194
+ for name, h in fingerprints.items():
195
+ if name not in saved:
196
+ diffs[name] = "new"
197
+ elif saved[name] != h:
198
+ diffs[name] = "changed"
199
+
200
+ if output_format == "json":
201
+ print(json.dumps({"current": fingerprints, "diffs": diffs}, indent=2))
202
+ else:
203
+ print(f"Comparison results for {args.config}:")
204
+ for name, status in diffs.items():
205
+ print(f" {name}: {status}")
206
+ if not diffs:
207
+ print(" Identical fingerprints.")
208
+
209
+ elif args.output:
210
+ with open(args.output, "w") as f:
211
+ json.dump(fingerprints, f, indent=2)
212
+ if output_format != "json":
213
+ print(f"Fingerprints saved to {args.output}")
214
+ else:
215
+ print(json.dumps({"status": "success", "file": args.output}, indent=2))
216
+
217
+ else:
218
+ if output_format == "json":
219
+ print(json.dumps(fingerprints, indent=2))
220
+ else:
221
+ for name, h in fingerprints.items():
222
+ print(f"{name:20} {h}")
223
+
224
+ elif args.command == "report":
225
+ findings = scan_config(config_path)
226
+ fingerprints = get_fingerprints(config_path)
227
+
228
+ report = {
229
+ "config": str(config_path),
230
+ "summary": {
231
+ "total_servers": len(fingerprints),
232
+ "total_findings": len(findings),
233
+ "critical": len([f for f in findings if f.severity == "critical"]),
234
+ "warning": len([f for f in findings if f.severity == "warning"])
235
+ },
236
+ "findings": [f.to_dict() for f in findings],
237
+ "fingerprints": fingerprints
238
+ }
239
+
240
+ if output_format == "json" or getattr(args, "format", "markdown") == "json":
241
+ print(json.dumps(report, indent=2))
242
+ else:
243
+ # Simple markdown report
244
+ print(f"# Security Report: {args.config}")
245
+ print()
246
+ print(f"- Total Servers: {report['summary']['total_servers']}")
247
+ print(f"- Total Findings: {report['summary']['total_findings']}")
248
+ print()
249
+ print("## Findings")
250
+ for f in findings:
251
+ print(f"- **{f.server}** ({f.severity.upper()}): {f.message}")
252
+
253
+ return 0
254
+ except Exception as e:
255
+ is_known = isinstance(e, (FileNotFoundError, ValueError, yaml.YAMLError))
256
+ msg = "A file access or syntax error occurred." if is_known else "An error occurred during scanning"
257
+ if output_format == "json":
258
+ print(json.dumps({"status": "error", "message": msg, "type": "ScanError" if is_known else "InternalError"}, indent=2))
259
+ else:
260
+ print(f"Error: {msg}")
261
+ return 1
262
+
263
+
264
+ if __name__ == "__main__":
265
+ sys.exit(main())