# Copyright 2026 Guildhouse Dev # SPDX-License-Identifier: Apache-2.0 """ConnectorRuntime — governed invocation with Chronicle emission. Fix C-6: capability_mask enforcement per operation. Fix C-7: AC validated against database before invocation. Fix H-4: Chronicle INTENT emitted before execution. """ from __future__ import annotations import logging from datetime import datetime, UTC from typing import Any, Optional from sqlalchemy import text from .base import ConnectorContext, ConnectorResult from .registry import ConnectorRegistry logger = logging.getLogger(__name__) class ConnectorRuntime: def __init__( self, registry: ConnectorRegistry, chronicle_client: Any = None ) -> None: self.registry = registry self.chronicle_client = chronicle_client async def invoke( self, connector_id: str, operation: str, parameters: dict[str, Any], context: ConnectorContext, ) -> ConnectorResult: connector = self.registry.get(connector_id) if connector is None: return ConnectorResult(success=False, error=f"Unknown connector: {connector_id}") # Fix C-7: validate AC exists and is active if connector.gsap_required: if not context.gsap_context_id: return ConnectorResult(success=False, error="GSAP context required") ac_valid = await self._validate_ac(context.gsap_context_id) if not ac_valid: return ConnectorResult( success=False, error=f"AC '{context.gsap_context_id}' not found, expired, or consumed", ) # Fix C-6: enforce capability_mask (only for governed connectors) required_cap = connector.capability_for_operation(operation) if connector.gsap_required else 0 if required_cap and not (context.capability_mask & required_cap): cap_names = {0x1: "READ", 0x2: "PROPOSE", 0x4: "MUTATE", 0x8: "ADMIN"} return ConnectorResult( success=False, error=f"Operation '{operation}' requires {cap_names.get(required_cap, hex(required_cap))} " f"capability, AC has mask={context.capability_mask}", ) # Fix H-4: emit Chronicle INTENT before execution if connector.chronicle_enabled and self.chronicle_client is not None: try: await self.chronicle_client.emit( "CONNECTOR_INVOCATION_INTENT", { "connector_id": connector_id, "operation": operation, "principal_did": context.principal_did, "gsap_context_id": context.gsap_context_id, }, ) except Exception: pass # Chronicle failure must not block invocation result = await connector.invoke(operation, parameters, context) # Emit Chronicle RESULT after execution if connector.chronicle_enabled and self.chronicle_client is not None: try: cid = await self.chronicle_client.emit( "CONNECTOR_INVOCATION_RESULT", { "connector_id": connector_id, "operation": operation, "parameters_cid": connector.compute_parameters_cid(parameters), "chronicle_session_id": context.chronicle_session_id, "gsap_context_id": context.gsap_context_id, "pipeline_run_id": context.pipeline_run_id, "dag_id": context.dag_id, "success": result.success, }, ) result.lineage_cid = cid except Exception: pass # Chronicle failure must not break invocation return result async def _validate_ac(self, context_id: str) -> bool: """Fix C-7: validate AC exists and is active in the database.""" # Skip validation for internal context IDs (compliance gate) if context_id == "compliance-gate": return True try: from gsap_broker.db import engine from sqlmodel.ext.asyncio.session import AsyncSession async with AsyncSession(engine) as session: result = await session.execute( text( "SELECT status, expires_at FROM authorization_contexts " "WHERE context_id = :ctx_id" ), {"ctx_id": context_id.replace("-", "")}, ) row = result.first() if not row: return False status = row[0] if status not in ("authorized", "active"): return False # Check expiry expires_str = row[1] if expires_str: try: expires = datetime.fromisoformat(str(expires_str)) if expires < datetime.now(UTC).replace(tzinfo=None): return False except (ValueError, TypeError): pass return True except Exception as e: logger.warning("AC validation failed: %s", e) return False