fix: harden workflow console contracts
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from wf_cli.context import config_path_from_context
|
||||
from wf_cli.io import emit_json
|
||||
from wf_config import McpSourceConfig, PythonSourceConfig, StdlibSourceConfig
|
||||
from wf_config.loader import load_workflow_config
|
||||
from wf_mcp.broker.config import migrate_broker_config_file
|
||||
from wf_sources_python import load_python_source
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from wf_config import McpSourceConfig, PythonSourceConfig, StdlibSourceConfig
|
||||
|
||||
|
||||
app = typer.Typer(
|
||||
name="config",
|
||||
@@ -31,6 +31,10 @@ def migrate_mcp_config(
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Convert legacy MCP broker config into neutral workflow config."""
|
||||
# Config migration is intentionally command-local; `wf --help` does not
|
||||
# need to load the legacy MCP broker or source loader.
|
||||
from wf_mcp.broker.config import migrate_broker_config_file
|
||||
|
||||
config = migrate_broker_config_file(input_path)
|
||||
payload = config.model_dump(mode="json")
|
||||
if output_path is None:
|
||||
@@ -54,6 +58,8 @@ def validate_config(
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Validate config shape and trusted static source imports."""
|
||||
from wf_config.loader import load_workflow_config
|
||||
|
||||
resolved_config_path = config_path or Path(config_path_from_context(ctx))
|
||||
try:
|
||||
config = load_workflow_config(resolved_config_path)
|
||||
@@ -73,7 +79,11 @@ def _validate_source(
|
||||
source: StdlibSourceConfig | PythonSourceConfig | McpSourceConfig,
|
||||
) -> dict[str, object]:
|
||||
"""Return a compact source validation summary without live network probes."""
|
||||
from wf_config import PythonSourceConfig
|
||||
|
||||
if isinstance(source, PythonSourceConfig):
|
||||
from wf_sources_python import load_python_source
|
||||
|
||||
try:
|
||||
loaded = load_python_source(
|
||||
source_id=source.id,
|
||||
|
||||
+50
-20
@@ -4,30 +4,23 @@ import json
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import typer
|
||||
from pydantic import ValidationError
|
||||
|
||||
from wf_api import (
|
||||
WorkflowAdminApi,
|
||||
WorkflowAdminSurface,
|
||||
WorkflowApi,
|
||||
WorkflowApiSurface,
|
||||
WorkflowSourceAdminApi,
|
||||
WorkflowSourceAdminSurface,
|
||||
WorkflowSourceRegistrySurface,
|
||||
)
|
||||
from wf_config import (
|
||||
FilesystemStoreConfig,
|
||||
LocalTargetConfig,
|
||||
RpcHttpTargetConfig,
|
||||
load_workflow_config,
|
||||
)
|
||||
from wf_mcp.broker import build_service_from_config, load_broker_config
|
||||
from wf_mcp.broker.service import WfMcpService
|
||||
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||
from wf_server.config import build_workflow_server_from_workflow_config
|
||||
from wf_transport_rpc_http import RpcWorkflowApiClient
|
||||
if TYPE_CHECKING:
|
||||
from wf_api import (
|
||||
WorkflowAdminSurface,
|
||||
WorkflowApi,
|
||||
WorkflowApiSurface,
|
||||
WorkflowSourceAdminSurface,
|
||||
WorkflowSourceRegistrySurface,
|
||||
)
|
||||
from wf_config import WorkflowConfigFile
|
||||
from wf_mcp.broker.service import WfMcpService
|
||||
from wf_server.context import WorkflowServer
|
||||
from wf_transport_rpc_http import RpcWorkflowApiClient
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -98,6 +91,15 @@ def config_path_from_context(ctx: typer.Context) -> str:
|
||||
return CliTyperState.from_context(ctx).config_path
|
||||
|
||||
|
||||
def build_workflow_server_from_workflow_config(
|
||||
config: WorkflowConfigFile,
|
||||
) -> WorkflowServer:
|
||||
"""Build the local server without importing the server runtime at CLI startup."""
|
||||
from wf_server.config import build_workflow_server_from_workflow_config as build
|
||||
|
||||
return build(config)
|
||||
|
||||
|
||||
def load_cli_context(
|
||||
config_path: str | Path,
|
||||
*,
|
||||
@@ -112,6 +114,8 @@ def load_cli_context(
|
||||
raise ValueError("--local and --url are mutually exclusive")
|
||||
|
||||
if rpc_url is not None:
|
||||
# Keep transport construction out of `wf --help`; only commands that
|
||||
# contact a remote target need the RPC client and its runtime stack.
|
||||
_validate_rpc_url(rpc_url)
|
||||
client = rpc_client_from_target(
|
||||
url=rpc_url,
|
||||
@@ -131,6 +135,16 @@ def load_cli_context(
|
||||
)
|
||||
|
||||
if _is_legacy_mcp_config(resolved_config_path):
|
||||
from wf_api import (
|
||||
WorkflowAdminApi,
|
||||
WorkflowApi,
|
||||
WorkflowSourceAdminApi,
|
||||
)
|
||||
from wf_mcp.broker import build_service_from_config, load_broker_config
|
||||
from wf_mcp.broker.service.workflow_operation_context import (
|
||||
context_from_service,
|
||||
)
|
||||
|
||||
config = load_broker_config(resolved_config_path)
|
||||
service = build_service_from_config(config)
|
||||
return CliContext(
|
||||
@@ -145,6 +159,14 @@ def load_cli_context(
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
from wf_api import WorkflowApi
|
||||
from wf_config import (
|
||||
FilesystemStoreConfig,
|
||||
LocalTargetConfig,
|
||||
RpcHttpTargetConfig,
|
||||
load_workflow_config,
|
||||
)
|
||||
|
||||
config = load_workflow_config(resolved_config_path)
|
||||
target = config.client.target
|
||||
if force_local or isinstance(target, LocalTargetConfig):
|
||||
@@ -195,6 +217,10 @@ def rpc_client_from_target(
|
||||
transport package.
|
||||
"""
|
||||
|
||||
# Importing the client here keeps HTTP/RPC server dependencies out of the
|
||||
# CLI's command registration and help path.
|
||||
from wf_transport_rpc_http import RpcWorkflowApiClient
|
||||
|
||||
return RpcWorkflowApiClient(url=url, timeout_seconds=timeout_seconds)
|
||||
|
||||
|
||||
@@ -212,6 +238,8 @@ def load_local_cli_context(
|
||||
rpc_url=rpc_url,
|
||||
rpc_timeout_seconds=rpc_timeout_seconds,
|
||||
)
|
||||
from wf_api import WorkflowApi
|
||||
|
||||
if not isinstance(context.handlers, WorkflowApi):
|
||||
raise ValueError(
|
||||
"this CLI command is not available for rpc_http targets yet; "
|
||||
@@ -280,6 +308,8 @@ def _rpc_timeout_from_optional_config(
|
||||
*,
|
||||
override: float | None,
|
||||
) -> float:
|
||||
from wf_config import RpcHttpTargetConfig, load_workflow_config
|
||||
|
||||
if override is not None:
|
||||
return override
|
||||
try:
|
||||
|
||||
@@ -2,13 +2,15 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
from typing import Any, TypeVar
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
|
||||
from wf_cli.context import CliContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import httpx
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -20,6 +22,10 @@ def run_cli_operation(context: CliContext, operation: Coroutine[Any, Any, T]) ->
|
||||
internal failures with a traceback.
|
||||
"""
|
||||
|
||||
# HTTP exceptions only matter after a CLI operation starts. Keep the HTTP
|
||||
# client stack out of command registration and the `wf --help` path.
|
||||
import httpx
|
||||
|
||||
try:
|
||||
return asyncio.run(operation)
|
||||
except (RuntimeError, httpx.HTTPError) as exc:
|
||||
|
||||
Reference in New Issue
Block a user