feat: type source registry results

This commit is contained in:
lda
2026-08-01 09:02:56 +07:00 Verified
parent fbcd3a61a5
commit 18b9002d17
9 changed files with 398 additions and 96 deletions
+4 -4
View File
@@ -74,10 +74,10 @@
- Typed-result slices now give `workflow.health`, all artifact, deployment, - Typed-result slices now give `workflow.health`, all artifact, deployment,
and run operations, every persisted draft-workspace operation, and both the and run operations, every persisted draft-workspace operation, and both the
capability and source-discovery surfaces named transport-neutral result capability and source-discovery surfaces named transport-neutral result
schemas: 55 of 70 methods. The remaining 15 success results still collapse schemas: 63 of 70 methods. The remaining seven success results still
to generic objects across the source-registry/admin operations. Continue collapse to generic objects across connections, events, and auth admin
introducing operation result DTOs before adopting generated TypeScript operations. Continue introducing operation result DTOs before adopting
contracts. generated TypeScript contracts.
- The stock `@open-rpc/generator` TypeScript client is not suitable here. It - The stock `@open-rpc/generator` TypeScript client is not suitable here. It
exhausted a 4 GB Node heap on the full contract and emitted invalid dotted exhausted a 4 GB Node heap on the full contract and emitted invalid dotted
class members plus `any` results for a minimal `workflow.health` contract. class members plus `any` results for a minimal `workflow.health` contract.
+16
View File
@@ -79,6 +79,15 @@ from .runs import (
TraceEntryPayload, TraceEntryPayload,
WorkflowRefPayload, WorkflowRefPayload,
) )
from .source_registry import (
ApplyRegistryChangesResult,
InspectRegistryEntryResult,
ListRegistryEntriesResult,
RegistryEntryMutationResult,
RegistryEntryPayload,
RegistryEntrySummaryPayload,
RemoveRegistryEntryResult,
)
from .sources import ( from .sources import (
InspectSourceResult, InspectSourceResult,
ListSourcesResult, ListSourcesResult,
@@ -99,6 +108,7 @@ from .sources import (
) )
__all__ = [ __all__ = [
"ApplyRegistryChangesResult",
"ArtifactVersionPayload", "ArtifactVersionPayload",
"ArtifactCatalogEntryPayload", "ArtifactCatalogEntryPayload",
"ArtifactKindPayload", "ArtifactKindPayload",
@@ -125,6 +135,7 @@ __all__ = [
"InterruptRoutePayload", "InterruptRoutePayload",
"InvalidDraftResult", "InvalidDraftResult",
"InspectCapabilityResult", "InspectCapabilityResult",
"InspectRegistryEntryResult",
"InspectSourceResult", "InspectSourceResult",
"JsonObject", "JsonObject",
"JsonProjector", "JsonProjector",
@@ -134,6 +145,7 @@ __all__ = [
"ListArtifactsResult", "ListArtifactsResult",
"ListCapabilitiesResult", "ListCapabilitiesResult",
"ListRunsResult", "ListRunsResult",
"ListRegistryEntriesResult",
"ListSourcesResult", "ListSourcesResult",
"NextActionPatchExamplePayload", "NextActionPatchExamplePayload",
"NextActionsPayload", "NextActionsPayload",
@@ -145,6 +157,9 @@ __all__ = [
"PatchedDraftInvalidResult", "PatchedDraftInvalidResult",
"PatchedDraftValidResult", "PatchedDraftValidResult",
"RawWorkflowPlan", "RawWorkflowPlan",
"RegistryEntryMutationResult",
"RegistryEntryPayload",
"RegistryEntrySummaryPayload",
"ReducerInventoryPayload", "ReducerInventoryPayload",
"ResumeReadiness", "ResumeReadiness",
"RunResult", "RunResult",
@@ -152,6 +167,7 @@ __all__ = [
"RunSummary", "RunSummary",
"RunTraceResult", "RunTraceResult",
"RequiredCapabilityPayload", "RequiredCapabilityPayload",
"RemoveRegistryEntryResult",
"SaveArtifactResult", "SaveArtifactResult",
"SavedDraftArtifactResult", "SavedDraftArtifactResult",
"SaveDeploymentResult", "SaveDeploymentResult",
+80
View File
@@ -0,0 +1,80 @@
from __future__ import annotations
from typing import NotRequired, TypedDict
from pydantic import ConfigDict, with_config
from .common import DependencyDiagnosticPayload, JsonObject, PageMetadataPayload
@with_config(ConfigDict(extra="allow"))
class RegistryEntryPayload(TypedDict):
"""Desired source entry with provider-specific configuration preserved."""
id: str
kind: str
enabled: bool
provider: NotRequired[str | None]
account: NotRequired[str | None]
profile: NotRequired[str | None]
transport: NotRequired[JsonObject | None]
auth_ref: NotRequired[str | None]
metadata: NotRequired[JsonObject]
class RegistryEntrySummaryPayload(TypedDict):
"""Compact desired-source row including config precedence facts."""
id: str
kind: str
enabled: bool
provider: str | None
account: str | None
profile: str | None
transport_kind: str | None
auth_ref: str | None
shadowed_by_config: bool
config_ownership: str | None
mutable: bool
class ListRegistryEntriesResult(PageMetadataPayload):
"""Cursor-paged desired source registry entries."""
entries: list[RegistryEntrySummaryPayload]
class InspectRegistryEntryResult(TypedDict):
"""Full desired source entry with config precedence facts."""
entry: RegistryEntryPayload
shadowed_by_config: bool
config_ownership: str | None
mutable: bool
class RegistryEntryMutationResult(TypedDict):
"""Desired source entry returned after add, update, or enablement changes."""
entry: RegistryEntryPayload
shadowed_by_config: bool
class RemoveRegistryEntryResult(TypedDict):
"""Outcome of removing one desired source registry entry."""
removed: bool
source_id: str
@with_config(ConfigDict(extra="allow"))
class ApplyRegistryChangesResult(TypedDict):
"""Summary of reconciling desired registry state into the live service."""
applied: bool
registered: list[str]
updated: list[str]
removed: list[str]
connection_count: int
registry_entry_count: int
auth_diagnostics: NotRequired[list[DependencyDiagnosticPayload]]
+60 -40
View File
@@ -6,6 +6,21 @@ from typing import Any, Protocol, runtime_checkable
from wf_platform import page_items from wf_platform import page_items
from .models import (
ApplyRegistryChangesResult,
InspectRegistryEntryResult,
JsonProjector,
ListRegistryEntriesResult,
RegistryEntryMutationResult,
RemoveRegistryEntryResult,
)
_PROJECT_REGISTRY_LIST = JsonProjector(ListRegistryEntriesResult)
_PROJECT_REGISTRY_INSPECT = JsonProjector(InspectRegistryEntryResult)
_PROJECT_REGISTRY_MUTATION = JsonProjector(RegistryEntryMutationResult)
_PROJECT_REGISTRY_REMOVE = JsonProjector(RemoveRegistryEntryResult)
_PROJECT_REGISTRY_APPLY = JsonProjector(ApplyRegistryChangesResult)
class WorkflowSourceRegistryProvider(Protocol): class WorkflowSourceRegistryProvider(Protocol):
"""Provides desired source registry state for read-only admin frontends.""" """Provides desired source registry state for read-only admin frontends."""
@@ -68,7 +83,7 @@ class WorkflowSourceRegistryApi:
*, *,
cursor: str | None = None, cursor: str | None = None,
limit: int = 50, limit: int = 50,
) -> dict[str, Any]: ) -> ListRegistryEntriesResult:
ownership = self._provider.config_source_ownership() ownership = self._provider.config_source_ownership()
entries = sorted( entries = sorted(
( (
@@ -80,104 +95,109 @@ class WorkflowSourceRegistryApi:
key=lambda item: str(item.get("id", "")), key=lambda item: str(item.get("id", "")),
) )
page = page_items(entries, cursor=cursor, limit=limit) page = page_items(entries, cursor=cursor, limit=limit)
return { return _PROJECT_REGISTRY_LIST(
"entries": list(page.items), {
"next_cursor": page.next_cursor, "entries": list(page.items),
"total": page.total, "next_cursor": page.next_cursor,
} "total": page.total,
}
)
async def inspect_registry_entry( async def inspect_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ) -> InspectRegistryEntryResult:
ownership = self._provider.config_source_ownership() ownership = self._provider.config_source_ownership()
for item in self._provider.list_registry_entries(): for item in self._provider.list_registry_entries():
entry = _payload(item) entry = _payload(item)
if entry.get("id") == source_id: if entry.get("id") == source_id:
return { return _PROJECT_REGISTRY_INSPECT(
"entry": entry, {
"shadowed_by_config": self._is_shadowed(source_id), "entry": entry,
"config_ownership": ownership.get(source_id), "shadowed_by_config": self._is_shadowed(source_id),
"mutable": ownership.get(source_id) != "locked", "config_ownership": ownership.get(source_id),
} "mutable": ownership.get(source_id) != "locked",
}
)
raise KeyError(f"unknown registry source {source_id!r}") raise KeyError(f"unknown registry source {source_id!r}")
async def add_registry_entry( async def add_registry_entry(
self, self,
*, *,
entry: dict[str, Any], entry: dict[str, Any],
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
if self._mutation_provider is None: if self._mutation_provider is None:
raise TypeError("add_registry_entry requires a mutation provider") raise TypeError("add_registry_entry requires a mutation provider")
raw = self._mutation_provider.add_registry_entry(entry) raw = self._mutation_provider.add_registry_entry(entry)
result = _payload(raw) result = _payload(raw)
return { return self._mutation_result(result)
"entry": result,
"shadowed_by_config": self._is_shadowed(result["id"]),
}
async def update_registry_entry( async def update_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
patch: dict[str, Any], patch: dict[str, Any],
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
if self._mutation_provider is None: if self._mutation_provider is None:
raise TypeError("update_registry_entry requires a mutation provider") raise TypeError("update_registry_entry requires a mutation provider")
raw = self._mutation_provider.update_registry_entry(source_id, patch) raw = self._mutation_provider.update_registry_entry(source_id, patch)
result = _payload(raw) result = _payload(raw)
return { return self._mutation_result(result)
"entry": result,
"shadowed_by_config": self._is_shadowed(result["id"]),
}
async def enable_registry_entry( async def enable_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
if self._mutation_provider is None: if self._mutation_provider is None:
raise TypeError("enable_registry_entry requires a mutation provider") raise TypeError("enable_registry_entry requires a mutation provider")
raw = self._mutation_provider.set_registry_entry_enabled(source_id, True) raw = self._mutation_provider.set_registry_entry_enabled(source_id, True)
result = _payload(raw) result = _payload(raw)
return { return self._mutation_result(result)
"entry": result,
"shadowed_by_config": self._is_shadowed(result["id"]),
}
async def disable_registry_entry( async def disable_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
if self._mutation_provider is None: if self._mutation_provider is None:
raise TypeError("disable_registry_entry requires a mutation provider") raise TypeError("disable_registry_entry requires a mutation provider")
raw = self._mutation_provider.set_registry_entry_enabled(source_id, False) raw = self._mutation_provider.set_registry_entry_enabled(source_id, False)
result = _payload(raw) result = _payload(raw)
return { return self._mutation_result(result)
"entry": result,
"shadowed_by_config": self._is_shadowed(result["id"]),
}
async def remove_registry_entry( async def remove_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ) -> RemoveRegistryEntryResult:
if self._mutation_provider is None: if self._mutation_provider is None:
raise TypeError("remove_registry_entry requires a mutation provider") raise TypeError("remove_registry_entry requires a mutation provider")
raw = self._mutation_provider.remove_registry_entry(source_id) raw = self._mutation_provider.remove_registry_entry(source_id)
result = _payload(raw) result = _payload(raw)
return { return _PROJECT_REGISTRY_REMOVE(
"removed": bool(result.get("removed")), {
"source_id": str(result.get("source_id", source_id)), "removed": bool(result.get("removed")),
} "source_id": str(result.get("source_id", source_id)),
}
)
async def apply_registry_changes(self) -> dict[str, Any]: async def apply_registry_changes(self) -> ApplyRegistryChangesResult:
if self._apply_provider is None: if self._apply_provider is None:
raise TypeError("apply_registry_changes requires an apply provider") raise TypeError("apply_registry_changes requires an apply provider")
return _payload(self._apply_provider.apply_registry_changes()) return _PROJECT_REGISTRY_APPLY(
_payload(self._apply_provider.apply_registry_changes())
)
def _mutation_result(self, entry: dict[str, Any]) -> RegistryEntryMutationResult:
"""Attach config precedence to one provider-validated mutation entry."""
return _PROJECT_REGISTRY_MUTATION(
{
"entry": entry,
"shadowed_by_config": self._is_shadowed(entry["id"]),
}
)
def _payload(value: Mapping[str, Any] | object) -> dict[str, Any]: def _payload(value: Mapping[str, Any] | object) -> dict[str, Any]:
+13 -8
View File
@@ -10,6 +10,7 @@ from wf_core.models.steps import InputBinding, OutputBinding
from .draft_authoring import RouteSource from .draft_authoring import RouteSource
from .draft_updates import CapabilityStepUpdate from .draft_updates import CapabilityStepUpdate
from .models import ( from .models import (
ApplyRegistryChangesResult,
CapabilityCallResult, CapabilityCallResult,
CompileDraftWorkspaceResult, CompileDraftWorkspaceResult,
CreateArtifactFromWorkspaceResult, CreateArtifactFromWorkspaceResult,
@@ -19,14 +20,18 @@ from .models import (
DeleteDraftWorkspaceResult, DeleteDraftWorkspaceResult,
DraftWorkspaceResult, DraftWorkspaceResult,
InspectCapabilityResult, InspectCapabilityResult,
InspectRegistryEntryResult,
InspectSourceResult, InspectSourceResult,
ListArtifactsResult, ListArtifactsResult,
ListCapabilitiesResult, ListCapabilitiesResult,
ListDeploymentsResult, ListDeploymentsResult,
ListDraftWorkspacesResult, ListDraftWorkspacesResult,
ListRegistryEntriesResult,
ListRunsResult, ListRunsResult,
ListSourcesResult, ListSourcesResult,
PatchDraftResult, PatchDraftResult,
RegistryEntryMutationResult,
RemoveRegistryEntryResult,
RunResult, RunResult,
RunTraceResult, RunTraceResult,
SaveArtifactResult, SaveArtifactResult,
@@ -562,46 +567,46 @@ class WorkflowSourceRegistrySurface(Protocol):
*, *,
cursor: str | None = None, cursor: str | None = None,
limit: int = 50, limit: int = 50,
) -> dict[str, Any]: ... ) -> ListRegistryEntriesResult: ...
async def inspect_registry_entry( async def inspect_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ... ) -> InspectRegistryEntryResult: ...
async def add_registry_entry( async def add_registry_entry(
self, self,
*, *,
entry: dict[str, Any], entry: dict[str, Any],
) -> dict[str, Any]: ... ) -> RegistryEntryMutationResult: ...
async def update_registry_entry( async def update_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
patch: dict[str, Any], patch: dict[str, Any],
) -> dict[str, Any]: ... ) -> RegistryEntryMutationResult: ...
async def enable_registry_entry( async def enable_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ... ) -> RegistryEntryMutationResult: ...
async def disable_registry_entry( async def disable_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ... ) -> RegistryEntryMutationResult: ...
async def remove_registry_entry( async def remove_registry_entry(
self, self,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ... ) -> RemoveRegistryEntryResult: ...
async def apply_registry_changes(self) -> dict[str, Any]: ... async def apply_registry_changes(self) -> ApplyRegistryChangesResult: ...
__all__ = [ __all__ = [
@@ -1,6 +1,14 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any, cast
from wf_api.models import (
ApplyRegistryChangesResult,
InspectRegistryEntryResult,
ListRegistryEntriesResult,
RegistryEntryMutationResult,
RemoveRegistryEntryResult,
)
from .base import RpcCaller from .base import RpcCaller
@@ -13,30 +21,39 @@ class RpcSourceRegistryClientMixin:
*, *,
cursor: str | None = None, cursor: str | None = None,
limit: int = 50, limit: int = 50,
) -> dict[str, Any]: ) -> ListRegistryEntriesResult:
return await self._call( return cast(
"workflow.admin.source_registry.list", ListRegistryEntriesResult,
{"cursor": cursor, "limit": limit}, await self._call(
"workflow.admin.source_registry.list",
{"cursor": cursor, "limit": limit},
),
) )
async def inspect_registry_entry( async def inspect_registry_entry(
self: RpcCaller, self: RpcCaller,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ) -> InspectRegistryEntryResult:
return await self._call( return cast(
"workflow.admin.source_registry.inspect", InspectRegistryEntryResult,
{"source_id": source_id}, await self._call(
"workflow.admin.source_registry.inspect",
{"source_id": source_id},
),
) )
async def add_registry_entry( async def add_registry_entry(
self: RpcCaller, self: RpcCaller,
*, *,
entry: dict[str, Any], entry: dict[str, Any],
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
return await self._call( return cast(
"workflow.admin.source_registry.add", RegistryEntryMutationResult,
{"entry": entry}, await self._call(
"workflow.admin.source_registry.add",
{"entry": entry},
),
) )
async def update_registry_entry( async def update_registry_entry(
@@ -44,44 +61,59 @@ class RpcSourceRegistryClientMixin:
*, *,
source_id: str, source_id: str,
patch: dict[str, Any], patch: dict[str, Any],
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
return await self._call( return cast(
"workflow.admin.source_registry.update", RegistryEntryMutationResult,
{"source_id": source_id, "patch": patch}, await self._call(
"workflow.admin.source_registry.update",
{"source_id": source_id, "patch": patch},
),
) )
async def enable_registry_entry( async def enable_registry_entry(
self: RpcCaller, self: RpcCaller,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
return await self._call( return cast(
"workflow.admin.source_registry.enable", RegistryEntryMutationResult,
{"source_id": source_id}, await self._call(
"workflow.admin.source_registry.enable",
{"source_id": source_id},
),
) )
async def disable_registry_entry( async def disable_registry_entry(
self: RpcCaller, self: RpcCaller,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
return await self._call( return cast(
"workflow.admin.source_registry.disable", RegistryEntryMutationResult,
{"source_id": source_id}, await self._call(
"workflow.admin.source_registry.disable",
{"source_id": source_id},
),
) )
async def remove_registry_entry( async def remove_registry_entry(
self: RpcCaller, self: RpcCaller,
*, *,
source_id: str, source_id: str,
) -> dict[str, Any]: ) -> RemoveRegistryEntryResult:
return await self._call( return cast(
"workflow.admin.source_registry.remove", RemoveRegistryEntryResult,
{"source_id": source_id}, await self._call(
"workflow.admin.source_registry.remove",
{"source_id": source_id},
),
) )
async def apply_registry_changes(self: RpcCaller) -> dict[str, Any]: async def apply_registry_changes(self: RpcCaller) -> ApplyRegistryChangesResult:
return await self._call( return cast(
"workflow.admin.source_registry.apply", ApplyRegistryChangesResult,
{}, await self._call(
"workflow.admin.source_registry.apply",
{},
),
) )
@@ -1,10 +1,19 @@
from __future__ import annotations """Source registry JSON-RPC method registration.
from typing import Any Return annotations stay eagerly evaluated because fastapi-jsonrpc captures them
while registering nested handlers for response validation and OpenRPC output.
"""
import fastapi_jsonrpc as jsonrpc import fastapi_jsonrpc as jsonrpc
from wf_api import WorkflowSourceRegistrySurface from wf_api import WorkflowSourceRegistrySurface
from wf_api.models import (
ApplyRegistryChangesResult,
InspectRegistryEntryResult,
ListRegistryEntriesResult,
RegistryEntryMutationResult,
RemoveRegistryEntryResult,
)
from wf_server import WorkflowServer from wf_server import WorkflowServer
from ..errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
@@ -51,7 +60,7 @@ def register_methods(
) )
async def workflow_admin_source_registry_list( async def workflow_admin_source_registry_list(
params: ListRegistryEntriesParams = RpcParams(), params: ListRegistryEntriesParams = RpcParams(),
) -> dict[str, Any]: ) -> ListRegistryEntriesResult:
admin = _require_source_registry_admin(server, operation="reads") admin = _require_source_registry_admin(server, operation="reads")
try: try:
return await admin.list_registry_entries( return await admin.list_registry_entries(
@@ -67,7 +76,7 @@ def register_methods(
) )
async def workflow_admin_source_registry_inspect( async def workflow_admin_source_registry_inspect(
params: InspectRegistryEntryParams = RpcParams(), params: InspectRegistryEntryParams = RpcParams(),
) -> dict[str, Any]: ) -> InspectRegistryEntryResult:
admin = _require_source_registry_admin(server, operation="reads") admin = _require_source_registry_admin(server, operation="reads")
try: try:
return await admin.inspect_registry_entry( return await admin.inspect_registry_entry(
@@ -82,7 +91,7 @@ def register_methods(
) )
async def workflow_admin_source_registry_add( async def workflow_admin_source_registry_add(
params: AddRegistryEntryParams = RpcParams(), params: AddRegistryEntryParams = RpcParams(),
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
admin = _require_source_registry_admin(server, operation="mutations") admin = _require_source_registry_admin(server, operation="mutations")
try: try:
return await admin.add_registry_entry( return await admin.add_registry_entry(
@@ -97,7 +106,7 @@ def register_methods(
) )
async def workflow_admin_source_registry_update( async def workflow_admin_source_registry_update(
params: UpdateRegistryEntryParams = RpcParams(), params: UpdateRegistryEntryParams = RpcParams(),
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
admin = _require_source_registry_admin(server, operation="mutations") admin = _require_source_registry_admin(server, operation="mutations")
try: try:
return await admin.update_registry_entry( return await admin.update_registry_entry(
@@ -113,7 +122,7 @@ def register_methods(
) )
async def workflow_admin_source_registry_enable( async def workflow_admin_source_registry_enable(
params: RegistryEntryIdParams = RpcParams(), params: RegistryEntryIdParams = RpcParams(),
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
admin = _require_source_registry_admin(server, operation="mutations") admin = _require_source_registry_admin(server, operation="mutations")
try: try:
return await admin.enable_registry_entry( return await admin.enable_registry_entry(
@@ -128,7 +137,7 @@ def register_methods(
) )
async def workflow_admin_source_registry_disable( async def workflow_admin_source_registry_disable(
params: RegistryEntryIdParams = RpcParams(), params: RegistryEntryIdParams = RpcParams(),
) -> dict[str, Any]: ) -> RegistryEntryMutationResult:
admin = _require_source_registry_admin(server, operation="mutations") admin = _require_source_registry_admin(server, operation="mutations")
try: try:
return await admin.disable_registry_entry( return await admin.disable_registry_entry(
@@ -143,7 +152,7 @@ def register_methods(
) )
async def workflow_admin_source_registry_remove( async def workflow_admin_source_registry_remove(
params: RegistryEntryIdParams = RpcParams(), params: RegistryEntryIdParams = RpcParams(),
) -> dict[str, Any]: ) -> RemoveRegistryEntryResult:
admin = _require_source_registry_admin(server, operation="mutations") admin = _require_source_registry_admin(server, operation="mutations")
try: try:
return await admin.remove_registry_entry( return await admin.remove_registry_entry(
@@ -158,7 +167,7 @@ def register_methods(
) )
async def workflow_admin_source_registry_apply( async def workflow_admin_source_registry_apply(
params: ApplyRegistryChangesParams = RpcParams(), params: ApplyRegistryChangesParams = RpcParams(),
) -> dict[str, Any]: ) -> ApplyRegistryChangesResult:
admin = _require_source_registry_admin(server, operation="apply") admin = _require_source_registry_admin(server, operation="apply")
try: try:
return await admin.apply_registry_changes() return await admin.apply_registry_changes()
@@ -169,6 +169,121 @@ def test_openrpc_exposes_typed_source_diagnosis_result(
assert diagnosed["additionalProperties"] is True assert diagnosed["additionalProperties"] is True
def test_openrpc_exposes_typed_source_registry_read_results(
openrpc_document: dict[str, Any],
) -> None:
_assert_result_component(
openrpc_document,
method_name="workflow.admin.source_registry.list",
component_name="ListRegistryEntriesResult",
properties={"entries", "next_cursor", "total"},
)
_assert_result_component(
openrpc_document,
method_name="workflow.admin.source_registry.inspect",
component_name="InspectRegistryEntryResult",
properties={"entry", "shadowed_by_config", "config_ownership", "mutable"},
)
schemas = openrpc_document["components"]["schemas"]
assert schemas["ListRegistryEntriesResult"]["properties"]["entries"]["items"] == {
"$ref": "#/components/schemas/RegistryEntrySummaryPayload"
}
assert schemas["InspectRegistryEntryResult"]["properties"]["entry"] == {
"$ref": "#/components/schemas/RegistryEntryPayload"
}
assert schemas["RegistryEntryPayload"]["additionalProperties"] is True
assert set(schemas["ListRegistryEntriesResult"]["required"]) == {
"entries",
"next_cursor",
"total",
}
assert set(schemas["RegistryEntrySummaryPayload"]["required"]) == {
"id",
"kind",
"enabled",
"provider",
"account",
"profile",
"transport_kind",
"auth_ref",
"shadowed_by_config",
"config_ownership",
"mutable",
}
assert set(schemas["InspectRegistryEntryResult"]["required"]) == {
"entry",
"shadowed_by_config",
"config_ownership",
"mutable",
}
@pytest.mark.parametrize(
"method_name",
[
"workflow.admin.source_registry.add",
"workflow.admin.source_registry.update",
"workflow.admin.source_registry.enable",
"workflow.admin.source_registry.disable",
],
)
def test_openrpc_exposes_typed_source_registry_mutation_result(
openrpc_document: dict[str, Any],
method_name: str,
) -> None:
_assert_result_component(
openrpc_document,
method_name=method_name,
component_name="RegistryEntryMutationResult",
properties={"entry", "shadowed_by_config"},
)
mutation = openrpc_document["components"]["schemas"]["RegistryEntryMutationResult"]
assert set(mutation["required"]) == {"entry", "shadowed_by_config"}
assert mutation["properties"]["entry"] == {
"$ref": "#/components/schemas/RegistryEntryPayload"
}
def test_openrpc_exposes_typed_source_registry_remove_and_apply_results(
openrpc_document: dict[str, Any],
) -> None:
_assert_result_component(
openrpc_document,
method_name="workflow.admin.source_registry.remove",
component_name="RemoveRegistryEntryResult",
properties={"removed", "source_id"},
)
_assert_result_component(
openrpc_document,
method_name="workflow.admin.source_registry.apply",
component_name="ApplyRegistryChangesResult",
properties={
"applied",
"registered",
"updated",
"removed",
"connection_count",
"registry_entry_count",
},
)
schemas = openrpc_document["components"]["schemas"]
assert set(schemas["RemoveRegistryEntryResult"]["required"]) == {
"removed",
"source_id",
}
assert set(schemas["ApplyRegistryChangesResult"]["required"]) == {
"applied",
"registered",
"updated",
"removed",
"connection_count",
"registry_entry_count",
}
assert schemas["ApplyRegistryChangesResult"]["properties"]["auth_diagnostics"][
"items"
] == {"$ref": "#/components/schemas/DependencyDiagnosticPayload"}
@pytest.mark.parametrize( @pytest.mark.parametrize(
("method_name", "component_name", "properties"), ("method_name", "component_name", "properties"),
[ [
@@ -22,6 +22,7 @@ class FakeRegistryEntry:
transport: dict[str, str] | None = None transport: dict[str, str] | None = None
auth_ref: str | None = "github.work" auth_ref: str | None = "github.work"
metadata: dict[str, object] | None = None metadata: dict[str, object] | None = None
provider_options: dict[str, object] | None = None
class FakeRegistryProvider: class FakeRegistryProvider:
@@ -31,6 +32,7 @@ class FakeRegistryProvider:
id="github.work", id="github.work",
transport={"kind": "stdio", "command": "npx"}, transport={"kind": "stdio", "command": "npx"},
metadata={}, metadata={},
provider_options={"region": "work"},
) )
] ]
@@ -152,6 +154,8 @@ async def test_rpc_source_registry_methods_return_registry_payloads(tmp_path) ->
assert listed["result"]["entries"][0]["id"] == "github.work" assert listed["result"]["entries"][0]["id"] == "github.work"
assert listed["result"]["entries"][0]["shadowed_by_config"] is True assert listed["result"]["entries"][0]["shadowed_by_config"] is True
assert inspected["result"]["entry"]["transport"]["kind"] == "stdio" assert inspected["result"]["entry"]["transport"]["kind"] == "stdio"
assert inspected["result"]["entry"]["transport"]["command"] == "npx"
assert inspected["result"]["entry"]["provider_options"] == {"region": "work"}
assert inspected["result"]["shadowed_by_config"] is True assert inspected["result"]["shadowed_by_config"] is True
@@ -246,12 +250,22 @@ async def test_rpc_source_registry_add_returns_entry(tmp_path) -> None:
payload = await _rpc( payload = await _rpc(
client, client,
"workflow.admin.source_registry.add", "workflow.admin.source_registry.add",
{"entry": {"id": "new.mcp", "kind": "mcp", "enabled": True}}, {
"entry": {
"id": "new.mcp",
"kind": "mcp",
"enabled": True,
"transport": {"kind": "stdio", "command": "custom-runner"},
"provider_options": {"region": "local"},
}
},
) )
assert "result" in payload assert "result" in payload
assert payload["result"]["entry"]["id"] == "new.mcp" assert payload["result"]["entry"]["id"] == "new.mcp"
assert payload["result"]["entry"]["kind"] == "mcp" assert payload["result"]["entry"]["kind"] == "mcp"
assert payload["result"]["entry"]["transport"]["command"] == "custom-runner"
assert payload["result"]["entry"]["provider_options"] == {"region": "local"}
async def test_rpc_source_registry_update_returns_entry(tmp_path) -> None: async def test_rpc_source_registry_update_returns_entry(tmp_path) -> None:
@@ -476,6 +490,16 @@ async def test_rpc_source_registry_apply_returns_summary(tmp_path) -> None:
"removed": [], "removed": [],
"connection_count": 1, "connection_count": 1,
"registry_entry_count": 1, "registry_entry_count": 1,
"auth_diagnostics": [
{
"severity": "error",
"code": "auth_not_found",
"logical_ref": "",
"bound_source": "demo.new",
"message": "Missing auth record.",
"repair_hint": "Save the referenced auth record.",
}
],
} }
server = replace( server = replace(
build_local_static_workflow_server(tmp_path / "store"), build_local_static_workflow_server(tmp_path / "store"),
@@ -494,6 +518,7 @@ async def test_rpc_source_registry_apply_returns_summary(tmp_path) -> None:
assert payload["result"]["applied"] is True assert payload["result"]["applied"] is True
assert payload["result"]["registered"] == ["demo.new"] assert payload["result"]["registered"] == ["demo.new"]
assert payload["result"]["auth_diagnostics"][0]["code"] == "auth_not_found"
admin.apply_registry_changes.assert_awaited_once() admin.apply_registry_changes.assert_awaited_once()