Files
lda-wf/docs/historical/superpowers/plans/2026-06-04-source-registry-mutations.md
T

12 KiB

Source Registry Mutations Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add safe source registry mutation operations so server-owned MCP sources can be added, updated, enabled, disabled, and removed without editing config files by hand.

Architecture: Mutations target only the persisted desired registry, never config files. wf_api stays protocol-neutral by accepting/returning registry entry dictionaries. The MCP provider owns validation through McpSourceRegistryEntry / SourceRegistryFile and persistence through SourceRegistryStore. Runtime hydration still happens through the existing startup/reload merge path.

Tech Stack: Python 3.14, Pydantic v2, wf_api.source_registry_admin, wf_mcp.source_registry, wf_transport_rpc_http, wf_cli, Typer, pytest, ruff, basedpyright.


Scope

Add these operations:

  • add source registry entry
  • update source registry entry
  • enable source registry entry
  • disable source registry entry
  • remove source registry entry

Out of scope:

  • no auth record creation/deletion
  • no catalog snapshot deletion
  • no config file mutation
  • no live transport validation by default
  • no raw proxy remount behavior changes

Semantics

Config Shadowing

Config-defined connections still win over registry entries during startup/reload.

For mutation v1:

  • add rejects a source id that already exists in config to avoid silently adding a registry entry with no runtime effect.
  • update, enable, disable, and remove may operate on existing registry entries even if they are currently shadowed by config.
  • A future allow_shadow flag can relax add; do not add it in this slice.

Rationale: add rejects config-shadowed ids to prevent silent no-ops: adding a registry entry that cannot activate while config owns the same id. Existing shadowed registry entries can still be updated, enabled, disabled, or removed so operators can prepare store state for config removal or future seed ownership policy.

Full-Registry Validation

Every mutation must:

  1. load the current registry
  2. build a new SourceRegistryFile
  3. validate the full file
  4. save one atomic replacement
  5. return the updated entry or removal payload

Never mutate the loaded registry object in-place and then save after partial validation. Build a new list first.

Events

Mutations should emit events if an event recorder is available:

  • source_registry_entry_added
  • source_registry_entry_updated
  • source_registry_entry_enabled
  • source_registry_entry_disabled
  • source_registry_entry_removed

Payload should include at least:

{"source_id": "github.work", "provider": "github", "account": "work"}

If adding event wiring is awkward for this slice, keep mutation payloads correct and document event wiring as future work. Do not block mutation safety on event polish.


Task 1: Extend Neutral API Provider Protocol

  • Update src/wf_api/source_registry_admin.py.
  • Add mutation methods to WorkflowSourceRegistryProvider:
def add_registry_entry(self, entry: Mapping[str, Any]) -> Mapping[str, Any] | object: ...

def update_registry_entry(
    self,
    source_id: str,
    patch: Mapping[str, Any],
) -> Mapping[str, Any] | object: ...

def set_registry_entry_enabled(
    self,
    source_id: str,
    enabled: bool,
) -> Mapping[str, Any] | object: ...

def remove_registry_entry(self, source_id: str) -> Mapping[str, Any] | object: ...
  • Add async API methods:
async def add_registry_entry(self, *, entry: dict[str, Any]) -> dict[str, Any]: ...

async def update_registry_entry(
    self,
    *,
    source_id: str,
    patch: dict[str, Any],
) -> dict[str, Any]: ...

async def enable_registry_entry(self, *, source_id: str) -> dict[str, Any]: ...

async def disable_registry_entry(self, *, source_id: str) -> dict[str, Any]: ...

async def remove_registry_entry(self, *, source_id: str) -> dict[str, Any]: ...
  • Return shapes:
{"entry": <full entry>, "shadowed_by_config": bool}
{"removed": true, "source_id": "..."}
  • Reuse existing _payload() and shadow helper logic.
  • Update WorkflowSourceRegistrySurface in src/wf_api/surface.py.

Tests

  • Extend tests/wf_api/test_source_registry_admin_api.py.
  • Add fake mutable provider.
  • Test each API method delegates and returns normalized payloads.
  • Test remove payload.

Task 2: Implement MCP Mutation Provider

  • Update src/wf_mcp/broker/service/source_registry_admin.py.
  • Keep SourceRegistryAdminProvider as the read/write provider.
  • Add optional event sink if practical:
from collections.abc import Callable
from ...events import McpEvent

event_sink: Callable[[McpEvent], None] | None = None

If this creates too much coupling, skip event sink and document it.

  • Add helpers:
def _load(self) -> SourceRegistryFile: ...
def _save(self, sources: list[McpSourceRegistryEntry]) -> SourceRegistryFile: ...
def _entry_map(self, registry: SourceRegistryFile) -> dict[str, McpSourceRegistryEntry]: ...
def _require_entry(self, source_id: str) -> McpSourceRegistryEntry: ...
  • add_registry_entry(entry):

    • reject if entry["id"] is in config_source_ids()
    • validate with McpSourceRegistryEntry.model_validate(entry)
    • reject duplicate existing registry id
    • save full SourceRegistryFile
    • return added entry
  • update_registry_entry(source_id, patch):

    • require existing registry entry
    • reject changing id in v1 unless it equals source_id
    • merge existing full JSON entry with patch
    • validate with McpSourceRegistryEntry
    • save full file
    • return updated entry
  • set_registry_entry_enabled(source_id, enabled):

    • require existing registry entry
    • update enabled
    • save full file
    • return updated entry
  • remove_registry_entry(source_id):

    • require existing registry entry
    • save full file without the entry
    • return {"removed": True, "source_id": source_id}
    • do not delete auth/catalog

