# Copyright 2026 Guildhouse Dev # SPDX-License-Identifier: Apache-2.0 """Orchestrator connector framework — multi-step workflow execution. Orchestrator connectors manage workflows that span multiple steps and potentially multiple targets. Each step may acquire its own credentials via the CredentialResolver. Unlike ``SessionConnector`` (single target, single credential, single command), an ``OrchestratorConnector``: - Plans a sequence of steps before execution - Executes steps in order, stopping on required-step failure - Reports partial results (which steps completed before failure) - Can target different hosts per step Rust port note: ``WorkflowStep`` and ``WorkflowPlan`` map to plain structs. ``OrchestratorConnector`` maps to an async trait with ``plan()`` and ``execute_step()`` methods. """ from __future__ import annotations import logging from abc import abstractmethod from dataclasses import dataclass, field from typing import Any from gsap_broker.connectors.base import ConnectorContext, ConnectorPlugin, ConnectorResult from gsap_broker.credentials.resolver import CredentialResolver logger = logging.getLogger(__name__) @dataclass class WorkflowStep: """A single step in a workflow plan.""" name: str command: str targets: list[str] = field(default_factory=list) required: bool = True extra_vars: dict[str, Any] = field(default_factory=dict) @dataclass class WorkflowPlan: """Ordered sequence of steps for a workflow.""" steps: list[WorkflowStep] = field(default_factory=list) class OrchestratorConnector(ConnectorPlugin): """Base for multi-step workflow connectors (Ansible, Terraform, etc). Subclasses implement ``plan()`` to convert an operation + parameters into a ``WorkflowPlan``, and ``execute_step()`` to run each step. The base ``invoke()`` handles: - Planning the workflow - Executing steps in order - Stopping on required-step failure - Aggregating results with partial-completion reporting """ def __init__(self, credential_resolver: CredentialResolver): self._resolver = credential_resolver async def invoke( self, operation: str, parameters: dict[str, Any], context: ConnectorContext ) -> ConnectorResult: try: plan = await self.plan(operation, parameters, context) except Exception as e: return ConnectorResult(success=False, error=f"Planning failed: {e}") if not plan.steps: return ConnectorResult(success=True, data={"steps": []}) results: list[dict[str, Any]] = [] for step in plan.steps: result = await self.execute_step(step, context) results.append({"step": step.name, **result}) if not result.get("success") and step.required: return ConnectorResult( success=False, data={"completed": results, "failed_at": step.name}, ) return ConnectorResult(success=True, data={"steps": results}) @abstractmethod async def plan( self, operation: str, parameters: dict[str, Any], context: ConnectorContext ) -> WorkflowPlan: """Convert an operation into a step-by-step execution plan.""" ... @abstractmethod async def execute_step( self, step: WorkflowStep, context: ConnectorContext ) -> dict[str, Any]: """Execute a single workflow step. Returns a dict with at minimum a ``success: bool`` key. """ ... def health_check(self) -> bool: return True