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,425 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Drift Detector — detects configuration and behavioral drift across agents.
|
|
4
|
+
|
|
5
|
+
Compares agent configurations, policies, and trust states across
|
|
6
|
+
repositories or environments to identify inconsistencies.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from agent_os.integrations.drift_detector import (
|
|
11
|
+
DriftDetector, DriftType,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
detector = DriftDetector()
|
|
15
|
+
findings = detector.compare_configs(
|
|
16
|
+
source_config={"max_tokens": 4096, "timeout": 300},
|
|
17
|
+
target_config={"max_tokens": 2048, "timeout": 300},
|
|
18
|
+
label="staging-vs-prod",
|
|
19
|
+
)
|
|
20
|
+
for f in findings:
|
|
21
|
+
print(f"{f.drift_type.value}: {f.field} — {f.message}")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from enum import Enum
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Enums & value objects ──────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DriftType(Enum):
|
|
39
|
+
"""Categories of drift that can be detected."""
|
|
40
|
+
|
|
41
|
+
CONFIG_DRIFT = "config_drift"
|
|
42
|
+
POLICY_DRIFT = "policy_drift"
|
|
43
|
+
TRUST_DRIFT = "trust_drift"
|
|
44
|
+
VERSION_DRIFT = "version_drift"
|
|
45
|
+
CAPABILITY_DRIFT = "capability_drift"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class DriftFinding:
|
|
50
|
+
"""A single drift finding.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
drift_type: Category of drift.
|
|
54
|
+
severity: ``"info"``, ``"warning"``, or ``"critical"``.
|
|
55
|
+
source: Label for the source side.
|
|
56
|
+
target: Label for the target side.
|
|
57
|
+
field: Configuration or policy field that drifted.
|
|
58
|
+
expected: Value on the source side.
|
|
59
|
+
actual: Value on the target side.
|
|
60
|
+
message: Human-readable description.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
drift_type: DriftType
|
|
64
|
+
severity: str
|
|
65
|
+
source: str
|
|
66
|
+
target: str
|
|
67
|
+
field: str
|
|
68
|
+
expected: Any
|
|
69
|
+
actual: Any
|
|
70
|
+
message: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class DriftReport:
|
|
75
|
+
"""Aggregate drift report.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
findings: All drift findings.
|
|
79
|
+
scanned_at: UTC timestamp of the scan.
|
|
80
|
+
sources_scanned: Number of sources compared.
|
|
81
|
+
summary: Brief textual summary.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
findings: list[DriftFinding] = field(default_factory=list)
|
|
85
|
+
scanned_at: str = ""
|
|
86
|
+
sources_scanned: int = 0
|
|
87
|
+
summary: str = ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── Drift Detector ─────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DriftDetector:
|
|
94
|
+
"""Detects configuration and behavioral drift across agents or environments.
|
|
95
|
+
|
|
96
|
+
All comparison methods return lists of :class:`DriftFinding`; the
|
|
97
|
+
high-level :meth:`scan` method accepts a list of source dicts and
|
|
98
|
+
produces a :class:`DriftReport`.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
# ── public comparison methods ───────────────────────────
|
|
102
|
+
|
|
103
|
+
def compare_configs(
|
|
104
|
+
self,
|
|
105
|
+
source_config: dict[str, Any],
|
|
106
|
+
target_config: dict[str, Any],
|
|
107
|
+
label: str = "",
|
|
108
|
+
) -> list[DriftFinding]:
|
|
109
|
+
"""Compare two configuration dicts and return findings for each diff.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
source_config: Reference configuration.
|
|
113
|
+
target_config: Configuration to compare against.
|
|
114
|
+
label: Optional human-readable label for this comparison.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of :class:`DriftFinding` for every key that differs.
|
|
118
|
+
"""
|
|
119
|
+
source_label = f"{label}/source" if label else "source"
|
|
120
|
+
target_label = f"{label}/target" if label else "target"
|
|
121
|
+
findings: list[DriftFinding] = []
|
|
122
|
+
|
|
123
|
+
all_keys = set(source_config) | set(target_config)
|
|
124
|
+
for key in sorted(all_keys):
|
|
125
|
+
src_val = source_config.get(key)
|
|
126
|
+
tgt_val = target_config.get(key)
|
|
127
|
+
if src_val != tgt_val:
|
|
128
|
+
severity = self._config_severity(key, src_val, tgt_val)
|
|
129
|
+
findings.append(
|
|
130
|
+
DriftFinding(
|
|
131
|
+
drift_type=DriftType.CONFIG_DRIFT,
|
|
132
|
+
severity=severity,
|
|
133
|
+
source=source_label,
|
|
134
|
+
target=target_label,
|
|
135
|
+
field=key,
|
|
136
|
+
expected=src_val,
|
|
137
|
+
actual=tgt_val,
|
|
138
|
+
message=self._config_message(key, src_val, tgt_val),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return findings
|
|
142
|
+
|
|
143
|
+
def compare_policies(
|
|
144
|
+
self,
|
|
145
|
+
source_policies: dict[str, Any],
|
|
146
|
+
target_policies: dict[str, Any],
|
|
147
|
+
) -> list[DriftFinding]:
|
|
148
|
+
"""Compare two policy dicts and return drift findings.
|
|
149
|
+
|
|
150
|
+
Missing keys on either side are flagged as *critical* drift.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
source_policies: Reference policy set.
|
|
154
|
+
target_policies: Policy set to compare.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of :class:`DriftFinding`.
|
|
158
|
+
"""
|
|
159
|
+
findings: list[DriftFinding] = []
|
|
160
|
+
all_keys = set(source_policies) | set(target_policies)
|
|
161
|
+
|
|
162
|
+
for key in sorted(all_keys):
|
|
163
|
+
src = source_policies.get(key)
|
|
164
|
+
tgt = target_policies.get(key)
|
|
165
|
+
if src == tgt:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
if src is None:
|
|
169
|
+
severity = "critical"
|
|
170
|
+
message = f"Policy '{key}' exists only in target"
|
|
171
|
+
elif tgt is None:
|
|
172
|
+
severity = "critical"
|
|
173
|
+
message = f"Policy '{key}' missing in target"
|
|
174
|
+
else:
|
|
175
|
+
severity = "warning"
|
|
176
|
+
message = f"Policy '{key}' differs: {src!r} vs {tgt!r}"
|
|
177
|
+
|
|
178
|
+
findings.append(
|
|
179
|
+
DriftFinding(
|
|
180
|
+
drift_type=DriftType.POLICY_DRIFT,
|
|
181
|
+
severity=severity,
|
|
182
|
+
source="source_policies",
|
|
183
|
+
target="target_policies",
|
|
184
|
+
field=key,
|
|
185
|
+
expected=src,
|
|
186
|
+
actual=tgt,
|
|
187
|
+
message=message,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
return findings
|
|
191
|
+
|
|
192
|
+
def compare_trust_scores(
|
|
193
|
+
self,
|
|
194
|
+
source_scores: dict[str, float],
|
|
195
|
+
target_scores: dict[str, float],
|
|
196
|
+
tolerance: float = 0.1,
|
|
197
|
+
) -> list[DriftFinding]:
|
|
198
|
+
"""Compare trust scores, flagging differences beyond *tolerance*.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
source_scores: Reference trust scores keyed by agent id.
|
|
202
|
+
target_scores: Trust scores to compare.
|
|
203
|
+
tolerance: Maximum allowed absolute difference before flagging.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
List of :class:`DriftFinding` for scores that drifted.
|
|
207
|
+
"""
|
|
208
|
+
findings: list[DriftFinding] = []
|
|
209
|
+
all_agents = set(source_scores) | set(target_scores)
|
|
210
|
+
|
|
211
|
+
for agent in sorted(all_agents):
|
|
212
|
+
src = source_scores.get(agent)
|
|
213
|
+
tgt = target_scores.get(agent)
|
|
214
|
+
|
|
215
|
+
if src is None or tgt is None:
|
|
216
|
+
findings.append(
|
|
217
|
+
DriftFinding(
|
|
218
|
+
drift_type=DriftType.TRUST_DRIFT,
|
|
219
|
+
severity="warning",
|
|
220
|
+
source="source_scores",
|
|
221
|
+
target="target_scores",
|
|
222
|
+
field=agent,
|
|
223
|
+
expected=src,
|
|
224
|
+
actual=tgt,
|
|
225
|
+
message=(
|
|
226
|
+
f"Trust score for '{agent}' missing on "
|
|
227
|
+
f"{'source' if src is None else 'target'} side"
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
diff = abs(src - tgt)
|
|
234
|
+
if diff > tolerance:
|
|
235
|
+
severity = "critical" if diff > tolerance * 3 else "warning"
|
|
236
|
+
findings.append(
|
|
237
|
+
DriftFinding(
|
|
238
|
+
drift_type=DriftType.TRUST_DRIFT,
|
|
239
|
+
severity=severity,
|
|
240
|
+
source="source_scores",
|
|
241
|
+
target="target_scores",
|
|
242
|
+
field=agent,
|
|
243
|
+
expected=src,
|
|
244
|
+
actual=tgt,
|
|
245
|
+
message=(
|
|
246
|
+
f"Trust score for '{agent}' drifted by "
|
|
247
|
+
f"{diff:.3f} (tolerance={tolerance})"
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
return findings
|
|
252
|
+
|
|
253
|
+
def detect_version_drift(
|
|
254
|
+
self, components: dict[str, str]
|
|
255
|
+
) -> list[DriftFinding]:
|
|
256
|
+
"""Find out-of-sync versions among components.
|
|
257
|
+
|
|
258
|
+
Expects a dict mapping component names to version strings. When
|
|
259
|
+
two or more distinct versions exist among *all* components that
|
|
260
|
+
share a common prefix (before the first ``-`` or ``/``), drift is
|
|
261
|
+
flagged.
|
|
262
|
+
|
|
263
|
+
For simpler use, the method also detects mixed version patterns
|
|
264
|
+
among *all* supplied components.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
components: Mapping of ``component_name → version_string``.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of :class:`DriftFinding` for version mismatches.
|
|
271
|
+
"""
|
|
272
|
+
findings: list[DriftFinding] = []
|
|
273
|
+
if not components:
|
|
274
|
+
return findings
|
|
275
|
+
|
|
276
|
+
# Group by prefix (e.g. "agent-os/kernel" → "agent-os")
|
|
277
|
+
groups: dict[str, dict[str, str]] = {}
|
|
278
|
+
for name, version in components.items():
|
|
279
|
+
prefix = name.split("-")[0].split("/")[0]
|
|
280
|
+
groups.setdefault(prefix, {})[name] = version
|
|
281
|
+
|
|
282
|
+
for prefix, members in sorted(groups.items()):
|
|
283
|
+
versions = set(members.values())
|
|
284
|
+
if len(versions) <= 1:
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
for name, version in sorted(members.items()):
|
|
288
|
+
majority = max(versions, key=lambda v: list(members.values()).count(v))
|
|
289
|
+
if version != majority:
|
|
290
|
+
findings.append(
|
|
291
|
+
DriftFinding(
|
|
292
|
+
drift_type=DriftType.VERSION_DRIFT,
|
|
293
|
+
severity="warning",
|
|
294
|
+
source=prefix,
|
|
295
|
+
target=name,
|
|
296
|
+
field="version",
|
|
297
|
+
expected=majority,
|
|
298
|
+
actual=version,
|
|
299
|
+
message=(
|
|
300
|
+
f"Component '{name}' at version {version} "
|
|
301
|
+
f"differs from majority {majority}"
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
return findings
|
|
306
|
+
|
|
307
|
+
def scan(self, sources: list[dict[str, Any]]) -> DriftReport:
|
|
308
|
+
"""Run a comprehensive scan over multiple source dicts.
|
|
309
|
+
|
|
310
|
+
Each source dict must contain a ``"label"`` key and at least one of:
|
|
311
|
+
- ``"config"`` — configuration dict
|
|
312
|
+
- ``"policies"`` — policy dict
|
|
313
|
+
- ``"trust_scores"`` — agent trust score dict
|
|
314
|
+
- ``"components"`` — version component dict
|
|
315
|
+
|
|
316
|
+
Pairwise comparisons are performed between successive sources.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
sources: List of source dicts to compare.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
A :class:`DriftReport`.
|
|
323
|
+
"""
|
|
324
|
+
all_findings: list[DriftFinding] = []
|
|
325
|
+
|
|
326
|
+
for i in range(len(sources) - 1):
|
|
327
|
+
src = sources[i]
|
|
328
|
+
tgt = sources[i + 1]
|
|
329
|
+
label = f"{src.get('label', f'src-{i}')}-vs-{tgt.get('label', f'src-{i+1}')}"
|
|
330
|
+
|
|
331
|
+
if "config" in src and "config" in tgt:
|
|
332
|
+
all_findings.extend(
|
|
333
|
+
self.compare_configs(src["config"], tgt["config"], label)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if "policies" in src and "policies" in tgt:
|
|
337
|
+
all_findings.extend(
|
|
338
|
+
self.compare_policies(src["policies"], tgt["policies"])
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if "trust_scores" in src and "trust_scores" in tgt:
|
|
342
|
+
tolerance = src.get("trust_tolerance", tgt.get("trust_tolerance", 0.1))
|
|
343
|
+
all_findings.extend(
|
|
344
|
+
self.compare_trust_scores(
|
|
345
|
+
src["trust_scores"], tgt["trust_scores"], tolerance
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Version drift across all sources that have components
|
|
350
|
+
all_components: dict[str, str] = {}
|
|
351
|
+
for src in sources:
|
|
352
|
+
all_components.update(src.get("components", {}))
|
|
353
|
+
if all_components:
|
|
354
|
+
all_findings.extend(self.detect_version_drift(all_components))
|
|
355
|
+
|
|
356
|
+
critical = sum(1 for f in all_findings if f.severity == "critical")
|
|
357
|
+
warnings = sum(1 for f in all_findings if f.severity == "warning")
|
|
358
|
+
info = sum(1 for f in all_findings if f.severity == "info")
|
|
359
|
+
|
|
360
|
+
report = DriftReport(
|
|
361
|
+
findings=all_findings,
|
|
362
|
+
scanned_at=datetime.now(timezone.utc).isoformat(),
|
|
363
|
+
sources_scanned=len(sources),
|
|
364
|
+
summary=(
|
|
365
|
+
f"{len(all_findings)} finding(s): "
|
|
366
|
+
f"{critical} critical, {warnings} warning, {info} info"
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
return report
|
|
370
|
+
|
|
371
|
+
def to_markdown(self, report: DriftReport) -> str:
|
|
372
|
+
"""Render a :class:`DriftReport` as a Markdown string.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
report: The report to render.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Markdown-formatted string.
|
|
379
|
+
"""
|
|
380
|
+
lines = [
|
|
381
|
+
"# Drift Detection Report",
|
|
382
|
+
"",
|
|
383
|
+
f"**Scanned at:** {report.scanned_at} ",
|
|
384
|
+
f"**Sources scanned:** {report.sources_scanned} ",
|
|
385
|
+
f"**Summary:** {report.summary}",
|
|
386
|
+
"",
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
if not report.findings:
|
|
390
|
+
lines.append("✅ No drift detected.")
|
|
391
|
+
return "\n".join(lines)
|
|
392
|
+
|
|
393
|
+
lines.append("| Severity | Type | Field | Expected | Actual | Message |")
|
|
394
|
+
lines.append("|----------|------|-------|----------|--------|---------|")
|
|
395
|
+
|
|
396
|
+
for f in report.findings:
|
|
397
|
+
lines.append(
|
|
398
|
+
f"| {f.severity} | {f.drift_type.value} | {f.field} "
|
|
399
|
+
f"| {f.expected} | {f.actual} | {f.message} |"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return "\n".join(lines)
|
|
403
|
+
|
|
404
|
+
# ── internal helpers ────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
@staticmethod
|
|
407
|
+
def _config_severity(
|
|
408
|
+
key: str, src_val: Any, tgt_val: Any
|
|
409
|
+
) -> str:
|
|
410
|
+
"""Determine severity of a config difference."""
|
|
411
|
+
if src_val is None or tgt_val is None:
|
|
412
|
+
return "critical"
|
|
413
|
+
if isinstance(src_val, (int, float)) and isinstance(tgt_val, (int, float)):
|
|
414
|
+
if src_val != 0 and abs(src_val - tgt_val) / abs(src_val) > 0.5:
|
|
415
|
+
return "warning"
|
|
416
|
+
return "info"
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def _config_message(key: str, src_val: Any, tgt_val: Any) -> str:
|
|
420
|
+
"""Build human-readable config diff message."""
|
|
421
|
+
if src_val is None:
|
|
422
|
+
return f"Key '{key}' only in target (value={tgt_val!r})"
|
|
423
|
+
if tgt_val is None:
|
|
424
|
+
return f"Key '{key}' missing in target (source={src_val!r})"
|
|
425
|
+
return f"Key '{key}' differs: {src_val!r} → {tgt_val!r}"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Dry-run policy wrapper for governance policies.
|
|
5
|
+
|
|
6
|
+
Wraps any GovernancePolicy and runs enforcement in "dry run" mode,
|
|
7
|
+
recording what WOULD have happened without actually blocking execution.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from .base import BaseIntegration, ExecutionContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DryRunDecision(Enum):
|
|
19
|
+
"""Decision that would have been made by the wrapped policy."""
|
|
20
|
+
ALLOW = "ALLOW"
|
|
21
|
+
DENY = "DENY"
|
|
22
|
+
WARN = "WARN"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DryRunResult:
|
|
27
|
+
"""Result of a dry-run policy evaluation."""
|
|
28
|
+
action: str
|
|
29
|
+
decision: DryRunDecision
|
|
30
|
+
reason: Optional[str]
|
|
31
|
+
policy_name: str
|
|
32
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DryRunCollector:
|
|
36
|
+
"""Accumulates dry-run results and provides summary reports."""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self._results: list[DryRunResult] = []
|
|
40
|
+
|
|
41
|
+
def add(self, result: DryRunResult) -> None:
|
|
42
|
+
self._results.append(result)
|
|
43
|
+
|
|
44
|
+
def get_results(self) -> list[DryRunResult]:
|
|
45
|
+
return list(self._results)
|
|
46
|
+
|
|
47
|
+
def summary(self) -> dict[str, Any]:
|
|
48
|
+
total = len(self._results)
|
|
49
|
+
counts = {d.value: 0 for d in DryRunDecision}
|
|
50
|
+
for r in self._results:
|
|
51
|
+
counts[r.decision.value] += 1
|
|
52
|
+
return {
|
|
53
|
+
"total": total,
|
|
54
|
+
"allowed": counts["ALLOW"],
|
|
55
|
+
"denied": counts["DENY"],
|
|
56
|
+
"warnings": counts["WARN"],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def clear(self) -> None:
|
|
60
|
+
self._results.clear()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DryRunPolicy:
|
|
64
|
+
"""
|
|
65
|
+
Wraps a BaseIntegration to run policy checks in dry-run mode.
|
|
66
|
+
|
|
67
|
+
All policy evaluations are recorded but never block execution.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
integration: BaseIntegration,
|
|
73
|
+
*,
|
|
74
|
+
policy_name: str = "default",
|
|
75
|
+
collector: Optional[DryRunCollector] = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
self._integration = integration
|
|
78
|
+
self._policy_name = policy_name
|
|
79
|
+
self.collector = collector or DryRunCollector()
|
|
80
|
+
|
|
81
|
+
def evaluate(self, action: str, context: ExecutionContext, input_data: Any = None) -> DryRunResult:
|
|
82
|
+
"""
|
|
83
|
+
Evaluate a policy check in dry-run mode.
|
|
84
|
+
|
|
85
|
+
Runs the wrapped integration's pre_execute and records the decision
|
|
86
|
+
without blocking. Always returns a DryRunResult.
|
|
87
|
+
"""
|
|
88
|
+
allowed, reason = self._integration.pre_execute(context, input_data)
|
|
89
|
+
|
|
90
|
+
if allowed:
|
|
91
|
+
decision = DryRunDecision.ALLOW
|
|
92
|
+
else:
|
|
93
|
+
# Distinguish warnings from denials: threshold-based checks
|
|
94
|
+
# (confidence, drift) are warnings; hard blocks are denials.
|
|
95
|
+
decision = DryRunDecision.DENY
|
|
96
|
+
|
|
97
|
+
result = DryRunResult(
|
|
98
|
+
action=action,
|
|
99
|
+
decision=decision,
|
|
100
|
+
reason=reason,
|
|
101
|
+
policy_name=self._policy_name,
|
|
102
|
+
)
|
|
103
|
+
self.collector.add(result)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
def evaluate_warn(self, action: str, reason: str) -> DryRunResult:
|
|
107
|
+
"""Record a warning without running policy checks."""
|
|
108
|
+
result = DryRunResult(
|
|
109
|
+
action=action,
|
|
110
|
+
decision=DryRunDecision.WARN,
|
|
111
|
+
reason=reason,
|
|
112
|
+
policy_name=self._policy_name,
|
|
113
|
+
)
|
|
114
|
+
self.collector.add(result)
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
def get_results(self) -> list[DryRunResult]:
|
|
118
|
+
return self.collector.get_results()
|
|
119
|
+
|
|
120
|
+
def summary(self) -> dict[str, Any]:
|
|
121
|
+
return self.collector.summary()
|
|
122
|
+
|
|
123
|
+
def clear(self) -> None:
|
|
124
|
+
self.collector.clear()
|