Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/omnibase_core/decorators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
Provides decorators to mark legitimate exceptions to ONEX strict typing standards.
"""

from .allow_dict_any import allow_dict_any
from .error_handling import (
io_error_handling,
standard_error_handling,
validation_error_handling,
)
from .pattern_exclusions import (
allow_any_type,
allow_dict_any,
allow_dict_str_any,
allow_legacy_pattern,
allow_mixed_types,
Expand Down
73 changes: 73 additions & 0 deletions src/omnibase_core/decorators/allow_dict_any.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Allow dict[str, Any] Decorator

Provides a decorator to mark functions/methods that legitimately use dict[str, Any]
for ONEX validation purposes. This is a specialized version that works with both
simple pass-through usage and with reason arguments.

Usage:
# Simple usage (no arguments)
@allow_dict_any
def serialize(self) -> dict[str, Any]:
return self.model_dump()

# With reason (for documentation)
@allow_dict_any(reason="Serialization method for Pydantic compatibility")
def to_dict(self) -> dict[str, Any]:
return {...}
"""

from collections.abc import Callable
from typing import Any


def allow_dict_any(
func: Callable[..., Any] | None = None,
*,
reason: str | None = None,
) -> Callable[..., Any]:
"""
Decorator to allow dict[str, Any] usage in specific functions.

This decorator is recognized by ONEX validation scripts and should only be used when:
1. Serialization methods that must return dict[str, Any] for Pydantic compatibility
2. Validator methods that accept raw untyped data before conversion
3. Legacy integration where gradual typing is being applied

Args:
func: The function to decorate (when used without arguments)
reason: Optional justification for using dict[str, Any] (when used with arguments)

Returns:
The decorated function with metadata attached

Examples:
# Simple usage without arguments
@allow_dict_any
def serialize(self) -> dict[str, Any]:
return self.model_dump()

# Usage with reason
@allow_dict_any(reason="Serialization method for Pydantic compatibility")
def to_dict(self) -> dict[str, Any]:
return {"key": "value"}
"""

def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
"""Apply the decorator to a function."""
# Mark the function with metadata for validation scripts
f._allow_dict_any = True # type: ignore[attr-defined]
if reason:
f._dict_any_reason = reason # type: ignore[attr-defined]
return f

# Handle both @allow_dict_any and @allow_dict_any(reason="...")
if func is not None:
# Called as @allow_dict_any (without parentheses)
return decorator(func)
else:
# Called as @allow_dict_any(...) (with parentheses and arguments)
return decorator


__all__ = ["allow_dict_any"]
43 changes: 31 additions & 12 deletions src/omnibase_core/mixins/mixin_service_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,19 +186,30 @@ def _send_discovery_request(self) -> None:
if not isinstance(source_id, UUID):
source_id = uuid4()

# Create event envelope
# Create event envelope with typed metadata
from omnibase_core.models.core.model_envelope_metadata import (
ModelEnvelopeMetadata,
)

domain_filter_value = getattr(self, "domain_filter", None)
metadata = ModelEnvelopeMetadata(
tags={
"request_type": "tool_discovery",
"domain_filter": str(domain_filter_value)
if domain_filter_value is not None
else "",
}
)

envelope: ModelEventEnvelope[ModelOnexEvent] = (
ModelEventEnvelope.create_broadcast(
payload=discovery_event,
source_node_id=source_id,
correlation_id=correlation_id,
)
)
# Set metadata after creation
envelope.metadata = {
"request_type": "tool_discovery",
"domain_filter": getattr(self, "domain_filter", None),
}
# Update metadata using with_metadata()
envelope = envelope.with_metadata(metadata)

logger.info(f"🔍 Sending discovery request envelope: {envelope}")
self.event_bus.publish(envelope)
Expand Down Expand Up @@ -341,7 +352,18 @@ def _send_introspection_request(self, node_id: UUID | str) -> None:
# Ensure target_node_id is UUID
target_id = node_id if isinstance(node_id, UUID) else UUID(node_id)

# Create event envelope
# Create event envelope with typed metadata
from omnibase_core.models.core.model_envelope_metadata import (
ModelEnvelopeMetadata,
)

metadata = ModelEnvelopeMetadata(
tags={
"target_node_id": str(node_id),
"request_type": "introspection",
}
)

envelope: ModelEventEnvelope[ModelOnexEvent] = (
ModelEventEnvelope.create_directed(
payload=introspection_event,
Expand All @@ -350,11 +372,8 @@ def _send_introspection_request(self, node_id: UUID | str) -> None:
correlation_id=correlation_id,
)
)
# Set metadata after creation
envelope.metadata = {
"target_node_id": str(node_id),
"request_type": "introspection",
}
# Update metadata using with_metadata()
envelope = envelope.with_metadata(metadata)

