feat: apply source registry changes
This commit is contained in:
@@ -26,6 +26,7 @@ from .service import WorkflowApi
|
||||
from .source_admin import WorkflowSourceAdminApi
|
||||
from .source_registry_admin import (
|
||||
WorkflowSourceRegistryApi,
|
||||
WorkflowSourceRegistryApplyProvider,
|
||||
WorkflowSourceRegistryMutationProvider,
|
||||
WorkflowSourceRegistryProvider,
|
||||
)
|
||||
@@ -108,6 +109,7 @@ __all__ = [
|
||||
"WorkflowSourceAdminApi",
|
||||
"WorkflowSourceAdminSurface",
|
||||
"WorkflowSourceRegistryApi",
|
||||
"WorkflowSourceRegistryApplyProvider",
|
||||
"WorkflowSourceRegistryMutationProvider",
|
||||
"WorkflowSourceRegistryProvider",
|
||||
"WorkflowSourceRegistrySurface",
|
||||
|
||||
@@ -33,6 +33,13 @@ class WorkflowSourceRegistryMutationProvider(Protocol):
|
||||
def remove_registry_entry(self, source_id: str) -> Mapping[str, Any] | object: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class WorkflowSourceRegistryApplyProvider(Protocol):
|
||||
"""Applies desired registry state to the currently running server."""
|
||||
|
||||
def apply_registry_changes(self) -> Mapping[str, Any] | object: ...
|
||||
|
||||
|
||||
class WorkflowSourceRegistryApi:
|
||||
"""Protocol-neutral desired source registry operations.
|
||||
|
||||
@@ -47,9 +54,11 @@ class WorkflowSourceRegistryApi:
|
||||
*,
|
||||
provider: WorkflowSourceRegistryProvider,
|
||||
mutation_provider: WorkflowSourceRegistryMutationProvider | None = None,
|
||||
apply_provider: WorkflowSourceRegistryApplyProvider | None = None,
|
||||
) -> None:
|
||||
self._provider = provider
|
||||
self._mutation_provider = mutation_provider
|
||||
self._apply_provider = apply_provider
|
||||
|
||||
def _is_shadowed(self, source_id: str) -> bool:
|
||||
return source_id in self._provider.config_source_ids()
|
||||
@@ -165,6 +174,11 @@ class WorkflowSourceRegistryApi:
|
||||
"source_id": str(result.get("source_id", source_id)),
|
||||
}
|
||||
|
||||
async def apply_registry_changes(self) -> dict[str, Any]:
|
||||
if self._apply_provider is None:
|
||||
raise TypeError("apply_registry_changes requires an apply provider")
|
||||
return _payload(self._apply_provider.apply_registry_changes())
|
||||
|
||||
|
||||
def _payload(value: Mapping[str, Any] | object) -> dict[str, Any]:
|
||||
"""Normalize provider objects without depending on MCP registry types."""
|
||||
|
||||
@@ -273,6 +273,8 @@ class WorkflowSourceRegistrySurface(Protocol):
|
||||
source_id: str,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def apply_registry_changes(self) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
__all__ = [
|
||||
"WorkflowAdminSurface",
|
||||
|
||||
@@ -144,6 +144,15 @@ def remove_registry_entry(
|
||||
emit_json(payload)
|
||||
|
||||
|
||||
@app.command("apply")
|
||||
def apply_registry_changes(ctx: typer.Context) -> None:
|
||||
"""Apply desired registry state to the running server."""
|
||||
context = load_cli_context_from_typer(ctx)
|
||||
admin = _require_registry_admin(context)
|
||||
payload = asyncio.run(admin.apply_registry_changes())
|
||||
emit_json(payload)
|
||||
|
||||
|
||||
def _read_json_arg(
|
||||
inline: str | None,
|
||||
file_path: str | None,
|
||||
|
||||
@@ -22,6 +22,7 @@ from .service.source_registry_admin import SourceRegistryAdminProvider
|
||||
from .service.workflow_operation_context import context_from_service
|
||||
from .tools import register_broker_tools
|
||||
from ..models import BrokerConfig
|
||||
from ..sdk.adapter import McpSdkAdapter
|
||||
from ..source_registry import FileSourceRegistryStore, SourceRegistryStore
|
||||
|
||||
|
||||
@@ -70,10 +71,18 @@ def workflow_server_from_service(
|
||||
registry_provider = SourceRegistryAdminProvider(
|
||||
source_registry_store=source_registry_store,
|
||||
config_connections=config.connections,
|
||||
connection_service=service.connection_service,
|
||||
config=config,
|
||||
ensure_adapter=lambda connection: service.register_adapter(
|
||||
connection.server, McpSdkAdapter()
|
||||
)
|
||||
if connection.server not in service.adapters
|
||||
else None,
|
||||
)
|
||||
source_registry_admin = WorkflowSourceRegistryApi(
|
||||
provider=registry_provider,
|
||||
mutation_provider=registry_provider,
|
||||
apply_provider=registry_provider,
|
||||
)
|
||||
stores = WorkflowStores(
|
||||
artifact_store=service.artifact_store,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from wf_api.source_registry_admin import WorkflowSourceRegistryMutationProvider
|
||||
|
||||
from ...models import ConnectionConfig
|
||||
from ...models import BrokerConfig, ConnectionConfig
|
||||
from ...source_registry import (
|
||||
McpSourceRegistryEntry,
|
||||
SourceRegistryFile,
|
||||
SourceRegistryStore,
|
||||
)
|
||||
from .connection_service import ConnectionService
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -24,6 +25,9 @@ class SourceRegistryAdminProvider(WorkflowSourceRegistryMutationProvider):
|
||||
|
||||
source_registry_store: SourceRegistryStore
|
||||
config_connections: Sequence[ConnectionConfig] = field(default_factory=tuple)
|
||||
connection_service: ConnectionService | None = None
|
||||
config: BrokerConfig | None = None
|
||||
ensure_adapter: Callable[[ConnectionConfig], None] | None = None
|
||||
|
||||
# -- read helpers -------------------------------------------------------
|
||||
|
||||
@@ -118,3 +122,41 @@ class SourceRegistryAdminProvider(WorkflowSourceRegistryMutationProvider):
|
||||
sources = [s for s in registry.sources if s.id != source_id]
|
||||
self._save(sources)
|
||||
return {"removed": True, "source_id": source_id}
|
||||
|
||||
def apply_registry_changes(self) -> dict[str, Any]:
|
||||
"""Reconcile desired registry state into the live service connection graph.
|
||||
|
||||
This mirrors config reload reconciliation, but it only applies persisted
|
||||
registry state. It does not mutate config files or remount FastMCP proxy
|
||||
providers.
|
||||
"""
|
||||
if self.connection_service is None or self.config is None:
|
||||
raise RuntimeError("source registry apply requires runtime service context")
|
||||
|
||||
before = {connection.id: connection for connection in self.connection_service.list_all()}
|
||||
self.connection_service.sync_connections_from_config(
|
||||
self.config,
|
||||
source_registry_store=self.source_registry_store,
|
||||
)
|
||||
after = {connection.id: connection for connection in self.connection_service.list_all()}
|
||||
|
||||
if self.ensure_adapter is not None:
|
||||
for connection in after.values():
|
||||
self.ensure_adapter(connection)
|
||||
|
||||
before_ids = set(before)
|
||||
after_ids = set(after)
|
||||
updated = sorted(
|
||||
source_id
|
||||
for source_id in before_ids & after_ids
|
||||
if before[source_id] != after[source_id]
|
||||
)
|
||||
registry = self._load()
|
||||
return {
|
||||
"applied": True,
|
||||
"registered": sorted(after_ids - before_ids),
|
||||
"updated": updated,
|
||||
"removed": sorted(before_ids - after_ids),
|
||||
"connection_count": len(after),
|
||||
"registry_entry_count": len(registry.sources),
|
||||
}
|
||||
|
||||
@@ -79,3 +79,9 @@ class RpcSourceRegistryClientMixin:
|
||||
"workflow.admin.source_registry.remove",
|
||||
{"source_id": source_id},
|
||||
)
|
||||
|
||||
async def apply_registry_changes(self) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.admin.source_registry.apply",
|
||||
{},
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from wf_server import WorkflowServer
|
||||
from .errors import WorkflowRpcError, raise_workflow_rpc_error
|
||||
from .models import (
|
||||
AddRegistryEntryParams,
|
||||
ApplyRegistryChangesParams,
|
||||
InspectRegistryEntryParams,
|
||||
ListRegistryEntriesParams,
|
||||
RegistryEntryIdParams,
|
||||
@@ -149,3 +150,16 @@ def register_methods(
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.admin.source_registry.apply",
|
||||
errors=[WorkflowRpcError],
|
||||
)
|
||||
async def workflow_admin_source_registry_apply(
|
||||
params: ApplyRegistryChangesParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
admin = _require_source_registry_admin(server, operation="apply")
|
||||
try:
|
||||
return await admin.apply_registry_changes()
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError, RuntimeError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@@ -201,3 +201,7 @@ class UpdateRegistryEntryParams(RpcParamsModel):
|
||||
|
||||
class RegistryEntryIdParams(RpcParamsModel):
|
||||
source_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class ApplyRegistryChangesParams(RpcParamsModel):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user