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:
addrejects a source id that already exists in config to avoid silently adding a registry entry with no runtime effect.update,enable,disable, andremovemay operate on existing registry entries even if they are currently shadowed by config.- A future
allow_shadowflag can relaxadd; 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:
- load the current registry
- build a new
SourceRegistryFile - validate the full file
- save one atomic replacement
- 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_addedsource_registry_entry_updatedsource_registry_entry_enabledsource_registry_entry_disabledsource_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
WorkflowSourceRegistrySurfaceinsrc/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
SourceRegistryAdminProvideras 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 inconfig_source_ids() - validate with
McpSourceRegistryEntry.model_validate(entry) - reject duplicate existing registry id
- save full
SourceRegistryFile - return added entry
- reject if
-
update_registry_entry(source_id, patch):- require existing registry entry
- reject changing
idin v1 unless it equalssource_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
WorkflowServerwithWorkflowSourceRegistryApi. - 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-filefor add and--patch-filefor 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
--confirmto 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
--confirmfails. - 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
WorkflowServerconstruction 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.