> **Navigation**: [Home](../index.md) > [Patterns](README.md) > Handler Plugin Loader

# Handler Plugin Loader Pattern

## Overview

The `HandlerPluginLoader` enables contract-driven handler discovery by dynamically loading Python handler classes from YAML contract specifications. This plugin-based approach decouples handler registration from orchestrator code, supporting the ONEX principle of declarative node configuration.

**Key Capabilities**:
- Single contract loading from a specific path
- Directory-based discovery with recursive scanning
- Glob pattern-based discovery for flexible matching
- Protocol validation via duck typing

## Why Plugin Pattern Over Hardcoded Registry

The plugin pattern offers significant advantages over hardcoded handler registries:

| Approach | Trade-offs |
|----------|------------|
| **Hardcoded Registry** | Compile-time safety, but tight coupling and requires code changes to add handlers |
| **Plugin Pattern** | Loose coupling, runtime discovery, but requires path validation |

### Benefits of Plugin Architecture

1. **Contract-driven**: Handler configuration lives in `contract.yaml` or `handler_contract.yaml`, not Python code
2. **Loose coupling**: Orchestrators don't import handler modules directly
3. **Runtime discovery**: New handlers can be added without modifying orchestrator code
4. **Testability**: Mock handlers can be injected via contract configuration
5. **Deployment flexibility**: Different environments can use different handlers via contract configuration

### Hardcoded Registry Drawbacks

```python
# AVOID: Hardcoded registry pattern
HANDLER_REGISTRY = {
    "auth": "myapp.handlers.AuthHandler",
    "validate": "myapp.handlers.ValidateHandler",
    # Adding a new handler requires code change AND deployment
}
```

With hardcoded registries:
- Adding handlers requires code changes
- Tight coupling between orchestrator and handler modules
- Testing requires mocking the registry
- Environment-specific handlers require conditional imports

---

## Why YAML Contracts Over Python Decorators

While Python decorators like `@register_handler("auth")` seem convenient, YAML contracts offer better tooling support and auditability:

| Aspect | YAML Contracts | Python Decorators |
|--------|---------------|-------------------|
| **Tooling** | Machine-readable, lintable, diffable | Requires AST parsing |
| **Auditability** | Changes visible in git, reviewable | Scattered across codebase |
| **Non-Python access** | CI/CD, dashboards can read contracts | Requires Python runtime |
| **Separation of concerns** | Configuration separate from logic | Mixes config with code |
| **Discovery** | Single scan of contract files | Must import all modules to find decorators |

### YAML Contract Advantages

1. **Machine-Readable**: CI/CD pipelines can validate contracts without Python
2. **Centralized Discovery**: Find all handlers by scanning YAML files
3. **Clean Diffs**: Contract changes are easy to review
4. **IDE Support**: YAML validation and completion available
5. **Documentation**: Contracts serve as handler documentation

---

## Contract-Based Handler Declaration

### Contract File Structure

The loader recognizes two contract file names:

| Filename | Purpose |
|----------|---------|
| `handler_contract.yaml` | Dedicated handler contract (preferred) |
| `contract.yaml` | General ONEX contract with handler fields |

### Required Fields

```yaml
# handler_contract.yaml
handler_name: "auth.validate"           # Unique handler identifier
handler_class: "myapp.handlers.auth.AuthHandler"  # Fully-qualified class path
handler_type: "effect"                  # Handler type: effect, compute, reducer, orchestrator
```

### Full Contract Example

```yaml
# handler_contract.yaml
handler_name: "db.query"
handler_class: "myapp.handlers.database.QueryHandler"
handler_type: "effect"
capability_tags:
  - database
  - async
  - pooled
```

### Contract in Node Directory

```
nodes/authentication/
    contract.yaml           # Node contract with handler routing
    node.py                 # Declarative node class
    handlers/
        handler_contract.yaml   # Handler-specific contract
        handler_authenticate.py # Handler implementation
```

---

## Contract File Precedence

> **FAIL-FAST: Ambiguous Contract Configuration Raises Error**
>
> When **both** `handler_contract.yaml` **and** `contract.yaml` exist in the **same directory**,
> the loader raises a `ProtocolConfigurationError` with error code `AMBIGUOUS_CONTRACT_CONFIGURATION`
> (HANDLER_LOADER_040). The loader does NOT load either file in this case.
>
> This fail-fast behavior prevents:
> - Duplicate handler registrations
> - Confusion about which contract is authoritative
> - Unexpected runtime behavior from conflicting configurations
>
> **Solution**: Use only **ONE** contract file per handler directory.

### Discovery Rules

When scanning a directory, the loader discovers contracts following these rules:

