third slice: artifacts and deployments...

This commit is contained in:
lda
2026-06-02 01:04:05 +07:00 Verified
parent fb8baf02ea
commit c46c636694
11 changed files with 2004 additions and 388 deletions
@@ -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: