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,143 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Role-Based Access Control (RBAC) for Agent OS.
|
|
5
|
+
|
|
6
|
+
Provides role assignment, policy lookup, and permission checking for agents.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from agent_os.integrations.base import GovernancePolicy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Role(Enum):
|
|
17
|
+
"""Standard roles for agent access control."""
|
|
18
|
+
READER = "reader"
|
|
19
|
+
WRITER = "writer"
|
|
20
|
+
ADMIN = "admin"
|
|
21
|
+
AUDITOR = "auditor"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Action permissions per role
|
|
25
|
+
_ROLE_PERMISSIONS: dict[Role, set[str]] = {
|
|
26
|
+
Role.READER: {"read"},
|
|
27
|
+
Role.WRITER: {"read", "write", "search"},
|
|
28
|
+
Role.ADMIN: {"read", "write", "search", "admin", "delete", "audit"},
|
|
29
|
+
Role.AUDITOR: {"read", "search", "audit"},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Default policy templates per role
|
|
33
|
+
_DEFAULT_POLICIES: dict[Role, GovernancePolicy] = {
|
|
34
|
+
Role.READER: GovernancePolicy(
|
|
35
|
+
max_tool_calls=0,
|
|
36
|
+
allowed_tools=[],
|
|
37
|
+
require_human_approval=True,
|
|
38
|
+
),
|
|
39
|
+
Role.WRITER: GovernancePolicy(
|
|
40
|
+
max_tool_calls=5,
|
|
41
|
+
allowed_tools=["read", "write", "search"],
|
|
42
|
+
require_human_approval=False,
|
|
43
|
+
),
|
|
44
|
+
Role.ADMIN: GovernancePolicy(
|
|
45
|
+
max_tool_calls=50,
|
|
46
|
+
allowed_tools=[],
|
|
47
|
+
max_tokens=16384,
|
|
48
|
+
require_human_approval=False,
|
|
49
|
+
),
|
|
50
|
+
Role.AUDITOR: GovernancePolicy(
|
|
51
|
+
max_tool_calls=5,
|
|
52
|
+
allowed_tools=["read", "search", "audit"],
|
|
53
|
+
log_all_calls=True,
|
|
54
|
+
require_human_approval=False,
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
DEFAULT_ROLE = Role.READER
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RBACManager:
|
|
62
|
+
"""Manages role-based access control for agents.
|
|
63
|
+
|
|
64
|
+
Assigns roles to agents, resolves governance policies per role,
|
|
65
|
+
and checks action permissions. Unknown agents receive the READER role.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
self._roles: dict[str, Role] = {}
|
|
70
|
+
self._custom_policies: dict[Role, GovernancePolicy] = {}
|
|
71
|
+
self._custom_permissions: dict[Role, set[str]] = {}
|
|
72
|
+
|
|
73
|
+
def assign_role(self, agent_id: str, role: Role) -> None:
|
|
74
|
+
"""Assign a role to an agent."""
|
|
75
|
+
self._roles[agent_id] = role
|
|
76
|
+
|
|
77
|
+
def get_role(self, agent_id: str) -> Role:
|
|
78
|
+
"""Return the role for an agent, defaulting to READER."""
|
|
79
|
+
return self._roles.get(agent_id, DEFAULT_ROLE)
|
|
80
|
+
|
|
81
|
+
def get_policy(self, agent_id: str) -> GovernancePolicy:
|
|
82
|
+
"""Return the governance policy template for an agent's role."""
|
|
83
|
+
role = self.get_role(agent_id)
|
|
84
|
+
if role in self._custom_policies:
|
|
85
|
+
return self._custom_policies[role]
|
|
86
|
+
return _DEFAULT_POLICIES[role]
|
|
87
|
+
|
|
88
|
+
def has_permission(self, agent_id: str, action: str) -> bool:
|
|
89
|
+
"""Check whether an agent is permitted to perform an action."""
|
|
90
|
+
role = self.get_role(agent_id)
|
|
91
|
+
perms = self._custom_permissions.get(role, _ROLE_PERMISSIONS.get(role, set()))
|
|
92
|
+
return action in perms
|
|
93
|
+
|
|
94
|
+
def remove_role(self, agent_id: str) -> None:
|
|
95
|
+
"""Remove a role assignment, reverting the agent to the default role."""
|
|
96
|
+
self._roles.pop(agent_id, None)
|
|
97
|
+
|
|
98
|
+
# ── YAML serialisation ────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def to_yaml(self, path: str) -> None:
|
|
101
|
+
"""Save current role assignments and custom definitions to a YAML file."""
|
|
102
|
+
data: dict[str, object] = {
|
|
103
|
+
"assignments": {aid: role.value for aid, role in self._roles.items()},
|
|
104
|
+
}
|
|
105
|
+
if self._custom_policies:
|
|
106
|
+
data["custom_policies"] = {
|
|
107
|
+
role.value: yaml.safe_load(policy.to_yaml())
|
|
108
|
+
for role, policy in self._custom_policies.items()
|
|
109
|
+
}
|
|
110
|
+
if self._custom_permissions:
|
|
111
|
+
data["custom_permissions"] = {
|
|
112
|
+
role.value: sorted(perms)
|
|
113
|
+
for role, perms in self._custom_permissions.items()
|
|
114
|
+
}
|
|
115
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
116
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_yaml(cls, path: str) -> "RBACManager":
|
|
120
|
+
"""Load an RBACManager from a YAML file."""
|
|
121
|
+
with open(path, encoding="utf-8") as f:
|
|
122
|
+
data = yaml.safe_load(f)
|
|
123
|
+
if not isinstance(data, dict):
|
|
124
|
+
raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}")
|
|
125
|
+
|
|
126
|
+
mgr = cls()
|
|
127
|
+
|
|
128
|
+
# Role assignments
|
|
129
|
+
for agent_id, role_value in data.get("assignments", {}).items():
|
|
130
|
+
mgr.assign_role(agent_id, Role(role_value))
|
|
131
|
+
|
|
132
|
+
# Custom policies
|
|
133
|
+
for role_value, policy_dict in data.get("custom_policies", {}).items():
|
|
134
|
+
role = Role(role_value)
|
|
135
|
+
yaml_str = yaml.dump(policy_dict, default_flow_style=False)
|
|
136
|
+
mgr._custom_policies[role] = GovernancePolicy.from_yaml(yaml_str)
|
|
137
|
+
|
|
138
|
+
# Custom permissions
|
|
139
|
+
for role_value, perms_list in data.get("custom_permissions", {}).items():
|
|
140
|
+
role = Role(role_value)
|
|
141
|
+
mgr._custom_permissions[role] = set(perms_list)
|
|
142
|
+
|
|
143
|
+
return mgr
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Adapter Registry with Auto-Discovery
|
|
5
|
+
|
|
6
|
+
Provides a central registry for framework adapters, with support for
|
|
7
|
+
manual registration, decorator-based registration, and automatic
|
|
8
|
+
discovery of BaseIntegration subclasses.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
import inspect
|
|
13
|
+
import logging
|
|
14
|
+
import pkgutil
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
from .base import BaseIntegration
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AdapterRegistry:
|
|
22
|
+
"""Singleton registry for framework adapters."""
|
|
23
|
+
|
|
24
|
+
_instance: "AdapterRegistry | None" = None
|
|
25
|
+
_adapters: dict[str, type[BaseIntegration]]
|
|
26
|
+
|
|
27
|
+
def __new__(cls) -> "AdapterRegistry":
|
|
28
|
+
if cls._instance is None:
|
|
29
|
+
cls._instance = super().__new__(cls)
|
|
30
|
+
cls._instance._adapters = {}
|
|
31
|
+
return cls._instance
|
|
32
|
+
|
|
33
|
+
def register(self, name: str, adapter_class: type[BaseIntegration]) -> None:
|
|
34
|
+
"""Register an adapter class under the given name.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If *name* is already registered.
|
|
38
|
+
TypeError: If *adapter_class* is not a BaseIntegration subclass.
|
|
39
|
+
"""
|
|
40
|
+
if not (isinstance(adapter_class, type) and issubclass(adapter_class, BaseIntegration)):
|
|
41
|
+
raise TypeError(
|
|
42
|
+
f"adapter_class must be a subclass of BaseIntegration, "
|
|
43
|
+
f"got {adapter_class!r}"
|
|
44
|
+
)
|
|
45
|
+
if name in self._adapters:
|
|
46
|
+
raise ValueError(f"Adapter '{name}' is already registered")
|
|
47
|
+
self._adapters[name] = adapter_class
|
|
48
|
+
|
|
49
|
+
def get(self, name: str) -> type[BaseIntegration]:
|
|
50
|
+
"""Return the adapter class registered under *name*.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
KeyError: If no adapter is registered with that name.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
return self._adapters[name]
|
|
57
|
+
except KeyError:
|
|
58
|
+
raise KeyError(f"No adapter registered with name '{name}'") from None
|
|
59
|
+
|
|
60
|
+
def list_adapters(self) -> list[str]:
|
|
61
|
+
"""Return sorted list of registered adapter names."""
|
|
62
|
+
return sorted(self._adapters)
|
|
63
|
+
|
|
64
|
+
def clear(self) -> None:
|
|
65
|
+
"""Remove all registered adapters (useful for testing)."""
|
|
66
|
+
self._adapters.clear()
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def auto_discover(cls) -> "AdapterRegistry":
|
|
70
|
+
"""Scan the integrations package and register all BaseIntegration subclasses.
|
|
71
|
+
|
|
72
|
+
Each subclass is registered under its class name. Returns the
|
|
73
|
+
(singleton) registry instance.
|
|
74
|
+
"""
|
|
75
|
+
registry = cls()
|
|
76
|
+
package = importlib.import_module("agent_os.integrations")
|
|
77
|
+
package_path = package.__path__
|
|
78
|
+
|
|
79
|
+
for _importer, modname, _ispkg in pkgutil.iter_modules(package_path):
|
|
80
|
+
if modname.startswith("_"):
|
|
81
|
+
continue
|
|
82
|
+
full_name = f"agent_os.integrations.{modname}"
|
|
83
|
+
try:
|
|
84
|
+
mod = importlib.import_module(full_name)
|
|
85
|
+
except Exception: # noqa: BLE001 — optional adapter may not be installed
|
|
86
|
+
logger.debug("Failed to import adapter module %s", full_name, exc_info=True)
|
|
87
|
+
continue
|
|
88
|
+
for _attr_name, obj in inspect.getmembers(mod, inspect.isclass):
|
|
89
|
+
if (
|
|
90
|
+
issubclass(obj, BaseIntegration)
|
|
91
|
+
and obj is not BaseIntegration
|
|
92
|
+
and obj.__name__ not in registry._adapters
|
|
93
|
+
):
|
|
94
|
+
registry._adapters[obj.__name__] = obj
|
|
95
|
+
|
|
96
|
+
return registry
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def register_adapter(name: str):
|
|
100
|
+
"""Class decorator that registers an adapter in the global registry.
|
|
101
|
+
|
|
102
|
+
Usage::
|
|
103
|
+
|
|
104
|
+
@register_adapter("my_framework")
|
|
105
|
+
class MyAdapter(BaseIntegration):
|
|
106
|
+
...
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def decorator(cls: type[BaseIntegration]) -> type[BaseIntegration]:
|
|
110
|
+
AdapterRegistry().register(name, cls)
|
|
111
|
+
return cls
|
|
112
|
+
|
|
113
|
+
return decorator
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Scope Guard — prevents agent actions from exceeding configured scope limits.
|
|
4
|
+
|
|
5
|
+
Evaluates file count, line count, and scope drift against per-agent
|
|
6
|
+
configuration to produce PASS / SOFT_FAIL / HARD_FAIL decisions.
|
|
7
|
+
Integrates with the PolicyEngine for governance audit trails.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from agent_os.integrations.scope_guard import ScopeGuard, ScopeConfig
|
|
12
|
+
|
|
13
|
+
config = ScopeConfig(max_files=10, max_lines=500)
|
|
14
|
+
guard = ScopeGuard()
|
|
15
|
+
result = guard.evaluate(
|
|
16
|
+
agent_id="implementer",
|
|
17
|
+
config=config,
|
|
18
|
+
changed_files=["src/main.py", "tests/test_main.py"],
|
|
19
|
+
insertions=120,
|
|
20
|
+
deletions=30,
|
|
21
|
+
)
|
|
22
|
+
print(result.decision) # "PASS"
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import subprocess
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from typing import Any, Optional
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ScopeConfig:
|
|
37
|
+
"""Per-agent scope configuration.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
max_files: Maximum number of files an agent may change.
|
|
41
|
+
max_lines: Maximum total lines (insertions + deletions) allowed.
|
|
42
|
+
mode: Guard mode — ``"on"`` (default) to enforce, ``"off"`` to skip.
|
|
43
|
+
drift_detection: Whether to evaluate drift indicators.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
max_files: int = 10
|
|
47
|
+
max_lines: int = 500
|
|
48
|
+
mode: str = "on"
|
|
49
|
+
drift_detection: bool = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ScopeEvaluation:
|
|
54
|
+
"""Result of a scope guard evaluation.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
decision: One of ``"PASS"``, ``"SOFT_FAIL"``, ``"HARD_FAIL"``.
|
|
58
|
+
files_changed: Number of files changed.
|
|
59
|
+
lines_changed: Total lines changed (insertions + deletions).
|
|
60
|
+
max_files: Configured file limit.
|
|
61
|
+
max_lines: Configured line limit.
|
|
62
|
+
drift_indicators: Drift indicator dicts passed in for audit.
|
|
63
|
+
reason: Human-readable explanation of the decision.
|
|
64
|
+
excess_files: File paths that exceed the file limit.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
decision: str
|
|
68
|
+
files_changed: int
|
|
69
|
+
lines_changed: int
|
|
70
|
+
max_files: int
|
|
71
|
+
max_lines: int
|
|
72
|
+
drift_indicators: list[dict[str, Any]] = field(default_factory=list)
|
|
73
|
+
reason: str = ""
|
|
74
|
+
excess_files: list[str] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _escalate(current: str, proposed: str) -> str:
|
|
78
|
+
"""Return the more severe of two decisions."""
|
|
79
|
+
severity = {"PASS": 0, "SOFT_FAIL": 1, "HARD_FAIL": 2}
|
|
80
|
+
if severity.get(proposed, 0) > severity.get(current, 0):
|
|
81
|
+
return proposed
|
|
82
|
+
return current
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_diff_stats(
|
|
86
|
+
repo_path: str, base_branch: str = "main"
|
|
87
|
+
) -> tuple[list[str], int, int]:
|
|
88
|
+
"""Return ``(changed_files, insertions, deletions)`` via ``git diff --numstat``.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
repo_path: Path to a git repository or worktree.
|
|
92
|
+
base_branch: Branch to diff against.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Tuple of (file paths, total insertions, total deletions).
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["git", "diff", "--numstat", base_branch],
|
|
100
|
+
cwd=repo_path,
|
|
101
|
+
capture_output=True,
|
|
102
|
+
text=True,
|
|
103
|
+
timeout=30,
|
|
104
|
+
)
|
|
105
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
|
|
106
|
+
logger.warning("_get_diff_stats failed: %s", exc)
|
|
107
|
+
return [], 0, 0
|
|
108
|
+
|
|
109
|
+
files: list[str] = []
|
|
110
|
+
insertions = 0
|
|
111
|
+
deletions = 0
|
|
112
|
+
for line in result.stdout.strip().splitlines():
|
|
113
|
+
parts = line.split("\t")
|
|
114
|
+
if len(parts) == 3:
|
|
115
|
+
ins = int(parts[0]) if parts[0] != "-" else 0
|
|
116
|
+
dels = int(parts[1]) if parts[1] != "-" else 0
|
|
117
|
+
insertions += ins
|
|
118
|
+
deletions += dels
|
|
119
|
+
files.append(parts[2])
|
|
120
|
+
return files, insertions, deletions
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ScopeGuard:
|
|
124
|
+
"""Evaluates whether agent changes are within configured scope limits.
|
|
125
|
+
|
|
126
|
+
Optionally records governance audit events through a *policy_engine*.
|
|
127
|
+
The policy engine, when provided, must expose a
|
|
128
|
+
``record_event(event: dict)`` method.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
policy_engine: Optional governance policy engine for audit trails.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, policy_engine: Optional[Any] = None) -> None:
|
|
135
|
+
self._policy_engine = policy_engine
|
|
136
|
+
|
|
137
|
+
# ── public API ──────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
def evaluate(
|
|
140
|
+
self,
|
|
141
|
+
agent_id: str,
|
|
142
|
+
config: ScopeConfig,
|
|
143
|
+
changed_files: list[str],
|
|
144
|
+
insertions: int,
|
|
145
|
+
deletions: int,
|
|
146
|
+
drift_indicators: Optional[list[dict[str, Any]]] = None,
|
|
147
|
+
) -> ScopeEvaluation:
|
|
148
|
+
"""Evaluate whether an agent's changes are within scope.
|
|
149
|
+
|
|
150
|
+
Decision logic:
|
|
151
|
+
1. ``config.mode == "off"`` → always ``PASS``.
|
|
152
|
+
2. files > ``max_files × 2`` **or** lines > ``max_lines × 2`` → ``HARD_FAIL``.
|
|
153
|
+
3. files > ``max_files`` **or** lines > ``max_lines`` → ``SOFT_FAIL``.
|
|
154
|
+
4. Any drift indicator with ``severity == "warning"`` → ``SOFT_FAIL``.
|
|
155
|
+
5. Otherwise → ``PASS``.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
agent_id: Unique agent identifier.
|
|
159
|
+
config: Scope configuration for this agent.
|
|
160
|
+
changed_files: List of changed file paths.
|
|
161
|
+
insertions: Total lines added.
|
|
162
|
+
deletions: Total lines removed.
|
|
163
|
+
drift_indicators: Optional list of drift indicator dicts. Each
|
|
164
|
+
dict may contain ``severity`` (``"info"`` | ``"warning"``).
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
A :class:`ScopeEvaluation` with the decision and supporting data.
|
|
168
|
+
"""
|
|
169
|
+
max_files = config.max_files
|
|
170
|
+
max_lines = config.max_lines
|
|
171
|
+
files_changed = len(changed_files)
|
|
172
|
+
lines_changed = insertions + deletions
|
|
173
|
+
drift_indicators = drift_indicators or []
|
|
174
|
+
|
|
175
|
+
# Mode "off" → always pass
|
|
176
|
+
if config.mode == "off":
|
|
177
|
+
evaluation = ScopeEvaluation(
|
|
178
|
+
decision="PASS",
|
|
179
|
+
files_changed=files_changed,
|
|
180
|
+
lines_changed=lines_changed,
|
|
181
|
+
max_files=max_files,
|
|
182
|
+
max_lines=max_lines,
|
|
183
|
+
reason="Scope guard disabled (mode=off)",
|
|
184
|
+
)
|
|
185
|
+
self._record(agent_id, evaluation)
|
|
186
|
+
return evaluation
|
|
187
|
+
|
|
188
|
+
reasons: list[str] = []
|
|
189
|
+
decision = "PASS"
|
|
190
|
+
|
|
191
|
+
# Check file count
|
|
192
|
+
if max_files > 0 and files_changed > max_files:
|
|
193
|
+
if files_changed > max_files * 2:
|
|
194
|
+
decision = "HARD_FAIL"
|
|
195
|
+
reasons.append(
|
|
196
|
+
f"files changed ({files_changed}) exceeds 2× limit "
|
|
197
|
+
f"({max_files * 2})"
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
decision = _escalate(decision, "SOFT_FAIL")
|
|
201
|
+
reasons.append(
|
|
202
|
+
f"files changed ({files_changed}) exceeds limit ({max_files})"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Check line count
|
|
206
|
+
if max_lines > 0 and lines_changed > max_lines:
|
|
207
|
+
if lines_changed > max_lines * 2:
|
|
208
|
+
decision = "HARD_FAIL"
|
|
209
|
+
reasons.append(
|
|
210
|
+
f"lines changed ({lines_changed}) exceeds 2× limit "
|
|
211
|
+
f"({max_lines * 2})"
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
decision = _escalate(decision, "SOFT_FAIL")
|
|
215
|
+
reasons.append(
|
|
216
|
+
f"lines changed ({lines_changed}) exceeds limit ({max_lines})"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Check drift indicators
|
|
220
|
+
if config.drift_detection and drift_indicators:
|
|
221
|
+
warnings = [
|
|
222
|
+
d for d in drift_indicators if d.get("severity") == "warning"
|
|
223
|
+
]
|
|
224
|
+
if warnings:
|
|
225
|
+
decision = _escalate(decision, "SOFT_FAIL")
|
|
226
|
+
reasons.append(
|
|
227
|
+
f"{len(warnings)} scope drift warning(s) detected"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Excess files for downstream remediation
|
|
231
|
+
excess_files: list[str] = []
|
|
232
|
+
if max_files > 0 and files_changed > max_files:
|
|
233
|
+
excess_files = changed_files[max_files:]
|
|
234
|
+
|
|
235
|
+
reason = "; ".join(reasons) if reasons else "All scope checks passed"
|
|
236
|
+
|
|
237
|
+
evaluation = ScopeEvaluation(
|
|
238
|
+
decision=decision,
|
|
239
|
+
files_changed=files_changed,
|
|
240
|
+
lines_changed=lines_changed,
|
|
241
|
+
max_files=max_files,
|
|
242
|
+
max_lines=max_lines,
|
|
243
|
+
drift_indicators=drift_indicators,
|
|
244
|
+
reason=reason,
|
|
245
|
+
excess_files=excess_files,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self._record(agent_id, evaluation)
|
|
249
|
+
return evaluation
|
|
250
|
+
|
|
251
|
+
def evaluate_from_git(
|
|
252
|
+
self,
|
|
253
|
+
agent_id: str,
|
|
254
|
+
config: ScopeConfig,
|
|
255
|
+
repo_path: str,
|
|
256
|
+
base_branch: str = "main",
|
|
257
|
+
drift_indicators: Optional[list[dict[str, Any]]] = None,
|
|
258
|
+
) -> ScopeEvaluation:
|
|
259
|
+
"""Convenience wrapper that reads diff stats from *repo_path*.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
agent_id: Unique agent identifier.
|
|
263
|
+
config: Scope configuration.
|
|
264
|
+
repo_path: Path to the git repository.
|
|
265
|
+
base_branch: Branch to diff against (default ``"main"``).
|
|
266
|
+
drift_indicators: Optional drift indicator dicts.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
A :class:`ScopeEvaluation`.
|
|
270
|
+
"""
|
|
271
|
+
changed_files, insertions, deletions = _get_diff_stats(
|
|
272
|
+
repo_path, base_branch
|
|
273
|
+
)
|
|
274
|
+
return self.evaluate(
|
|
275
|
+
agent_id=agent_id,
|
|
276
|
+
config=config,
|
|
277
|
+
changed_files=changed_files,
|
|
278
|
+
insertions=insertions,
|
|
279
|
+
deletions=deletions,
|
|
280
|
+
drift_indicators=drift_indicators,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# ── internal helpers ────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
def _record(self, agent_id: str, evaluation: ScopeEvaluation) -> None:
|
|
286
|
+
"""Record the evaluation as an audit event if a policy engine is set."""
|
|
287
|
+
if self._policy_engine is None:
|
|
288
|
+
return
|
|
289
|
+
try:
|
|
290
|
+
self._policy_engine.record_event(
|
|
291
|
+
{
|
|
292
|
+
"type": "scope_evaluation",
|
|
293
|
+
"agent_id": agent_id,
|
|
294
|
+
"decision": evaluation.decision,
|
|
295
|
+
"files_changed": evaluation.files_changed,
|
|
296
|
+
"max_files": evaluation.max_files,
|
|
297
|
+
"lines_changed": evaluation.lines_changed,
|
|
298
|
+
"max_lines": evaluation.max_lines,
|
|
299
|
+
"reason": evaluation.reason,
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
except Exception: # pragma: no cover — best-effort audit
|
|
303
|
+
logger.exception("Failed to record scope evaluation event")
|