third slice: artifacts and deployments...
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""MCP-adapter-owned live source diagnostics for deployment validation.
|
||||
|
||||
Static deployment validation only checks the last known source catalog.
|
||||
This probe intentionally performs live upstream I/O, so MCP tools keep it
|
||||
disabled by default and only run it when the caller asks for liveness.
|
||||
|
||||
This module owns the MCP-only imports to avoid a circular import:
|
||||
handlers.py -> workflow_operation_context.py -> handlers.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Sequence
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
from mcp.client.streamable_http import StreamableHTTPError
|
||||
from mcp.shared.exceptions import McpError
|
||||
|
||||
from wf_artifacts import DependencyDiagnostic, DiagnosticSeverity, WorkflowArtifact, WorkflowDeployment
|
||||
|
||||
from .adapters import require_adapter
|
||||
from .core import WfMcpService
|
||||
|
||||
LIVE_SOURCE_CHECK_TIMEOUT_SECONDS = 8.0
|
||||
_LIVE_SOURCE_CHECK_FAILURES = (
|
||||
KeyError,
|
||||
TimeoutError,
|
||||
OSError,
|
||||
anyio.ClosedResourceError,
|
||||
anyio.EndOfStream,
|
||||
anyio.BrokenResourceError,
|
||||
httpx.HTTPError,
|
||||
McpError,
|
||||
StreamableHTTPError,
|
||||
)
|
||||
|
||||
|
||||
def _required_live_sources(
|
||||
deployment: WorkflowDeployment,
|
||||
artifacts: Sequence[WorkflowArtifact],
|
||||
) -> dict[str, str]:
|
||||
"""Return concrete upstream source ids to live-check, with one logical ref."""
|
||||
bindings = deployment.binding_map()
|
||||
required: dict[str, str] = {}
|
||||
for artifact in artifacts:
|
||||
for logical_ref, capability in artifact.required_capability_map().items():
|
||||
source_id = bindings.get(capability.logical_source)
|
||||
if source_id is not None:
|
||||
required.setdefault(source_id, logical_ref)
|
||||
return required
|
||||
|
||||
|
||||
async def live_source_diagnostics(
|
||||
service: WfMcpService,
|
||||
*,
|
||||
deployment: WorkflowDeployment,
|
||||
artifacts: Sequence[WorkflowArtifact],
|
||||
) -> list[DependencyDiagnostic]:
|
||||
"""Return opt-in diagnostics for bound upstream sources that cannot answer.
|
||||
|
||||
Static deployment validation only checks the last known source catalog.
|
||||
This probe intentionally performs live upstream I/O, so MCP tools keep it
|
||||
disabled by default and only run it when the caller asks for liveness.
|
||||
"""
|
||||
diagnostics: list[DependencyDiagnostic] = []
|
||||
for source_id, logical_ref in _required_live_sources(deployment, artifacts).items():
|
||||
source = service.capability_sources.get(source_id)
|
||||
if (
|
||||
source is None
|
||||
or not source.enabled
|
||||
or not source.permissions.calls_upstream
|
||||
):
|
||||
continue
|
||||
try:
|
||||
connection = service.connections.get(source_id)
|
||||
adapter = require_adapter(connection, service.adapters)
|
||||
auth = service.load_auth(source_id)
|
||||
await asyncio.wait_for(
|
||||
adapter.list_tools(connection, auth),
|
||||
timeout=LIVE_SOURCE_CHECK_TIMEOUT_SECONDS,
|
||||
)
|
||||
except _LIVE_SOURCE_CHECK_FAILURES as exc:
|
||||
diagnostics.append(
|
||||
DependencyDiagnostic(
|
||||
severity=DiagnosticSeverity.ERROR,
|
||||
code="source_unreachable",
|
||||
logical_ref=logical_ref,
|
||||
bound_source=source_id,
|
||||
message=(
|
||||
f"Live check for upstream source {source_id!r} failed: "
|
||||
f"{type(exc).__name__}: {exc}"
|
||||
),
|
||||
repair_hint=(
|
||||
"Start or reconnect the source, fix its transport/auth "
|
||||
"configuration, or bind this deployment to another source."
|
||||
),
|
||||
)
|
||||
)
|
||||
return diagnostics
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LIVE_SOURCE_CHECK_TIMEOUT_SECONDS",
|
||||
"live_source_diagnostics",
|
||||
]
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from wf_artifacts import DependencyDiagnostic, WorkflowArtifact, WorkflowDeployment
|
||||
from wf_api.operation_context import (
|
||||
WorkflowArtifactCataloger,
|
||||
WorkflowEventRecorder,
|
||||
@@ -11,8 +13,10 @@ from wf_api.operation_context import (
|
||||
WorkflowRuntimeRunner,
|
||||
WorkflowSpecProvider,
|
||||
)
|
||||
from wf_mcp.events import make_event
|
||||
|
||||
from .core import WfMcpService
|
||||
from .workflow_live_checks import live_source_diagnostics
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -24,6 +28,17 @@ class WfMcpWorkflowEventRecorder(WorkflowEventRecorder):
|
||||
def record_event(self, event: Any) -> None:
|
||||
self.service._record_event(event) # noqa: SLF001
|
||||
|
||||
def record_workflow_event(
|
||||
self,
|
||||
event_type: str,
|
||||
*,
|
||||
capability_id: str,
|
||||
payload: dict[str, Any],
|
||||
) -> None:
|
||||
self.service._record_event( # noqa: SLF001
|
||||
make_event(event_type, capability_id=capability_id, payload=payload)
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WfMcpWorkflowSpecProvider(WorkflowSpecProvider):
|
||||
@@ -64,20 +79,21 @@ class WfMcpWorkflowRuntimeRunner(WorkflowRuntimeRunner):
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WfMcpWorkflowLiveSourceChecker(WorkflowLiveSourceChecker):
|
||||
"""Placeholder live source checker while Slice 4A only defines the seam.
|
||||
|
||||
See `docs/superpowers/plans/2026-06-01-wf-api-extraction-roadmap.md`.
|
||||
Real live source checks still live in workflow handlers until a later
|
||||
capability-domain extraction can move them without dragging MCP adapters
|
||||
into `wf_api`.
|
||||
"""
|
||||
"""Adapter-owned live source checker backed by WfMcpService."""
|
||||
|
||||
service: WfMcpService
|
||||
|
||||
async def available_sources(self) -> list[object]:
|
||||
# Existing live source availability logic still lives near handlers.
|
||||
# Slice 4A only creates the seam; it does not move live-check behavior.
|
||||
return []
|
||||
async def deployment_diagnostics(
|
||||
self,
|
||||
*,
|
||||
deployment: WorkflowDeployment,
|
||||
artifacts: Sequence[WorkflowArtifact],
|
||||
) -> list[DependencyDiagnostic]:
|
||||
return await live_source_diagnostics(
|
||||
self.service,
|
||||
deployment=deployment,
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
|
||||
def context_from_service(service: WfMcpService) -> WorkflowOperationContext:
|
||||
|
||||
Reference in New Issue
Block a user