1. **Both filenames searched**: `handler_contract.yaml` and `contract.yaml` are both valid
2. **No precedence between files**: Both files in different directories are loaded
3. **FAIL-FAST if both in same directory**: Raises `ProtocolConfigurationError` immediately
4. **Deduplication by resolved path**: Same file via different paths is loaded once
5. **Sorted processing**: Discovered paths are sorted for deterministic order

### Same Directory Behavior (Ambiguous Configuration)

If both `handler_contract.yaml` and `contract.yaml` exist in the same directory,
the loader **fails fast** with an error:

```
ProtocolConfigurationError: Ambiguous contract configuration in 'auth':
Found both 'handler_contract.yaml' and 'contract.yaml'.
Use only ONE contract file per handler directory to avoid conflicts.
```

**Error Code**: `HANDLER_LOADER_040` (AMBIGUOUS_CONTRACT_CONFIGURATION)

```
# Directory structure (CAUSES ERROR)
handlers/
    handler_contract.yaml   # Found
    contract.yaml          # Also found = ERROR!

# Result: ProtocolConfigurationError raised - no handlers loaded
```

### Why Fail-Fast Instead of Precedence?

The loader intentionally raises an error instead of implementing precedence because:

1. **Explicit is better than implicit**: Silent precedence could mask configuration errors
2. **Fail-fast philosophy**: Errors are caught early at startup, not at runtime
3. **No assumptions**: The loader cannot know which file the user intends to be authoritative
4. **Clear resolution**: Error message tells exactly what to do

### Correct Configuration (Recommended)

Use **one contract file per handler directory** to avoid ambiguity:

```
# CORRECT: One contract per directory
nodes/auth/
    handler_contract.yaml   # Preferred: dedicated handler contract
    handler_auth.py

nodes/validate/
    handler_contract.yaml
    handler_validate.py
```

### Incorrect Configuration (Causes Error)

```
# INCORRECT: Both contract types in same directory
nodes/auth/
    handler_contract.yaml   # Defines "auth.validate" handler
    contract.yaml          # Also defines "auth.validate" handler (conflict!)
    handler_auth.py

# This will:
# 1. Raise ProtocolConfigurationError with AMBIGUOUS_CONTRACT_CONFIGURATION
# 2. Stop handler loading immediately (fail-fast)
# 3. Provide actionable error message explaining how to fix
```

### Resolving Ambiguous Configurations

If you encounter the `AMBIGUOUS_CONTRACT_CONFIGURATION` error:

1. **Identify the intended contract**: Decide which file should be authoritative
2. **Remove or rename the other**: Delete the unused contract or rename it (e.g., `contract.yaml.bak`)
3. **Consolidate if needed**: Merge handler definitions into a single contract file
4. **Verify after changes**: Re-run the loader and confirm no errors occur

---

## Protocol Validation

The loader validates that loaded classes implement `ProtocolHandler` using duck typing:

### Required Methods

| Method | Type | Purpose |
|--------|------|---------|
| `handler_type` | Property | Returns handler type identifier |
| `initialize(config)` | Async | Connection/pool setup |
| `shutdown(timeout_seconds)` | Async | Resource cleanup |
| `execute(request, operation_config)` | Async | Operation execution |
| `describe()` | Sync | Handler metadata/introspection |

### Validation Example

```python
class ValidHandler:
    """Handler implementing ProtocolHandler via duck typing."""

    @property
    def handler_type(self) -> str:
        return "effect"

    async def initialize(self, config: dict) -> None:
        """Initialize handler connections."""
        pass

    async def shutdown(self, timeout_seconds: float = 30.0) -> None:
        """Release resources."""
        pass

    async def execute(self, request: object, config: object) -> object:
        """Execute operation."""
        return {}

    def describe(self) -> dict:
        """Return handler metadata."""
        return {"handler_id": "valid.handler", "version": "1.0.0"}
```

### Why Duck Typing

ONEX uses duck typing for protocol validation to:
1. Avoid tight coupling to specific base classes
2. Enable flexibility in handler implementation strategies
3. Support mixin-based handler composition
4. Allow testing with mock handlers that satisfy the protocol

---

## Security Considerations

**CRITICAL**: YAML contracts are treated as executable code, not mere configuration.

This section provides comprehensive security documentation for the handler plugin loader's dynamic import mechanism. Understanding these security implications is essential for safe production deployment.

### Quick Security Reference

