348 lines
10 KiB
Markdown
348 lines
10 KiB
Markdown
# Source Registry Admin Reads 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:** Expose the persisted desired source registry through read-only admin APIs, JSON-RPC, and CLI without confusing it with observed runtime source inventory.
|
|
|
|
**Architecture:** Desired registry state is server/platform configuration. It is not workflow lifecycle state and not observed catalog/source inventory. Add a neutral read-only `WorkflowSourceRegistryApi` over a provider protocol in `wf_api`; implement the provider in `wf_mcp` using `FileSourceRegistryStore` and optional config connection ids for shadow information. Keep mutations out of scope.
|
|
|
|
**Tech Stack:** Python 3.14, Pydantic v2, `wf_api`, `wf_mcp.source_registry`, `wf_transport_rpc_http`, `wf_cli`, Typer, pytest, ruff, basedpyright.
|
|
|
|
---
|
|
|
|
## Naming Decision
|
|
|
|
Use admin/config naming:
|
|
|
|
- JSON-RPC:
|
|
- `workflow.admin.source_registry.list`
|
|
- `workflow.admin.source_registry.inspect`
|
|
- CLI:
|
|
- `wf admin registry list`
|
|
- `wf admin registry inspect SOURCE_ID`
|
|
|
|
Do **not** use `wf source registry ...` in this slice. `wf source list` already means observed/hydrated source inventory. Registry reads are desired server-owned configuration state.
|
|
|
|
---
|
|
|
|
## Payload Shape
|
|
|
|
List payload:
|
|
|
|
```json
|
|
{
|
|
"entries": [
|
|
{
|
|
"id": "github.work",
|
|
"kind": "mcp",
|
|
"enabled": true,
|
|
"provider": "github",
|
|
"account": "work",
|
|
"profile": null,
|
|
"transport_kind": "stdio",
|
|
"auth_ref": "github.work",
|
|
"shadowed_by_config": false
|
|
}
|
|
],
|
|
"next_cursor": null,
|
|
"total": 1
|
|
}
|
|
```
|
|
|
|
Inspect payload:
|
|
|
|
```json
|
|
{
|
|
"entry": {
|
|
"id": "github.work",
|
|
"kind": "mcp",
|
|
"enabled": true,
|
|
"provider": "github",
|
|
"account": "work",
|
|
"profile": null,
|
|
"transport": {"kind": "stdio", "command": "npx", "args": [], "env": {}},
|
|
"auth_ref": "github.work",
|
|
"metadata": {}
|
|
},
|
|
"shadowed_by_config": false
|
|
}
|
|
```
|
|
|
|
Notes:
|
|
|
|
- List returns summaries; inspect returns full entry detail.
|
|
- `shadowed_by_config` is advisory. If a caller has no config provider, return
|
|
`false` rather than guessing.
|
|
- Do not include auth secret payloads. `auth_ref` is only an id/reference.
|
|
|
|
---
|
|
|
|
## Task 1: Add Neutral Source Registry Admin API
|
|
|
|
- [ ] Create `src/wf_api/source_registry_admin.py`.
|
|
- [ ] Define:
|
|
|
|
```python
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping, Sequence, Set
|
|
from dataclasses import asdict, is_dataclass
|
|
from typing import Any, Protocol
|
|
|
|
from wf_platform import page_items
|
|
|
|
|
|
class WorkflowSourceRegistryProvider(Protocol):
|
|
"""Provides desired source registry state for read-only admin frontends."""
|
|
|
|
def list_registry_entries(self) -> Sequence[Mapping[str, Any] | object]: ...
|
|
|
|
def config_source_ids(self) -> Set[str]: ...
|
|
```
|
|
|
|
- [ ] Define `WorkflowSourceRegistryApi` with:
|
|
|
|
```python
|
|
async def list_registry_entries(
|
|
self,
|
|
*,
|
|
cursor: str | None = None,
|
|
limit: int = 50,
|
|
) -> dict[str, Any]: ...
|
|
|
|
async def inspect_registry_entry(self, *, source_id: str) -> dict[str, Any]: ...
|
|
```
|
|
|
|
- [ ] Normalize provider objects using the same style as `wf_api.admin._payload`:
|
|
mapping, dataclass, or Pydantic `model_dump(mode="json")`.
|
|
- [ ] Add helpers:
|
|
|
|
```python
|
|
def _entry_summary(entry: dict[str, Any], shadowed_ids: set[str]) -> dict[str, Any]:
|
|
transport = entry.get("transport")
|
|
transport_kind = transport.get("kind") if isinstance(transport, Mapping) else None
|
|
return {
|
|
"id": entry["id"],
|
|
"kind": entry["kind"],
|
|
"enabled": entry["enabled"],
|
|
"provider": entry.get("provider"),
|
|
"account": entry.get("account"),
|
|
"profile": entry.get("profile"),
|
|
"transport_kind": transport_kind,
|
|
"auth_ref": entry.get("auth_ref"),
|
|
"shadowed_by_config": entry["id"] in shadowed_ids,
|
|
}
|
|
```
|
|
|
|
- [ ] `inspect_registry_entry()` should raise `KeyError(f"unknown registry source {source_id!r}")` when missing.
|
|
- [ ] Export `WorkflowSourceRegistryApi` and `WorkflowSourceRegistryProvider` from `src/wf_api/__init__.py`.
|
|
- [ ] Add `WorkflowSourceRegistrySurface` to `src/wf_api/surface.py` and `__all__`.
|
|
|
|
### Tests
|
|
|
|
- [ ] Create `tests/wf_api/test_source_registry_admin_api.py`.
|
|
- [ ] Test list returns compact summaries in id order.
|
|
- [ ] Test pagination.
|
|
- [ ] Test inspect returns full entry and shadow flag.
|
|
- [ ] Test unknown inspect raises clear `KeyError`.
|
|
- [ ] Test the concrete API satisfies `WorkflowSourceRegistrySurface`.
|
|
|
|
---
|
|
|
|
## Task 2: Add MCP Provider for Registry Reads
|
|
|
|
- [ ] Create `src/wf_mcp/broker/service/source_registry_admin.py`.
|
|
- [ ] Define:
|
|
|
|
```python
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass, field
|
|
|
|
from ...models import ConnectionConfig
|
|
from ...source_registry import SourceRegistryStore
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class SourceRegistryAdminProvider:
|
|
"""Read desired MCP source registry state without mutating it."""
|
|
|
|
source_registry_store: SourceRegistryStore
|
|
config_connections: Sequence[ConnectionConfig] = field(default_factory=tuple)
|
|
|
|
def list_registry_entries(self) -> list[object]:
|
|
return list(self.source_registry_store.load_registry().sources)
|
|
|
|
def config_source_ids(self) -> set[str]:
|
|
return {connection.id for connection in self.config_connections}
|
|
```
|
|
|
|
- [ ] Keep this provider read-only.
|
|
- [ ] Do not load auth records or catalog snapshots here.
|
|
|
|
### Tests
|
|
|
|
- [ ] Create `tests/wf_mcp/service/test_source_registry_admin.py`.
|
|
- [ ] Test provider lists entries from `FileSourceRegistryStore`.
|
|
- [ ] Test provider reports config-shadowed ids.
|
|
|
|
---
|
|
|
|
## Task 3: Wire Server Context
|
|
|
|
- [ ] Update `src/wf_server/context.py`.
|
|
- [ ] Add a nullable `source_registry_admin` field to `WorkflowServer`:
|
|
|
|
```python
|
|
source_registry_admin: WorkflowSourceRegistryApi | None = None
|
|
```
|
|
|
|
- [ ] For the local/static server builder, leave it as `None`. Local/static server has no file-backed MCP source registry.
|
|
- [ ] This field is used by JSON-RPC registration; missing support should return a structured RPC error.
|
|
|
|
### Tests
|
|
|
|
- [ ] Update `tests/wf_server/test_local_static_server.py` if needed to assert local/static construction still works.
|
|
|
|
---
|
|
|
|
## Task 4: Wire MCP/CLI Server Construction
|
|
|
|
- [ ] Identify the current `WorkflowServer` construction path for JSON-RPC HTTP target servers.
|
|
- [ ] When constructing a server from broker config, create:
|
|
|
|
```python
|
|
source_registry_admin = WorkflowSourceRegistryApi(
|
|
SourceRegistryAdminProvider(
|
|
source_registry_store=FileSourceRegistryStore(config.store_root),
|
|
config_connections=config.connections,
|
|
)
|
|
)
|
|
```
|
|
|
|
- [ ] Attach it to `WorkflowServer`.
|
|
- [ ] Do not alter source registry startup merge behavior in this slice.
|
|
|
|
### Tests
|
|
|
|
- [ ] Add or update a JSON-RPC server construction test that seeds `source_registry.json` and asserts `server.source_registry_admin` is not `None`.
|
|
|
|
---
|
|
|
|
## Task 5: Add JSON-RPC Methods and Client Mixin
|
|
|
|
- [ ] Add `src/wf_transport_rpc_http/methods_source_registry.py`.
|
|
- [ ] Register:
|
|
|
|
```python
|
|
workflow.admin.source_registry.list
|
|
workflow.admin.source_registry.inspect
|
|
```
|
|
|
|
- [ ] If `server.source_registry_admin is None`, raise `WorkflowRpcError` with:
|
|
|
|
```json
|
|
{
|
|
"code": "source_registry_unavailable",
|
|
"message": "source registry admin reads are not available for this server"
|
|
}
|
|
```
|
|
|
|
- [ ] Add `src/wf_transport_rpc_http/client_source_registry.py`.
|
|
- [ ] Add `RpcSourceRegistryClientMixin` with:
|
|
|
|
```python
|
|
async def list_registry_entries(self, *, cursor: str | None = None, limit: int = 50) -> dict[str, Any]: ...
|
|
|
|
async def inspect_registry_entry(self, *, source_id: str) -> dict[str, Any]: ...
|
|
```
|
|
|
|
- [ ] Include the mixin in `src/wf_transport_rpc_http/client.py`.
|
|
- [ ] Register methods in `src/wf_transport_rpc_http/app.py`.
|
|
|
|
### Tests
|
|
|
|
- [ ] Add focused JSON-RPC method tests.
|
|
- [ ] Add client tests proving the mixin calls the correct method names.
|
|
- [ ] Add a local/static unavailable test.
|
|
|
|
---
|
|
|
|
## Task 6: Add CLI Commands
|
|
|
|
- [ ] Add `src/wf_cli/commands/source_registry.py`.
|
|
- [ ] Register it under the existing `wf admin` app:
|
|
|
|
```bash
|
|
wf admin registry list
|
|
wf admin registry inspect SOURCE_ID
|
|
```
|
|
|
|
- [ ] Use the target-aware CLI context, not local-only context.
|
|
- [ ] Output JSON by default, following existing CLI command conventions.
|
|
- [ ] Do not add mutation flags.
|
|
|
|
### Tests
|
|
|
|
- [ ] Add CLI tests:
|
|
- local/server target lists entries
|
|
- inspect returns full entry
|
|
- missing entry exits nonzero or returns structured error, matching current CLI patterns
|
|
|
|
---
|
|
|
|
## Task 7: 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`.
|
|
- [ ] Add a short note to CLI docs if there is a current CLI command reference:
|
|
|
|
```md
|
|
`wf admin registry list` shows desired persisted registry entries. `wf source list`
|
|
shows observed/hydrated source inventory. Use both when debugging disabled,
|
|
shadowed, or not-yet-hydrated sources.
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Verify
|
|
|
|
- [ ] Focused tests:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
uv run pytest tests/wf_transport_rpc_http tests/wf_cli -q
|
|
```
|
|
|
|
- [ ] Existing source registry tests:
|
|
|
|
```bash
|
|
uv run pytest tests/wf_api/test_source_registry.py tests/wf_mcp/test_source_registry.py -q
|
|
```
|
|
|
|
- [ ] Quality:
|
|
|
|
```bash
|
|
uv run ruff check src/wf_api src/wf_mcp src/wf_transport_rpc_http src/wf_cli tests/wf_api tests/wf_mcp tests/wf_transport_rpc_http tests/wf_cli
|
|
uv run basedpyright --level error src/wf_api src/wf_mcp src/wf_transport_rpc_http src/wf_cli tests/wf_api tests/wf_mcp tests/wf_transport_rpc_http tests/wf_cli
|
|
```
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
- Desired registry reads are available separately from observed source inventory.
|
|
- List is compact and paginated.
|
|
- Inspect returns full persisted entry details.
|
|
- Shadowed-by-config status is visible.
|
|
- Local/static servers report registry reads unavailable instead of pretending to have an empty registry.
|
|
- No mutation commands are added.
|
|
- `wf source list` behavior is unchanged.
|