agent_os_kernel 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- nexus/tests/test_schemas.py +295 -0
|
@@ -0,0 +1,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
|
agent_os/cli/mcp_scan.py
ADDED
|
@@ -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())
|