| Threat | Risk | Status | Control |
|--------|------|--------|---------|
| **YAML deserialization attacks** | CRITICAL | **MITIGATED** | `yaml.safe_load()` blocks `!!python/object` tags |
| **Memory exhaustion (DoS)** | HIGH | **MITIGATED** | 10MB file size limit (`MAX_CONTRACT_SIZE`) |
| **Arbitrary class loading** | HIGH | **MITIGATED** | Protocol validation requires 5 `ProtocolHandler` methods |
| **Path information disclosure** | MEDIUM | **MITIGATED** | `_sanitize_exception_message()` strips paths from errors |
| **Non-class object loading** | MEDIUM | **MITIGATED** | `isinstance(handler_class, type)` check |
| **Malicious contract injection** | CRITICAL | **OPTIONALLY MITIGATED** | Enable `allowed_namespaces` parameter |
| **Module path traversal** | CRITICAL | **OPTIONALLY MITIGATED** | Enable `allowed_namespaces` with package boundary validation |
| **Import-time code execution** | CRITICAL | **OPTIONALLY MITIGATED** | Enable `allowed_namespaces` to restrict to trusted modules |
| **Handler instantiation exploits** | HIGH | **OPTIONALLY MITIGATED** | Enable `allowed_namespaces` to restrict to trusted modules |

**Legend**:
- **MITIGATED**: Built-in protection, always active
- **OPTIONALLY MITIGATED**: Requires enabling `allowed_namespaces` parameter (recommended for production)

**For production deployments**: Enable `allowed_namespaces` to convert "optionally mitigated" threats to "mitigated".

---

### Dynamic Import Security Model

The `HandlerPluginLoader` uses Python's `importlib.import_module()` to dynamically load handler classes. This is a powerful but security-sensitive operation.

#### Why Dynamic Import Is Used

| Benefit | Explanation |
|---------|-------------|
| **Contract-driven architecture** | Handler bindings defined in YAML, not hardcoded |
| **Plugin extensibility** | Third-party handlers load without core runtime changes |
| **Deployment flexibility** | Different environments use different handlers via contracts |
| **Zero coupling** | Runtime has no compile-time dependency on handler implementations |

#### Why Contracts Equal Code

When the loader processes a contract:
1. It reads the `handler_class` fully-qualified path (e.g., `myapp.handlers.AuthHandler`)
2. It calls `importlib.import_module()` on the module path
3. **The module's top-level code executes during import**

This means:
- Module-level side effects execute immediately when a contract is loaded
- Malicious contracts can execute arbitrary code if an attacker can write to contract directories
- Import-time vulnerabilities in handler modules are triggered by contract discovery

---

### Threat Model