Tests

  • Extend or create tests in tests/wf_mcp/service/test_source_registry_admin.py.
  • Test add persists and round-trips through store.
  • Test add rejects config-shadowed id.
  • Test add rejects duplicate registry id.
  • Test update persists provider/account/transport changes.
  • Test update rejects id change.
  • Test enable/disable persist.
  • Test remove persists absence and does not touch unrelated entries.
  • Test missing source raises clear KeyError.
  • Test malformed payload raises validation error with actionable message.

Task 3: Add RPC Mutation Methods

  • Update src/wf_transport_rpc_http/models.py.
  • Add params:
class AddRegistryEntryParams(RpcParamsModel):
    entry: dict[str, Any]


class UpdateRegistryEntryParams(RpcParamsModel):
    source_id: str = Field(min_length=1)
    patch: dict[str, Any]


class RegistryEntryIdParams(RpcParamsModel):
    source_id: str = Field(min_length=1)
  • Update src/wf_transport_rpc_http/methods_source_registry.py.
  • Register:
workflow.admin.source_registry.add
workflow.admin.source_registry.update
workflow.admin.source_registry.enable
workflow.admin.source_registry.disable
workflow.admin.source_registry.remove
  • Keep unavailable behavior identical to read methods:
{"code": "source_registry_unavailable", ...}
  • Use Params(...) for required params, matching inspect methods.
  • Update src/wf_transport_rpc_http/client_source_registry.py.
  • Add matching client methods.

Tests

  • Extend tests/wf_transport_rpc_http/test_source_registry_rpc.py.
  • Add positive method tests using a fake WorkflowServer with WorkflowSourceRegistryApi.
  • Add unavailable mutation test on local/static server.
  • Add client method tests for method names and payloads.

Task 4: Add CLI Mutation Commands

  • Update src/wf_cli/commands/source_registry.py.
  • Add commands:
wf admin registry add --input '{"id":"github.work",...}'
wf admin registry update SOURCE_ID --patch '{"enabled":false}'
wf admin registry enable SOURCE_ID
wf admin registry disable SOURCE_ID
wf admin registry remove SOURCE_ID
  • Support --input-file for add and --patch-file for update if existing CLI helpers make it easy.
  • JSON inline input is required for v1. Do not design a large flag matrix for every MCP field yet.
  • For remove, require --confirm to avoid accidental deletion:
wf admin registry remove github.work --confirm
  • If source_registry_admin is None, keep the existing "not available" behavior.

Tests

  • Extend tests/wf_cli/test_source_registry.py.
  • Test help includes add/update/enable/disable/remove.
  • Test local/static unavailable for one mutation command.
  • Test remove without --confirm fails.
  • Test commands delegate to a fake context where possible, or use RPC client monkeypatch patterns already used in CLI tests.

Task 5: Wire Real MCP Server Provider If Available

Current state: WorkflowServer.source_registry_admin exists, but local/static servers set it to None. There may still be no concrete MCP-backed WorkflowServer construction path.

  • If a concrete MCP-backed WorkflowServer construction path exists, wire:
WorkflowSourceRegistryApi(
    provider=SourceRegistryAdminProvider(
        source_registry_store=FileSourceRegistryStore(config.store_root),
        config_connections=config.connections,
    )
)
  • If no such path exists, do not invent it in this slice. Keep docs explicit that RPC/CLI mutation methods require a target exposing source_registry_admin.

Task 6: Docs

  • Update docs/current_roadmap.md.
  • Update docs/superpowers/specs/2026-06-03-store-backed-source-registry-design.md.
  • Update docs/superpowers/plans/2026-06-03-source-registry-next-slices.md.
  • Document:
    • registry mutations change desired state only
    • config files are not mutated
    • auth/catalog are not deleted on remove
    • startup/reload is still how runtime hydration sees changes
    • config-shadowed add is rejected in v1

Task 7: Verify

  • Focused API/provider tests:
uv run pytest tests/wf_api/test_source_registry_admin_api.py tests/wf_mcp/service/test_source_registry_admin.py -q
  • RPC/CLI tests:
uv run pytest tests/wf_transport_rpc_http/test_source_registry_rpc.py tests/wf_cli/test_source_registry.py -q
  • Existing registry/startup tests:
uv run pytest tests/wf_api/test_source_registry.py tests/wf_mcp/test_source_registry.py tests/wf_mcp/service/test_connection_service.py tests/wf_mcp/test_broker_server.py -q
  • Quality:
uv run ruff check src/wf_api/source_registry_admin.py src/wf_mcp/broker/service/source_registry_admin.py src/wf_transport_rpc_http src/wf_cli tests/wf_api/test_source_registry_admin_api.py tests/wf_mcp/service/test_source_registry_admin.py tests/wf_transport_rpc_http/test_source_registry_rpc.py tests/wf_cli/test_source_registry.py
uv run basedpyright --level error src/wf_api/source_registry_admin.py src/wf_mcp/broker/service/source_registry_admin.py src/wf_transport_rpc_http src/wf_cli tests/wf_api/test_source_registry_admin_api.py tests/wf_mcp/service/test_source_registry_admin.py tests/wf_transport_rpc_http/test_source_registry_rpc.py tests/wf_cli/test_source_registry.py

Acceptance Criteria

  • Registry add/update/enable/disable/remove work through neutral API methods.
  • MCP provider validates all writes through McpSourceRegistryEntry / SourceRegistryFile.
  • Whole registry is validated before every save.
  • Config files are not mutated.
  • Auth/catalog files are not deleted.
  • Adding an id owned by config is rejected in v1.
  • Existing shadowed registry entries can still be updated or removed.
  • RPC methods and CLI commands exist.
  • Remove requires explicit CLI confirmation.
  • Observed source inventory behavior is unchanged until reload/startup rehydrates runtime state.