self.event_bus.publish(envelope)

Expand Down
15 changes: 14 additions & 1 deletion src/omnibase_core/models/common/model_reducer_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ class ModelReducerMetadata(BaseModel):
# attributes even when class identity differs (e.g., in pytest-xdist
# parallel execution where model classes are imported in separate workers).
# See CLAUDE.md section "Pydantic from_attributes=True for Value Objects".
model_config = ConfigDict(extra="forbid", from_attributes=True)
#
# extra="allow" permits arbitrary context data (e.g., user_id, request_id)
# to be passed through metadata while still providing typed fields for
# common metadata patterns (trace_id, correlation_id, etc.).
model_config = ConfigDict(extra="allow", from_attributes=True)

source: str | None = Field(
default=None,
Expand Down Expand Up @@ -59,6 +63,15 @@ class ModelReducerMetadata(BaseModel):
default_factory=list,
description="Tags for categorization",
)
trigger: str | None = Field(
default=None,
description=(
"Event or condition that triggered this reduction operation. "
"Used in FSM-based reducers to specify which event caused a state transition. "
"Maps to the 'trigger' field in ModelFSMStateTransition for deterministic "
"state machine processing."
),
)


__all__ = ["ModelReducerMetadata"]
26 changes: 22 additions & 4 deletions src/omnibase_core/models/core/model_action_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,35 @@ class ModelActionMetadata(BaseModel):

Provides comprehensive tracking for action execution with strong typing
and support for tool-as-a-service architecture.

Optional Fields Rationale:
- action_type: Optional to allow default construction; should be set during
action initialization for type safety and introspection.
- parent_correlation_id/session_id: Optional for standalone actions without
parent context or session grouping.
- started_at/completed_at: Populated via mark_started()/mark_completed()
lifecycle methods, not at construction time.
- timeout_seconds: Optional for actions without timeout constraints.
- execution_context: Optional for actions not requiring environment context
(development vs. production, resource limits, etc.).
- result_data/error_details: Populated only after action completion or failure.
- mcp_endpoint/graphql_endpoint: Optional for nodes not exposing these
protocol endpoints.
"""

# Core identification
action_id: UUID = Field(
default_factory=uuid4,
description="Unique identifier for this action instance",
)
action_type: ModelNodeActionType = Field(
default=..., description="Rich action type model"
action_type: ModelNodeActionType | None = Field(
default=None,
description="Rich action type model (optional for default construction)",
)
action_name: str = Field(
default="",
description="Human-readable action name (empty for default construction)",
)
action_name: str = Field(default=..., description="Human-readable action name")

# Correlation tracking
correlation_id: UUID = Field(
Expand Down Expand Up @@ -183,7 +201,7 @@ def to_service_discovery_metadata(
"""Generate metadata for service discovery as JSON-serializable data."""
return {
"action_id": str(self.action_id),
"action_type": self.action_type.name,
"action_type": self.action_type.name if self.action_type else None,
"action_name": self.action_name,
"correlation_id": str(self.correlation_id),
"trust_score": self.trust_score,
Expand Down
44 changes: 28 additions & 16 deletions src/omnibase_core/models/core/model_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from types.core_types (not from models or types.constraints).
"""

from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -87,7 +89,7 @@ class ModelEnvironment(BaseModel):
description="Feature flag configuration",
)

security_level: "ModelSecurityLevel | None" = Field(
security_level: ModelSecurityLevel | None = Field(
default=None,
description="Security requirements and configuration",
)
Expand Down Expand Up @@ -118,7 +120,7 @@ class ModelEnvironment(BaseModel):
pattern="^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$",
)