Understanding what the loader protects against (and what it doesn't) is critical for secure deployment.

#### Attack Vectors

| Attack Vector | Description | Risk Level |
|---------------|-------------|------------|
| **Malicious contract injection** | Attacker writes contract pointing to malicious module | CRITICAL |
| **Module path manipulation** | Contract specifies path to unintended module | HIGH |
| **YAML deserialization attack** | Malicious YAML constructs execute code | HIGH (mitigated) |
| **Memory exhaustion** | Oversized contract files consume memory | MEDIUM (mitigated) |
| **Import side effects** | Legitimate modules have unintended import-time behavior | MEDIUM |
| **Handler instantiation exploits** | Malicious code runs when handler is instantiated | HIGH |

#### Trust Boundaries

```
┌─────────────────────────────────────────────────────────────────────┐
│                      TRUSTED ZONE                                    │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐  │
│  │ Contract Files  │───>│ HandlerPlugin   │───>│ Handler Modules │  │
│  │ (YAML)          │    │ Loader          │    │ (Python)        │  │
│  └─────────────────┘    └─────────────────┘    └─────────────────┘  │
│         │                        │                      │           │
│         ▼                        ▼                      ▼           │
│  Filesystem access       importlib.import_module   Arbitrary code   │
│  controls                                          execution        │
└─────────────────────────────────────────────────────────────────────┘
         │
         │ TRUST BOUNDARY: Everything entering this zone must be trusted
         ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      UNTRUSTED ZONE                                  │
│  User input, external APIs, unverified sources                       │
└─────────────────────────────────────────────────────────────────────┘
```

**Key principle**: Contract files and handler modules MUST come from trusted sources. The loader cannot distinguish between legitimate and malicious module paths.

---

### Built-in Security Controls

The loader implements several protections:

| Protection | Implementation | Mitigates | Location |
|------------|----------------|-----------|----------|
| **File size limits** | `MAX_CONTRACT_SIZE = 10MB` | Memory exhaustion, DoS | `_validate_file_size()` |
| **YAML safe loading** | `yaml.safe_load()` | YAML deserialization attacks (`!!python/object`) | `load_from_contract()` |
| **Protocol validation** | Duck typing checks for all 5 `ProtocolHandler` methods | Loading arbitrary non-handler classes | `_validate_handler_protocol()` |
| **Error containment** | Graceful failure per contract | Single bad contract doesn't crash system | `load_from_directory()` |
| **Correlation ID tracking** | UUID4 for all operations | Audit trail for security events | All public methods |
| **Class type verification** | `isinstance(handler_class, type)` check | Loading non-class objects | `_import_handler_class()` |

#### Protocol Validation Details

The loader verifies loaded classes implement `ProtocolHandler` via duck typing:

```python
# Required methods checked by _validate_handler_protocol():
required_methods = [
    "handler_type",   # Property: handler type identifier
    "initialize",     # Async: connection/pool setup
    "shutdown",       # Async: resource cleanup
    "execute",        # Async: operation execution
    "describe",       # Sync: handler metadata
]
```

A class **fails validation** if any of these 5 methods are missing, preventing loading of arbitrary classes that don't implement the handler contract.

---

### Optional Security Controls

The loader provides namespace allowlisting as an **optional built-in security control**:

| Control | Implementation | Mitigates | Status |
|---------|----------------|-----------|--------|
| **Namespace allowlisting** | `allowed_namespaces` constructor parameter | Untrusted module imports, path sandboxing | **IMPLEMENTED (OPTIONAL)** |

**Namespace Allowlisting Usage:**
```python
from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader

# Restrict imports to trusted namespaces only (recommended for production)
loader = HandlerPluginLoader(
    allowed_namespaces=["omnibase_infra.", "omnibase_core.", "mycompany.handlers."]
)

# This succeeds (handler_class: omnibase_infra.handlers.AuthHandler):
handler = loader.load_from_contract(Path("auth_contract.yaml"))

# This fails with NAMESPACE_NOT_ALLOWED (HANDLER_LOADER_013):
# (handler_class: malicious_pkg.EvilHandler)
handler = loader.load_from_contract(Path("malicious_contract.yaml"))
```

**Security Trade-offs:**

| Configuration | Behavior | Use Case |
|---------------|----------|----------|
| `allowed_namespaces=None` (default) | All namespaces allowed | Development, trusted environments |
| `allowed_namespaces=["omnibase_.", "myapp."]` | Only specified prefixes allowed | Production deployments |
| `allowed_namespaces=[]` | ALL namespaces blocked | Effectively disables loader |

#### Path Traversal Protection

The `allowed_namespaces` parameter provides **package boundary validation** that prevents path traversal attacks:

**Attack Example**:
```yaml
# Malicious contract attempting path traversal
handler_class: "os.system"  # Attempt to load system module
# OR
handler_class: "sys.modules"  # Attempt to access runtime internals
```

**How Namespace Allowlisting Protects**:
1. **Prefix matching with boundary validation**: The namespace `"foo."` only matches packages starting with `foo.`, not `foobar.`
2. **Explicit trailing dot requirement**: Namespaces should end with `.` to enforce package boundaries
3. **No dynamic path construction**: Module paths cannot escape the allowed namespace prefixes

**Implementation Detail** (from `_validate_namespace()`):
```python
# Package boundary validation prevents "foo" from matching "foobar.module"
for namespace in self._allowed_namespaces:
    if class_path.startswith(namespace):
        if namespace.endswith("."):
            return  # Matched with proper package boundary
        remaining = class_path[len(namespace):]
        if remaining == "" or remaining.startswith("."):
            return  # Exact match or proper boundary
```

**Recommendation**: Always use trailing dots in namespace prefixes for clarity and consistency:
```python
# RECOMMENDED: Trailing dots make package boundaries explicit
loader = HandlerPluginLoader(
    allowed_namespaces=["omnibase_infra.", "myapp.handlers."]
)

# WORKS BUT LESS EXPLICIT: Without trailing dot, relies on boundary validation
# The implementation validates that remaining path is empty or starts with "."
# So "omnibase" matches "omnibase" and "omnibase.core" but NOT "omnibase_malicious"
loader = HandlerPluginLoader(
    allowed_namespaces=["omnibase", "myapp"]  # Prefer trailing dots for clarity
)
```

### Contract Content Security: No Secrets in Contracts

**CRITICAL**: Handler contracts must NEVER contain secrets, credentials, or sensitive configuration values.

#### What Must NOT Be in Contracts

| Category | Examples | Why Prohibited |
|----------|----------|----------------|
| **Credentials** | Passwords, API keys, tokens, secrets | Git history exposure, log leakage |
| **Connection strings with auth** | `postgresql://user:pass@host` | Full credential disclosure |
| **Private keys** | SSL certificates, encryption keys | Cryptographic compromise |
| **Environment-specific secrets** | Production passwords, staging tokens | Cross-environment leakage |
| **PII** | User data, emails in examples | Privacy/compliance violation |

#### What IS Safe in Contracts

| Category | Examples | Rationale |
|----------|----------|-----------|
| **Handler class paths** | `myapp.handlers.AuthHandler` | Code reference, not secret |
| **Handler names** | `auth.validate`, `db.query` | Identifiers only |
| **Handler types** | `effect`, `compute`, `reducer` | Static categorization |
| **Capability tags** | `["database", "async"]` | Feature flags, non-sensitive |
| **Non-secret configuration** | Timeouts, retry counts, feature flags | Operational metadata |

#### Example: Correct vs Incorrect Contract

```yaml
# INCORRECT - Contains secrets (NEVER DO THIS)
handler_name: "db.query"
handler_class: "myapp.handlers.DatabaseHandler"
handler_type: "effect"
config:
  connection_string: "postgresql://admin:secret123@db.prod:5432/app"  # VIOLATION
  api_key: "sk-abc123xyz"  # VIOLATION
  aws_secret_key: "AKIAIOSFODNN7EXAMPLE"  # VIOLATION
```

```yaml
# CORRECT - No secrets, references environment/Infisical
handler_name: "db.query"
handler_class: "myapp.handlers.DatabaseHandler"
handler_type: "effect"
capability_tags:
  - database
  - pooled
  - async
# Secrets loaded at runtime from:
# - Environment variables (POSTGRES_PASSWORD)
# - Infisical (/shared/database/POSTGRES_PASSWORD)
# - Kubernetes secrets (mounted as env vars)
```

#### Where Secrets Should Live

| Source | When to Use | Example |
|--------|-------------|---------|
| **Environment variables** | Container/pod configuration | `POSTGRES_PASSWORD`, `API_KEY` |
| **Infisical** | Dynamic secrets, rotation | `/shared/database/POSTGRES_PASSWORD` |
| **Kubernetes secrets** | Static secrets in K8s | `secretKeyRef` in pod spec |
| **AWS Secrets Manager** | AWS deployments | `secretsmanager:GetSecretValue` |

#### CI Validation

Contracts should be validated in CI to detect accidental secret inclusion:

```bash
# Example: Scan contracts for potential secrets
grep -rE "(password|secret|key|token|credential).*:" contracts/ && echo "FAIL: Potential secrets in contracts" && exit 1
```

Consider using tools like:
- **gitleaks** - Scan for hardcoded secrets in contracts
- **trufflehog** - Detect high-entropy strings
- **detect-secrets** - Pre-commit hook for secret detection

**See Also**: [Secret Management](./security_patterns.md#secret-management) for comprehensive secret handling guidance.

---

### What the Loader Does NOT Protect Against

**CRITICAL**: The following protections require deployment-level mitigation:

| Gap | Description | Recommended Mitigation |
|-----|-------------|------------------------|
| **Module signature verification** | No cryptographic validation of handler code | Use signed container images |
| **Import hook filtering** | No interception of `importlib` calls | Add custom `MetaPathFinder` |
| **Runtime isolation** | Handlers run in same process as loader | Use subprocess isolation for untrusted code |
| **Mutation prevention** | Loaded modules can modify global state | Run in separate process or container |

#### Why `yaml.safe_load()` Is Not Sufficient

`yaml.safe_load()` prevents YAML deserialization attacks (e.g., `!!python/object` tags) but does NOT prevent:

- Malicious `handler_class` values that point to attacker-controlled modules
- Module side effects during `import_module()` execution
- Post-import exploitation via handler instantiation

---

### Secure Deployment Checklist

#### Minimum Requirements (All Deployments)

All production deployments MUST implement:

1. **File permissions**: Contract directories readable only by runtime user
   ```bash
   chmod 755 /app/contracts
   chmod 644 /app/contracts/**/*.yaml
   chown -R appuser:appgroup /app/contracts
   ```

2. **Write protection**: Contract directories read-only at runtime
   ```yaml
   # Kubernetes volume mount
   volumeMounts:
     - name: handler-contracts
       mountPath: /app/contracts
       readOnly: true
   ```

3. **Source validation**: Contracts from trusted sources only
   - Version-controlled repositories with code review
   - Signed container images
   - Verified artifact registries

4. **Logging enabled**: Ensure handler loading is logged
   ```python
   import logging
   logging.getLogger("omnibase_infra.runtime.handler_plugin_loader").setLevel(logging.INFO)
   ```

#### High-Security Environments

For elevated security requirements, implement additional controls:

##### Namespace Allowlisting (Built-in - Recommended)

Use the built-in `allowed_namespaces` parameter (see [Optional Security Controls](#optional-security-controls) above):

```python
from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader

# Recommended for production: restrict to trusted namespaces
loader = HandlerPluginLoader(
    allowed_namespaces=[
        "omnibase_infra.handlers.",     # First-party handlers
        "omnibase_core.handlers.",      # Core handlers
        "myapp.handlers.",              # Application handlers
        "approved_plugins.",            # Vetted third-party
    ]
)
```

##### External Policy Service Integration (Advanced)

For environments requiring external policy service validation (e.g., OPA, custom approval service), you can wrap the loader. Note that for simple namespace validation, use the built-in `allowed_namespaces` parameter instead (see above).

```python
from pathlib import Path
from typing import Protocol
from uuid import UUID

import yaml

from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
from omnibase_infra.models.runtime import ModelLoadedHandler


class PolicyServiceClient(Protocol):
    """Protocol for external policy service integration."""

    async def check_handler_approval(
        self,
        handler_class: str,
        contract_path: str,
        correlation_id: UUID,
    ) -> "ApprovalResult":
        """Check if handler is approved for loading."""
        ...


class ApprovalResult(Protocol):
    """Result from policy service approval check."""

    approved: bool
    reason: str


from omnibase_infra.errors import ProtocolConfigurationError, ModelInfraErrorContext
from omnibase_infra.enums import EnumInfraTransportType


class PolicyServiceValidationError(ProtocolConfigurationError):
    """Raised when external policy service rejects handler loading.

    Extends ProtocolConfigurationError to integrate with ONEX error handling.
    Use this for custom validation that goes beyond namespace prefix matching.

    IMPORTANT - Error Code Domain Separation:
        This error uses POLICY_SERVICE_xxx codes which are DISTINCT from
        EnumHandlerLoaderError (HANDLER_LOADER_xxx) codes. This separation
        reflects different validation domains:

        - HANDLER_LOADER_xxx: Built-in loader validation (file not found,
          invalid YAML, protocol not implemented, namespace not allowed, etc.)
        - POLICY_SERVICE_xxx: External policy service validation (approval
          service rejection, hash verification, etc.)

    NOTE: For namespace validation, prefer the built-in `allowed_namespaces`
    parameter which uses EnumHandlerLoaderError.NAMESPACE_NOT_ALLOWED
    (HANDLER_LOADER_013). This custom error is for additional validation
    scenarios like external policy service integration.

    Error Codes (POLICY_SERVICE domain - not EnumHandlerLoaderError):
        POLICY_SERVICE_001: External policy service rejected handler
        POLICY_SERVICE_002: Policy service unavailable
        POLICY_SERVICE_003: Handler hash not in approved list
    """

    def __init__(
        self,
        message: str,
        error_code: str,
        context: ModelInfraErrorContext | None = None,
        **kwargs: object,
    ) -> None:
        """Initialize PolicyServiceValidationError with required error code.

        Args:
            message: Human-readable error message
            error_code: Error code for categorization (e.g., "POLICY_SERVICE_001").
                        Uses POLICY_SERVICE_xxx codes, NOT EnumHandlerLoaderError.
            context: Optional infrastructure error context with correlation_id
            **kwargs: Additional context fields
        """
        super().__init__(message, context=context, **kwargs)
        self.error_code = error_code


async def load_with_policy_service_validation(
    loader: HandlerPluginLoader,
    path: Path,
    policy_service: PolicyServiceClient,
    correlation_id: UUID | None = None,
) -> ModelLoadedHandler:
    """Load handler with external policy service validation.

    This wrapper pattern is for advanced scenarios requiring external
    policy service integration. For simple namespace validation, use
    the built-in `allowed_namespaces` parameter instead.

    Use this wrapper when you need:
    - External policy service approval (e.g., OPA, custom approval service)
    - Handler code hash verification against approved list
    - Audit logging to external compliance system

    NOTE: For namespace-based validation, prefer:
        loader = HandlerPluginLoader(
            allowed_namespaces=["omnibase_infra.", "myapp.handlers."]
        )
    This uses the built-in HANDLER_LOADER_013 (NAMESPACE_NOT_ALLOWED) error.

    Args:
        loader: The handler plugin loader (should have allowed_namespaces set)
        path: Path to contract file
        policy_service: External policy service client
        correlation_id: Optional correlation ID for tracing

    Returns:
        Loaded handler if policy service approves

    Raises:
        PolicyServiceValidationError: If policy service rejects handler
    """
    from uuid import uuid4
    import hashlib

    corr_id = correlation_id or uuid4()

    # Read contract to get handler class path
    with open(path) as f:
        contract_data = yaml.safe_load(f)

    handler_class = contract_data.get("handler_class", "")

    # Consult external policy service for approval
    approval_result = await policy_service.check_handler_approval(
        handler_class=handler_class,
        contract_path=str(path),
        correlation_id=corr_id,
    )

    if not approval_result.approved:
        context = ModelInfraErrorContext(
            transport_type=EnumInfraTransportType.RUNTIME,
            operation="policy_service_validation",
            correlation_id=corr_id,
        )
        raise PolicyServiceValidationError(
            f"Policy service rejected handler: {approval_result.reason}",
            error_code="POLICY_SERVICE_001",
            context=context,
            handler_class=handler_class,
        )

    # Policy service approved, proceed with loading
    # The loader's built-in allowed_namespaces will also validate
    return loader.load_from_contract(path, corr_id)
```

##### Import Hook Monitoring

Use Python's import system to audit dynamic imports:

```python
import logging
import sys
from importlib.abc import MetaPathFinder

logger = logging.getLogger("security.imports")


class SecurityAuditFinder(MetaPathFinder):
    """Meta path finder that logs all dynamic imports."""

    def find_module(self, fullname: str, path=None):
        """Log import attempt without blocking.

        Returns None to allow the import to proceed via normal mechanisms.
        """
        logger.info(
            "Dynamic import: %s",
            fullname,
            extra={"module": fullname, "path": path},
        )
        return None  # Allow import to proceed


# Install the audit hook at application startup
sys.meta_path.insert(0, SecurityAuditFinder())
```

##### Subprocess Isolation

For complete isolation, run handlers in separate processes:

```python
import multiprocessing


def run_handler_isolated(
    handler_module: str,
    handler_class: str,
    method: str,
    args: tuple[object, ...],
) -> object:
    """Execute handler method in isolated subprocess.

    The subprocess has its own memory space, so malicious code
    cannot affect the parent process.
    """
    def worker(queue, module, cls, method, args):
        import importlib
        mod = importlib.import_module(module)
        handler = getattr(mod, cls)()
        result = getattr(handler, method)(*args)
        queue.put(result)

    queue = multiprocessing.Queue()
    process = multiprocessing.Process(
        target=worker,
        args=(queue, handler_module, handler_class, method, args),
    )
    process.start()
    process.join(timeout=30)

    if process.is_alive():
        process.terminate()
        raise TimeoutError("Handler execution timed out")

    return queue.get()
```

---

### Monitoring and Incident Response

#### Security Event Logging

The loader logs security-relevant events with correlation IDs:

| Event | Log Level | When |
|-------|-----------|------|
| Contract load success | INFO | Handler successfully loaded |
| Contract load failure | WARNING | Individual contract failed |
| Protocol validation failure | WARNING | Class missing required methods |
| Module import error | WARNING | `importlib.import_module()` failed |
| File size exceeded | WARNING | Contract exceeds 10MB limit |
| Ambiguous contract configuration | WARNING | Both contract types in same directory |

#### Alerting Recommendations

Configure alerts for:

1. **Unexpected handler paths**: New module paths not seen before
2. **High failure rates**: Many contracts failing to load
3. **Import errors from unknown modules**: May indicate probing attacks
4. **File system changes**: Modifications to contract directories

#### Incident Response

If a malicious contract is suspected:

1. **Isolate**: Stop loading new contracts immediately
2. **Identify**: Check logs for correlation IDs of suspicious loads
3. **Contain**: Remove compromised contract files
4. **Analyze**: Review what code was executed during import
5. **Remediate**: Rotate any secrets that may have been exposed
6. **Prevent**: Implement additional controls (allowlisting, monitoring)

---

### Security Decision Trade-offs

| Decision | Trade-off |
|----------|-----------|
| **Plugin architecture vs hardcoded registry** | Flexibility vs compile-time safety |
| **Dynamic import vs static binding** | Runtime extensibility vs predictable behavior |
| **YAML contracts vs Python decorators** | Machine-readable config vs AST safety |
| **Duck typing vs explicit inheritance** | Loose coupling vs type guarantees |

The loader prioritizes flexibility and loose coupling, placing security responsibility on deployment configuration rather than runtime enforcement.

---

**For complete security rationale, see**: [ADR: Handler Plugin Loader Security Model](../decisions/adr-handler-plugin-loader-security.md)

---

## Error Codes

| Code | Enum Value | Meaning |
|------|------------|---------|
| HANDLER_LOADER_001 | `FILE_NOT_FOUND` | Contract file not found |
| HANDLER_LOADER_002 | `INVALID_YAML_SYNTAX` | YAML parsing failed |
| HANDLER_LOADER_003 | `SCHEMA_VALIDATION_FAILED` | Pydantic validation failed |
| HANDLER_LOADER_004 | `MISSING_REQUIRED_FIELDS` | Required contract fields missing |
| HANDLER_LOADER_005 | `FILE_SIZE_EXCEEDED` | Contract exceeds 10MB limit |
| HANDLER_LOADER_006 | `PROTOCOL_NOT_IMPLEMENTED` | Class doesn't implement ProtocolHandler |
| HANDLER_LOADER_007 | `NOT_A_FILE` | Path exists but is not a file |
| HANDLER_LOADER_008 | `FILE_READ_ERROR` | I/O error reading file |
| HANDLER_LOADER_009 | `FILE_STAT_ERROR` | I/O error getting file info |
| HANDLER_LOADER_010 | `MODULE_NOT_FOUND` | Handler module not found |
| HANDLER_LOADER_011 | `CLASS_NOT_FOUND` | Class not found in module |
| HANDLER_LOADER_012 | `IMPORT_ERROR` | Module import failed |
| HANDLER_LOADER_013 | `NAMESPACE_NOT_ALLOWED` | Handler module namespace not in allowlist |
| HANDLER_LOADER_020 | `DIRECTORY_NOT_FOUND` | Directory not found |
| HANDLER_LOADER_021 | `PERMISSION_DENIED` | Permission denied |
| HANDLER_LOADER_022 | `NOT_A_DIRECTORY` | Path is not a directory |
| HANDLER_LOADER_030 | `EMPTY_PATTERNS_LIST` | Glob patterns list empty |
| HANDLER_LOADER_031 | `INVALID_GLOB_PATTERN` | Invalid glob pattern syntax |
| HANDLER_LOADER_040 | `AMBIGUOUS_CONTRACT_CONFIGURATION` | Both contract types in same directory |

---

## Usage Examples

### Single Contract Loading

```python
from pathlib import Path
from uuid import uuid4

from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader

loader = HandlerPluginLoader()

# Load single handler
handler = loader.load_from_contract(
    Path("src/handlers/auth/handler_contract.yaml"),
    correlation_id=uuid4(),
)

print(f"Loaded: {handler.handler_name}")
print(f"Type: {handler.handler_type}")
print(f"Class: {handler.handler_class}")
```

### Directory Discovery

```python
# Load all handlers from a directory tree
handlers = loader.load_from_directory(
    Path("src/handlers"),
    correlation_id=uuid4(),
)

print(f"Discovered {len(handlers)} handlers")
for handler in handlers:
    print(f"  - {handler.handler_name} ({handler.handler_type})")
```

### Glob Pattern Discovery

```python
# Discover with specific patterns
handlers = loader.discover_and_load(
    patterns=[
        "src/**/handler_contract.yaml",
        "plugins/**/contract.yaml",
    ],
    correlation_id=uuid4(),
    base_path=Path("/app/project"),  # Optional: explicit base for deterministic results
)
```

### Error Handling

```python
from omnibase_infra.errors import ProtocolConfigurationError, InfraConnectionError

try:
    handler = loader.load_from_contract(contract_path)
except ProtocolConfigurationError as e:
    # Contract validation failed
    print(f"Contract error: {e}")
    print(f"Error code: {e.model.loader_error}")
    print(f"Correlation ID: {e.model.correlation_id}")
except InfraConnectionError as e:
    # Handler import failed
    print(f"Import error: {e}")
    print(f"Module path: {e.model.module_path}")
```

---

## Testing Handlers

### Mock Handler for Tests

```python
class MockTestHandler:
    """Mock handler for testing that satisfies ProtocolHandler."""

    @property
    def handler_type(self) -> str:
        return "mock"

    async def initialize(self, config: dict) -> None:
        pass

    async def shutdown(self, timeout_seconds: float = 30.0) -> None:
        pass

    async def execute(self, request: object, config: object) -> object:
        return {"mocked": True}

    def describe(self) -> dict:
        return {"handler_id": "mock.test"}
```

### Test Contract Fixture

```python
import pytest
from pathlib import Path

@pytest.fixture
def valid_contract(tmp_path: Path) -> Path:
    """Create a valid handler contract for testing."""
    contract = tmp_path / "handler_contract.yaml"
    contract.write_text("""
handler_name: "test.handler"
handler_class: "tests.fixtures.MockTestHandler"
handler_type: "compute"
capability_tags:
  - test
  - mock
""")
    return contract
```

---

## Related Patterns

- [Contract Dependency Injection](./container_dependency_injection.md) - Container-based DI
- [Security Patterns](./security_patterns.md) - General security guidance
- [Error Handling Patterns](./error_handling_patterns.md) - Error context and codes
- [Correlation ID Tracking](./correlation_id_tracking.md) - Request tracing

## See Also

- `src/omnibase_infra/runtime/handler_plugin_loader.py` - Implementation
- `src/omnibase_infra/runtime/protocol_handler_plugin_loader.py` - Protocol definition
- `src/omnibase_infra/models/runtime/model_loaded_handler.py` - Result model
- `src/omnibase_infra/models/runtime/model_handler_contract.py` - Contract model
- [ADR: Handler Plugin Loader Security Model](../decisions/adr-handler-plugin-loader-security.md) - Security decisions
- [Migration Guide: wire_default_handlers()](../migration/MIGRATION_WIRE_DEFAULT_HANDLERS.md) - Migration from legacy wiring
