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
amb_core/schema.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Schema validation for AMB messages.
|
|
4
|
+
|
|
5
|
+
This module provides schema registry and validation capabilities
|
|
6
|
+
to ensure message payloads conform to expected schemas.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Any, Dict, Optional, Type, Union, Callable
|
|
11
|
+
from pydantic import BaseModel, ValidationError
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SchemaValidationError(Exception):
|
|
16
|
+
"""Raised when message schema validation fails."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, topic: str, errors: list, payload: Dict[str, Any]):
|
|
19
|
+
self.topic = topic
|
|
20
|
+
self.errors = errors
|
|
21
|
+
self.payload = payload
|
|
22
|
+
super().__init__(f"Schema validation failed for topic '{topic}': {errors}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Schema(ABC):
|
|
26
|
+
"""Abstract base class for message schemas."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def validate(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Validate a payload against the schema.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
payload: The payload to validate
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The validated payload (potentially coerced/normalized)
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
SchemaValidationError: If validation fails
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PydanticSchema(Schema):
|
|
46
|
+
"""Schema backed by a Pydantic model."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, model: Type[BaseModel]):
|
|
49
|
+
"""
|
|
50
|
+
Initialize with a Pydantic model.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
model: Pydantic model class to use for validation
|
|
54
|
+
"""
|
|
55
|
+
self._model = model
|
|
56
|
+
|
|
57
|
+
def validate(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
58
|
+
"""Validate payload using Pydantic model."""
|
|
59
|
+
try:
|
|
60
|
+
validated = self._model.model_validate(payload)
|
|
61
|
+
return validated.model_dump()
|
|
62
|
+
except ValidationError as e:
|
|
63
|
+
raise SchemaValidationError(
|
|
64
|
+
topic="unknown",
|
|
65
|
+
errors=e.errors(),
|
|
66
|
+
payload=payload
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DictSchema(Schema):
|
|
71
|
+
"""Schema based on a dictionary specification with type checking."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, spec: Dict[str, type], strict: bool = False):
|
|
74
|
+
"""
|
|
75
|
+
Initialize with a dictionary specification.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
spec: Dict mapping field names to expected types
|
|
79
|
+
strict: If True, reject payloads with extra fields
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
schema = DictSchema({
|
|
83
|
+
"user_id": str,
|
|
84
|
+
"amount": float,
|
|
85
|
+
"timestamp": str
|
|
86
|
+
})
|
|
87
|
+
"""
|
|
88
|
+
self._spec = spec
|
|
89
|
+
self._strict = strict
|
|
90
|
+
|
|
91
|
+
def validate(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
92
|
+
"""Validate payload against dictionary specification."""
|
|
93
|
+
errors = []
|
|
94
|
+
|
|
95
|
+
# Check required fields
|
|
96
|
+
for field, expected_type in self._spec.items():
|
|
97
|
+
if field not in payload:
|
|
98
|
+
errors.append({
|
|
99
|
+
"type": "missing",
|
|
100
|
+
"loc": [field],
|
|
101
|
+
"msg": f"Field '{field}' is required"
|
|
102
|
+
})
|
|
103
|
+
elif not isinstance(payload[field], expected_type):
|
|
104
|
+
errors.append({
|
|
105
|
+
"type": "type_error",
|
|
106
|
+
"loc": [field],
|
|
107
|
+
"msg": f"Expected {expected_type.__name__}, got {type(payload[field]).__name__}"
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# Check for extra fields if strict
|
|
111
|
+
if self._strict:
|
|
112
|
+
extra_fields = set(payload.keys()) - set(self._spec.keys())
|
|
113
|
+
for field in extra_fields:
|
|
114
|
+
errors.append({
|
|
115
|
+
"type": "extra_forbidden",
|
|
116
|
+
"loc": [field],
|
|
117
|
+
"msg": f"Extra field '{field}' not allowed"
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if errors:
|
|
121
|
+
raise SchemaValidationError(
|
|
122
|
+
topic="unknown",
|
|
123
|
+
errors=errors,
|
|
124
|
+
payload=payload
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return payload
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CallableSchema(Schema):
|
|
131
|
+
"""Schema using a custom validation function."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, validator: Callable[[Dict[str, Any]], Dict[str, Any]]):
|
|
134
|
+
"""
|
|
135
|
+
Initialize with a validation function.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
validator: Function that takes payload and returns validated payload,
|
|
139
|
+
or raises an exception on validation failure
|
|
140
|
+
"""
|
|
141
|
+
self._validator = validator
|
|
142
|
+
|
|
143
|
+
def validate(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
144
|
+
"""Validate using custom function."""
|
|
145
|
+
try:
|
|
146
|
+
return self._validator(payload)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
raise SchemaValidationError(
|
|
149
|
+
topic="unknown",
|
|
150
|
+
errors=[{"type": "validation_error", "msg": str(e)}],
|
|
151
|
+
payload=payload
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SchemaRegistry:
|
|
156
|
+
"""
|
|
157
|
+
Registry for message schemas.
|
|
158
|
+
|
|
159
|
+
Provides centralized schema management for topic validation.
|
|
160
|
+
Supports multiple schema types: Pydantic models, dict specs, and custom validators.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
from pydantic import BaseModel
|
|
164
|
+
|
|
165
|
+
class FraudAlertPayload(BaseModel):
|
|
166
|
+
transaction_id: str
|
|
167
|
+
amount: float
|
|
168
|
+
risk_score: float
|
|
169
|
+
|
|
170
|
+
registry = SchemaRegistry()
|
|
171
|
+
registry.register("fraud.alerts", FraudAlertPayload)
|
|
172
|
+
|
|
173
|
+
# Or with dict schema
|
|
174
|
+
registry.register("user.events", {
|
|
175
|
+
"user_id": str,
|
|
176
|
+
"event_type": str
|
|
177
|
+
})
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(self, strict: bool = True):
|
|
181
|
+
"""
|
|
182
|
+
Initialize the schema registry.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
strict: If True, require schema for all topics when validating
|
|
186
|
+
"""
|
|
187
|
+
self._schemas: Dict[str, Schema] = {}
|
|
188
|
+
self._strict = strict
|
|
189
|
+
|
|
190
|
+
def register(
|
|
191
|
+
self,
|
|
192
|
+
topic: str,
|
|
193
|
+
schema: Union[Type[BaseModel], Dict[str, type], Schema, Callable]
|
|
194
|
+
) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Register a schema for a topic.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
topic: Topic pattern to register schema for
|
|
200
|
+
schema: Schema specification (Pydantic model, dict, Schema instance, or callable)
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
# Pydantic model
|
|
204
|
+
registry.register("fraud.alerts", FraudAlertSchema)
|
|
205
|
+
|
|
206
|
+
# Dict specification
|
|
207
|
+
registry.register("user.events", {"user_id": str, "event": str})
|
|
208
|
+
|
|
209
|
+
# Custom Schema instance
|
|
210
|
+
registry.register("custom.topic", MyCustomSchema())
|
|
211
|
+
|
|
212
|
+
# Callable validator
|
|
213
|
+
registry.register("validated.topic", lambda p: p if p.get("valid") else raise_error())
|
|
214
|
+
"""
|
|
215
|
+
if isinstance(schema, type) and issubclass(schema, BaseModel):
|
|
216
|
+
self._schemas[topic] = PydanticSchema(schema)
|
|
217
|
+
elif isinstance(schema, dict):
|
|
218
|
+
self._schemas[topic] = DictSchema(schema)
|
|
219
|
+
elif isinstance(schema, Schema):
|
|
220
|
+
self._schemas[topic] = schema
|
|
221
|
+
elif callable(schema):
|
|
222
|
+
self._schemas[topic] = CallableSchema(schema)
|
|
223
|
+
else:
|
|
224
|
+
raise TypeError(
|
|
225
|
+
f"Schema must be a Pydantic model, dict, Schema instance, or callable. "
|
|
226
|
+
f"Got {type(schema)}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def unregister(self, topic: str) -> bool:
|
|
230
|
+
"""
|
|
231
|
+
Unregister a schema for a topic.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
topic: Topic to unregister
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
True if schema was removed, False if topic wasn't registered
|
|
238
|
+
"""
|
|
239
|
+
if topic in self._schemas:
|
|
240
|
+
del self._schemas[topic]
|
|
241
|
+
return True
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
def has_schema(self, topic: str) -> bool:
|
|
245
|
+
"""Check if a topic has a registered schema."""
|
|
246
|
+
return topic in self._schemas
|
|
247
|
+
|
|
248
|
+
def get_schema(self, topic: str) -> Optional[Schema]:
|
|
249
|
+
"""Get the schema for a topic."""
|
|
250
|
+
return self._schemas.get(topic)
|
|
251
|
+
|
|
252
|
+
def validate(self, topic: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
253
|
+
"""
|
|
254
|
+
Validate a payload for a given topic.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
topic: Topic the message is for
|
|
258
|
+
payload: Message payload to validate
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Validated payload (potentially normalized)
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
SchemaValidationError: If validation fails
|
|
265
|
+
ValueError: If strict mode and no schema registered for topic
|
|
266
|
+
"""
|
|
267
|
+
schema = self._schemas.get(topic)
|
|
268
|
+
|
|
269
|
+
if schema is None:
|
|
270
|
+
if self._strict:
|
|
271
|
+
raise ValueError(f"No schema registered for topic '{topic}'")
|
|
272
|
+
return payload
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
return schema.validate(payload)
|
|
276
|
+
except SchemaValidationError as e:
|
|
277
|
+
# Update error with correct topic
|
|
278
|
+
raise SchemaValidationError(
|
|
279
|
+
topic=topic,
|
|
280
|
+
errors=e.errors,
|
|
281
|
+
payload=e.payload
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def list_topics(self) -> list:
|
|
285
|
+
"""Get list of topics with registered schemas."""
|
|
286
|
+
return list(self._schemas.keys())
|
|
287
|
+
|
|
288
|
+
def __len__(self) -> int:
|
|
289
|
+
"""Get number of registered schemas."""
|
|
290
|
+
return len(self._schemas)
|
|
291
|
+
|
|
292
|
+
def __contains__(self, topic: str) -> bool:
|
|
293
|
+
"""Check if topic has a schema registered."""
|
|
294
|
+
return topic in self._schemas
|
amb_core/tracing.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Distributed tracing for AMB.
|
|
4
|
+
|
|
5
|
+
This module provides distributed tracing capabilities for tracking
|
|
6
|
+
message flow across agents and services.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional, List
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
from contextvars import ContextVar
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Context variable for current trace
|
|
18
|
+
_current_trace: ContextVar[Optional["TraceContext"]] = ContextVar(
|
|
19
|
+
"current_trace", default=None
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TraceSpan:
|
|
25
|
+
"""
|
|
26
|
+
A span within a trace representing a single operation.
|
|
27
|
+
"""
|
|
28
|
+
span_id: str
|
|
29
|
+
operation_name: str
|
|
30
|
+
trace_id: str
|
|
31
|
+
parent_span_id: Optional[str] = None
|
|
32
|
+
start_time: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
33
|
+
end_time: Optional[datetime] = None
|
|
34
|
+
tags: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
logs: List[Dict[str, Any]] = field(default_factory=list)
|
|
36
|
+
status: str = "in_progress" # in_progress, success, error
|
|
37
|
+
|
|
38
|
+
def finish(self, status: str = "success") -> None:
|
|
39
|
+
"""Mark span as finished."""
|
|
40
|
+
self.end_time = datetime.now(timezone.utc)
|
|
41
|
+
self.status = status
|
|
42
|
+
|
|
43
|
+
def log(self, event: str, **kwargs) -> None:
|
|
44
|
+
"""Add a log entry to the span."""
|
|
45
|
+
self.logs.append({
|
|
46
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
47
|
+
"event": event,
|
|
48
|
+
**kwargs
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
def set_tag(self, key: str, value: Any) -> None:
|
|
52
|
+
"""Set a tag on the span."""
|
|
53
|
+
self.tags[key] = value
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def duration_ms(self) -> Optional[float]:
|
|
57
|
+
"""Get duration in milliseconds."""
|
|
58
|
+
if not self.end_time:
|
|
59
|
+
return None
|
|
60
|
+
delta = self.end_time - self.start_time
|
|
61
|
+
return delta.total_seconds() * 1000
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
64
|
+
"""Convert to dictionary."""
|
|
65
|
+
return {
|
|
66
|
+
"span_id": self.span_id,
|
|
67
|
+
"operation_name": self.operation_name,
|
|
68
|
+
"trace_id": self.trace_id,
|
|
69
|
+
"parent_span_id": self.parent_span_id,
|
|
70
|
+
"start_time": self.start_time.isoformat(),
|
|
71
|
+
"end_time": self.end_time.isoformat() if self.end_time else None,
|
|
72
|
+
"duration_ms": self.duration_ms,
|
|
73
|
+
"tags": self.tags,
|
|
74
|
+
"logs": self.logs,
|
|
75
|
+
"status": self.status
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class TraceContext:
|
|
81
|
+
"""
|
|
82
|
+
Context for distributed tracing across message flows.
|
|
83
|
+
|
|
84
|
+
TraceContext maintains trace and span IDs that propagate with messages,
|
|
85
|
+
allowing you to track the full journey of a message through the system.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
# Start a new trace
|
|
89
|
+
with TraceContext.start("process_order") as ctx:
|
|
90
|
+
await bus.publish("orders.new", payload, trace_id=ctx.trace_id)
|
|
91
|
+
|
|
92
|
+
# Continue an existing trace
|
|
93
|
+
with TraceContext.from_message(message) as ctx:
|
|
94
|
+
ctx.log("Processing started")
|
|
95
|
+
# ... process message
|
|
96
|
+
ctx.log("Processing complete")
|
|
97
|
+
"""
|
|
98
|
+
trace_id: str
|
|
99
|
+
span_id: str
|
|
100
|
+
parent_span_id: Optional[str] = None
|
|
101
|
+
baggage: Dict[str, str] = field(default_factory=dict)
|
|
102
|
+
spans: List[TraceSpan] = field(default_factory=list)
|
|
103
|
+
_current_span: Optional[TraceSpan] = field(default=None, repr=False)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def new(cls, operation_name: str = "root") -> "TraceContext":
|
|
107
|
+
"""
|
|
108
|
+
Create a new trace context.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
operation_name: Name of the root operation
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
New TraceContext
|
|
115
|
+
"""
|
|
116
|
+
trace_id = str(uuid4())
|
|
117
|
+
span_id = str(uuid4())
|
|
118
|
+
|
|
119
|
+
ctx = cls(
|
|
120
|
+
trace_id=trace_id,
|
|
121
|
+
span_id=span_id,
|
|
122
|
+
parent_span_id=None
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Create root span
|
|
126
|
+
root_span = TraceSpan(
|
|
127
|
+
span_id=span_id,
|
|
128
|
+
operation_name=operation_name,
|
|
129
|
+
trace_id=trace_id
|
|
130
|
+
)
|
|
131
|
+
ctx.spans.append(root_span)
|
|
132
|
+
ctx._current_span = root_span
|
|
133
|
+
|
|
134
|
+
return ctx
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_headers(cls, headers: Dict[str, str]) -> "TraceContext":
|
|
138
|
+
"""
|
|
139
|
+
Create trace context from message headers.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
headers: Headers containing trace information
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
TraceContext (new or continued)
|
|
146
|
+
"""
|
|
147
|
+
trace_id = headers.get("x-trace-id")
|
|
148
|
+
parent_span_id = headers.get("x-span-id")
|
|
149
|
+
baggage_json = headers.get("x-trace-baggage", "{}")
|
|
150
|
+
|
|
151
|
+
if not trace_id:
|
|
152
|
+
return cls.new()
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
baggage = json.loads(baggage_json)
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
baggage = {}
|
|
158
|
+
|
|
159
|
+
span_id = str(uuid4())
|
|
160
|
+
|
|
161
|
+
return cls(
|
|
162
|
+
trace_id=trace_id,
|
|
163
|
+
span_id=span_id,
|
|
164
|
+
parent_span_id=parent_span_id,
|
|
165
|
+
baggage=baggage
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_message(cls, message: "Message") -> "TraceContext": # noqa: F821
|
|
170
|
+
"""
|
|
171
|
+
Extract trace context from a message.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
message: Message with trace metadata
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
TraceContext (new or continued)
|
|
178
|
+
"""
|
|
179
|
+
trace_id = message.metadata.get("trace_id")
|
|
180
|
+
parent_span_id = message.metadata.get("span_id")
|
|
181
|
+
baggage = message.metadata.get("trace_baggage", {})
|
|
182
|
+
|
|
183
|
+
if not trace_id:
|
|
184
|
+
return cls.new()
|
|
185
|
+
|
|
186
|
+
span_id = str(uuid4())
|
|
187
|
+
|
|
188
|
+
return cls(
|
|
189
|
+
trace_id=trace_id,
|
|
190
|
+
span_id=span_id,
|
|
191
|
+
parent_span_id=parent_span_id,
|
|
192
|
+
baggage=baggage if isinstance(baggage, dict) else {}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def current(cls) -> Optional["TraceContext"]:
|
|
197
|
+
"""Get current trace context from context var."""
|
|
198
|
+
return _current_trace.get()
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def start(cls, operation_name: str = "root") -> "TraceContext":
|
|
202
|
+
"""
|
|
203
|
+
Start a new trace and set as current.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
operation_name: Name of the root operation
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
New TraceContext
|
|
210
|
+
"""
|
|
211
|
+
ctx = cls.new(operation_name)
|
|
212
|
+
_current_trace.set(ctx)
|
|
213
|
+
return ctx
|
|
214
|
+
|
|
215
|
+
def start_span(self, operation_name: str) -> TraceSpan:
|
|
216
|
+
"""
|
|
217
|
+
Start a new child span.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
operation_name: Name of the operation
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
New TraceSpan
|
|
224
|
+
"""
|
|
225
|
+
parent_id = self._current_span.span_id if self._current_span else self.span_id
|
|
226
|
+
|
|
227
|
+
span = TraceSpan(
|
|
228
|
+
span_id=str(uuid4()),
|
|
229
|
+
operation_name=operation_name,
|
|
230
|
+
trace_id=self.trace_id,
|
|
231
|
+
parent_span_id=parent_id
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.spans.append(span)
|
|
235
|
+
self._current_span = span
|
|
236
|
+
return span
|
|
237
|
+
|
|
238
|
+
def finish_span(self, status: str = "success") -> None:
|
|
239
|
+
"""Finish the current span."""
|
|
240
|
+
if self._current_span:
|
|
241
|
+
self._current_span.finish(status)
|
|
242
|
+
|
|
243
|
+
# Find parent span
|
|
244
|
+
if self._current_span.parent_span_id:
|
|
245
|
+
for span in self.spans:
|
|
246
|
+
if span.span_id == self._current_span.parent_span_id:
|
|
247
|
+
self._current_span = span
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
self._current_span = None
|
|
251
|
+
|
|
252
|
+
def log(self, event: str, **kwargs) -> None:
|
|
253
|
+
"""Log an event to the current span."""
|
|
254
|
+
if self._current_span:
|
|
255
|
+
self._current_span.log(event, **kwargs)
|
|
256
|
+
|
|
257
|
+
def set_tag(self, key: str, value: Any) -> None:
|
|
258
|
+
"""Set a tag on the current span."""
|
|
259
|
+
if self._current_span:
|
|
260
|
+
self._current_span.set_tag(key, value)
|
|
261
|
+
|
|
262
|
+
def set_baggage(self, key: str, value: str) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Set baggage item that propagates with the trace.
|
|
265
|
+
|
|
266
|
+
Baggage items are key-value pairs that travel with the trace
|
|
267
|
+
across all services.
|
|
268
|
+
"""
|
|
269
|
+
self.baggage[key] = value
|
|
270
|
+
|
|
271
|
+
def get_baggage(self, key: str) -> Optional[str]:
|
|
272
|
+
"""Get a baggage item."""
|
|
273
|
+
return self.baggage.get(key)
|
|
274
|
+
|
|
275
|
+
def to_headers(self) -> Dict[str, str]:
|
|
276
|
+
"""
|
|
277
|
+
Convert trace context to headers for propagation.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Headers dict
|
|
281
|
+
"""
|
|
282
|
+
return {
|
|
283
|
+
"x-trace-id": self.trace_id,
|
|
284
|
+
"x-span-id": self.span_id,
|
|
285
|
+
"x-parent-span-id": self.parent_span_id or "",
|
|
286
|
+
"x-trace-baggage": json.dumps(self.baggage)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
def to_message_metadata(self) -> Dict[str, Any]:
|
|
290
|
+
"""
|
|
291
|
+
Convert trace context to message metadata.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Metadata dict to add to message
|
|
295
|
+
"""
|
|
296
|
+
return {
|
|
297
|
+
"trace_id": self.trace_id,
|
|
298
|
+
"span_id": self.span_id,
|
|
299
|
+
"parent_span_id": self.parent_span_id,
|
|
300
|
+
"trace_baggage": self.baggage
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
def __enter__(self) -> "TraceContext":
|
|
304
|
+
"""Context manager entry."""
|
|
305
|
+
_current_trace.set(self)
|
|
306
|
+
return self
|
|
307
|
+
|
|
308
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
309
|
+
"""Context manager exit."""
|
|
310
|
+
if exc_type:
|
|
311
|
+
self.finish_span(status="error")
|
|
312
|
+
else:
|
|
313
|
+
self.finish_span(status="success")
|
|
314
|
+
_current_trace.set(None)
|
|
315
|
+
|
|
316
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
317
|
+
"""Convert to dictionary."""
|
|
318
|
+
return {
|
|
319
|
+
"trace_id": self.trace_id,
|
|
320
|
+
"span_id": self.span_id,
|
|
321
|
+
"parent_span_id": self.parent_span_id,
|
|
322
|
+
"baggage": self.baggage,
|
|
323
|
+
"spans": [s.to_dict() for s in self.spans]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def get_current_trace() -> Optional[TraceContext]:
|
|
328
|
+
"""Get the current trace context."""
|
|
329
|
+
return _current_trace.get()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def inject_trace(message: "Message") -> "Message": # noqa: F821
|
|
333
|
+
"""
|
|
334
|
+
Inject current trace context into a message.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
message: Message to inject trace into
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Message with trace metadata
|
|
341
|
+
"""
|
|
342
|
+
ctx = get_current_trace()
|
|
343
|
+
if ctx:
|
|
344
|
+
message.metadata.update(ctx.to_message_metadata())
|
|
345
|
+
return message
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def extract_trace(message: "Message") -> TraceContext: # noqa: F821
|
|
349
|
+
"""
|
|
350
|
+
Extract trace context from a message.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
message: Message to extract trace from
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
TraceContext (new or continued)
|
|
357
|
+
"""
|
|
358
|
+
return TraceContext.from_message(message)
|