resource_limits: "ModelResourceLimits | None" = Field(
resource_limits: ModelResourceLimits | None = Field(
default=None,
description="Resource limits for this environment",
)
Expand Down Expand Up @@ -283,7 +285,7 @@ def to_environment_dict(self) -> dict[str, str]:
# === Factory Methods ===

@classmethod
def create_default(cls, name: str = "development") -> "ModelEnvironment":
def create_default(cls, name: str = "development") -> ModelEnvironment:
"""Create a default environment configuration."""
display_names = {
"development": "Development",
Expand Down Expand Up @@ -367,7 +369,7 @@ def create_default(cls, name: str = "development") -> "ModelEnvironment":
)

@classmethod
def create_development(cls) -> "ModelEnvironment":
def create_development(cls) -> ModelEnvironment:
"""Create a development environment."""
from omnibase_core.models.configuration.model_resource_limits import (
ModelResourceLimits,
Expand All @@ -394,7 +396,7 @@ def create_development(cls) -> "ModelEnvironment":
return env

@classmethod
def create_staging(cls) -> "ModelEnvironment":
def create_staging(cls) -> ModelEnvironment:
"""Create a staging environment."""
from omnibase_core.models.configuration.model_resource_limits import (
ModelResourceLimits,
Expand All @@ -420,7 +422,7 @@ def create_staging(cls) -> "ModelEnvironment":
return env

@classmethod
def create_production(cls) -> "ModelEnvironment":
def create_production(cls) -> ModelEnvironment:
"""Create a production environment."""
from omnibase_core.models.configuration.model_resource_limits import (
ModelResourceLimits,
Expand Down Expand Up @@ -464,9 +466,9 @@ def create_custom(
auto_scaling_enabled: bool = False,
monitoring_enabled: bool = True,
logging_level: str = "INFO",
security_level: "ModelSecurityLevel | None" = None,
security_level: ModelSecurityLevel | None = None,
configuration_url: HttpUrl | None = None,
) -> "ModelEnvironment":
) -> ModelEnvironment:
"""Create a custom environment configuration."""
return cls(
name=name,
Expand All @@ -483,12 +485,12 @@ def create_custom(
)

@classmethod
def create_testing(cls) -> "ModelEnvironment":
def create_testing(cls) -> ModelEnvironment:
"""Create a testing environment."""
return cls.create_default("test")

@classmethod
def create_local(cls) -> "ModelEnvironment":
def create_local(cls) -> ModelEnvironment:
"""Create a local development environment."""
env = cls.create_default("local")
env.feature_flags.enable("debug_mode")
Expand All @@ -498,9 +500,19 @@ def create_local(cls) -> "ModelEnvironment":
return env


# NOTE: model_rebuild() must be called lazily to avoid circular imports.
# The forward references (ModelSecurityLevel, ModelResourceLimits) are resolved
# either on first use or explicitly by calling:
# from omnibase_core.models.security.model_security_level import ModelSecurityLevel
# from omnibase_core.models.configuration.model_resource_limits import ModelResourceLimits
# ModelEnvironment.model_rebuild()
# Rebuild model to resolve forward references
# With `from __future__ import annotations`, this should not be needed,
# but we do it explicitly to ensure Pydantic has all types available
try:
from omnibase_core.models.configuration.model_resource_limits import (
ModelResourceLimits,
)
from omnibase_core.models.security.model_security_level import (
ModelSecurityLevel,
)

ModelEnvironment.model_rebuild()
except ImportError:
# If imports fail (e.g., during initial module loading), skip rebuild
# The model will be rebuilt on first use
pass
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,10 @@ def _validate_metadata(
result.security_checks["not_expired"] = True

# Validate action type consistency
if metadata.action_type != action.action_type:
if (
metadata.action_type is not None
and metadata.action_type != action.action_type
):
result.validation_errors.append(
f"Metadata action type '{metadata.action_type.name}' does not match "
f"action type '{action.action_type.name}'",
Expand Down
6 changes: 4 additions & 2 deletions src/omnibase_core/models/effect/model_effect_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from pydantic import BaseModel, Field

from omnibase_core.enums.enum_effect_types import EnumEffectType
from omnibase_core.models.common.model_effect_metadata import ModelEffectMetadata
from omnibase_core.utils.util_decorators import allow_dict_str_any


Expand Down Expand Up @@ -87,7 +88,8 @@ class ModelEffectInput(BaseModel):
Defaults to False.
timeout_ms: Maximum time to wait for operation completion in milliseconds.
Operations exceeding this timeout are cancelled. Defaults to 30000 (30 seconds).
metadata: Additional context metadata for tracking, logging, or custom behavior.
metadata: Typed metadata for tracking, tracing, correlation, and operation context.
Includes fields like trace_id, correlation_id, environment, tags, and priority.
timestamp: When this input was created. Auto-generated to current time.

Example:
Expand All @@ -109,7 +111,7 @@ class ModelEffectInput(BaseModel):
retry_delay_ms: int = 1000
circuit_breaker_enabled: bool = False
timeout_ms: int = 30000
metadata: dict[str, Any] = Field(default_factory=dict)
metadata: ModelEffectMetadata = Field(default_factory=ModelEffectMetadata)
timestamp: datetime = Field(default_factory=datetime.now)


Expand Down
Loading