feat: add source auth diagnostics

This commit is contained in:
lda
2026-06-13 16:07:24 +07:00 Verified
parent 381d0b1924
commit 3f91c4bca5
17 changed files with 1460 additions and 5 deletions
@@ -0,0 +1,875 @@
# Source Auth Diagnostics 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 source-level auth and health diagnostics so users can tell whether a source is configured, authenticated, catalogued, and safe to call before running `wf cap call`.
**Architecture:** Keep `wf_api` neutral by adding an optional source diagnostics provider to `WorkflowSourceAdminApi`. Implement MCP-specific checks in `wf_mcp.broker.service.source_diagnostics`, wire that provider into MCP-backed `WorkflowServer`, and expose diagnostics over JSON-RPC and CLI via `wf source diagnose`. `source inspect` should include a compact `diagnostics` block when a provider exists, but local/static servers must keep working without MCP imports.
**Tech Stack:** Python 3.14, dataclasses/protocols, Pydantic JSON-RPC params, Typer CLI, pytest-asyncio, existing `AuthRecord`/`StoredAuthRecord` and MCP broker services.
---
## File Structure
- Modify `src/wf_api/source_admin.py`: add `WorkflowSourceDiagnosticsProvider` protocol, optional `diagnostics` provider, `diagnose_source()`, and optional diagnostics in `inspect_source()`.
- Modify `src/wf_api/surface.py`: add `diagnose_source()` to `WorkflowSourceAdminSurface`.
- Create `src/wf_mcp/broker/service/source_diagnostics.py`: MCP-specific diagnostics provider.
- Modify `src/wf_mcp/broker/server.py`: wire diagnostics provider when building MCP-backed server.
- Modify `src/wf_transport_rpc_http/models.py`: add `DiagnoseSourceParams`.
- Modify `src/wf_transport_rpc_http/methods/sources.py`: register `workflow.sources.diagnose`.
- Modify `src/wf_transport_rpc_http/client/sources.py`: add `diagnose_source()`.
- Modify `src/wf_cli/commands/sources.py`: add `wf source diagnose <source_id>`.
- Add/modify tests:
- `tests/wf_api/test_source_admin_api.py`
- `tests/wf_mcp/service/test_source_diagnostics.py`
- `tests/wf_mcp/test_mcp_workflow_server.py`
- `tests/wf_transport_rpc_http/test_app.py`
- `tests/wf_transport_rpc_http/test_client.py`
- `tests/wf_cli/test_remote_target.py`
- Modify docs:
- `docs/wf_cli.md`
- `docs/current_roadmap.md`
---
### Task 1: Neutral Source Diagnostics API
**Files:**
- Modify: `src/wf_api/source_admin.py`
- Modify: `src/wf_api/surface.py`
- Test: `tests/wf_api/test_source_admin_api.py`
- [ ] **Step 1: Write neutral API tests**
Create `tests/wf_api/test_source_admin_api.py` if it does not exist. Add:
```python
from __future__ import annotations
import pytest
from wf_api.operation_context import WorkflowOperationContext
from wf_api.source_admin import WorkflowSourceAdminApi
from wf_api.stores import memory_workflow_stores
from wf_platform import CapabilityBuckets, CapabilitySource
class _Specs:
def __init__(self) -> None:
self.capability_sources = {
"demo.source": CapabilitySource(
id="demo.source",
kind="connection",
capabilities=CapabilityBuckets(),
description="Demo source",
)
}
def get_qualified_spec(self, qualified_name: str):
raise KeyError(qualified_name)
class _Diagnostics:
def __init__(self) -> None:
self.calls: list[str] = []
def diagnose_source(self, source_id: str) -> dict[str, object]:
self.calls.append(source_id)
return {
"source_id": source_id,
"status": "ok",
"auth": {"record_present": True},
"diagnostics": [],
}
def _api(*, diagnostics: object | None = None) -> WorkflowSourceAdminApi:
stores = memory_workflow_stores()
context = WorkflowOperationContext(
artifact_store=stores.artifact_store,
draft_workspace_store=stores.draft_workspace_store,
run_store=stores.run_store,
events=None,
specs=_Specs(),
runtime=None,
live_sources=None,
)
return WorkflowSourceAdminApi(context, diagnostics=diagnostics)
@pytest.mark.asyncio
async def test_inspect_source_includes_optional_diagnostics() -> None:
provider = _Diagnostics()
payload = await _api(diagnostics=provider).inspect_source(
source_id="demo.source"
)
assert payload["id"] == "demo.source"
assert payload["diagnostics"]["source_id"] == "demo.source"
assert provider.calls == ["demo.source"]
@pytest.mark.asyncio
async def test_inspect_source_omits_diagnostics_without_provider() -> None:
payload = await _api().inspect_source(source_id="demo.source")
assert payload["id"] == "demo.source"
assert "diagnostics" not in payload
@pytest.mark.asyncio
async def test_diagnose_source_uses_provider() -> None:
payload = await _api(diagnostics=_Diagnostics()).diagnose_source(
source_id="demo.source"
)
assert payload["status"] == "ok"
assert payload["auth"]["record_present"] is True
@pytest.mark.asyncio
async def test_diagnose_source_without_provider_returns_basic_status() -> None:
payload = await _api().diagnose_source(source_id="demo.source")
assert payload == {
"source_id": "demo.source",
"status": "unknown",
"diagnostics": [],
"message": "No source diagnostics provider is configured.",
}
```
- [ ] **Step 2: Run tests and confirm failure**
Run:
```bash
uv run pytest tests/wf_api/test_source_admin_api.py -q
```
Expected: fails because `WorkflowSourceAdminApi.__init__()` has no `diagnostics` argument and `diagnose_source()` does not exist.
- [ ] **Step 3: Implement neutral protocol and API**
In `src/wf_api/source_admin.py`, add:
```python
from typing import Any, Protocol
```
Then add above `WorkflowSourceAdminApi`:
```python
class WorkflowSourceDiagnosticsProvider(Protocol):
"""Optional source-specific diagnostics provider.
Implementations may know about transport/auth/catalog details. The neutral
API only forwards source ids and serializes returned dictionaries.
"""
def diagnose_source(self, source_id: str) -> dict[str, Any]: ...
```
Change the constructor:
```python
def __init__(
self,
context: WorkflowOperationContext,
*,
diagnostics: WorkflowSourceDiagnosticsProvider | None = None,
) -> None:
self.context = context
self.diagnostics = diagnostics
```
Change `inspect_source()` to:
```python
async def inspect_source(self, *, source_id: str) -> dict[str, Any]:
try:
source = self.context.specs.capability_sources[source_id]
except KeyError as exc:
raise KeyError(f"unknown source {source_id!r}") from exc
payload = source.as_inventory().model_dump(mode="json")
if self.diagnostics is not None:
payload["diagnostics"] = self.diagnostics.diagnose_source(source_id)
return payload
```
Add:
```python
async def diagnose_source(self, *, source_id: str) -> dict[str, Any]:
try:
self.context.specs.capability_sources[source_id]
except KeyError as exc:
raise KeyError(f"unknown source {source_id!r}") from exc
if self.diagnostics is None:
return {
"source_id": source_id,
"status": "unknown",
"diagnostics": [],
"message": "No source diagnostics provider is configured.",
}
return self.diagnostics.diagnose_source(source_id)
```
In `src/wf_api/surface.py`, add to `WorkflowSourceAdminSurface`:
```python
async def diagnose_source(self, *, source_id: str) -> dict[str, Any]: ...
```
- [ ] **Step 4: Run tests and commit**
Run:
```bash
uv run pytest tests/wf_api/test_source_admin_api.py -q
uv run basedpyright --level error src/wf_api/source_admin.py src/wf_api/surface.py tests/wf_api/test_source_admin_api.py
```
Expected: tests pass and typecheck has 0 errors.
Commit:
```bash
git add src/wf_api/source_admin.py src/wf_api/surface.py tests/wf_api/test_source_admin_api.py
git commit -m "feat: add neutral source diagnostics api"
```
---
### Task 2: MCP Source Diagnostics Provider
**Files:**
- Create: `src/wf_mcp/broker/service/source_diagnostics.py`
- Modify: `src/wf_mcp/broker/server.py`
- Test: `tests/wf_mcp/service/test_source_diagnostics.py`
- Test: `tests/wf_mcp/test_mcp_workflow_server.py`
- [ ] **Step 1: Write provider tests**
Create `tests/wf_mcp/service/test_source_diagnostics.py`:
```python
from __future__ import annotations
from wf_api.auth import AuthRecord
from wf_mcp.broker.service.source_diagnostics import SourceDiagnosticsProvider
from wf_mcp.connections import ConnectionRegistry
from wf_mcp.models import ConnectionConfig
from wf_sources_mcp.catalog import CatalogSnapshot
from wf_sources_mcp.storage import FileAuthStore, FileCatalogStore
def _connection(**metadata: object) -> ConnectionConfig:
return ConnectionConfig(
id="demo.personal",
server="demo",
account="personal",
enabled=True,
metadata={"transport": "http", "url": "https://example.test/mcp", **metadata},
)
def _provider(tmp_path, connection: ConnectionConfig) -> SourceDiagnosticsProvider:
registry = ConnectionRegistry()
registry.register(connection)
return SourceDiagnosticsProvider(
connection_lookup=registry.get,
auth_store=FileAuthStore(tmp_path / "auth"),
catalog_store=FileCatalogStore(tmp_path / "catalog"),
)
def test_source_diagnostics_reports_present_auth(tmp_path) -> None:
connection = _connection(auth_ref="demo.creds")
provider = _provider(tmp_path, connection)
provider.auth_store.save_auth(
AuthRecord(
connection_id="demo.creds",
scheme="oauth_refresh_token",
payload={
"client_id": "client",
"client_secret": "secret",
"refresh_token": "refresh",
"token_url": "https://oauth2.example.test/token",
},
)
)
payload = provider.diagnose_source("demo.personal")
assert payload["source_id"] == "demo.personal"
assert payload["status"] == "ok"
assert payload["auth"]["auth_ref"] == "demo.creds"
assert payload["auth"]["record_present"] is True
assert payload["auth"]["scheme"] == "oauth_refresh_token"
assert payload["auth"]["transport_supported"] is True
assert payload["diagnostics"] == []
def test_source_diagnostics_reports_missing_auth(tmp_path) -> None:
payload = _provider(
tmp_path,
_connection(auth_ref="missing.creds"),
).diagnose_source("demo.personal")
assert payload["status"] == "error"
assert payload["auth"]["record_present"] is False
assert payload["diagnostics"][0]["code"] == "auth_not_found"
def test_source_diagnostics_reports_unsupported_transport_auth(tmp_path) -> None:
connection = ConnectionConfig(
id="demo.personal",
server="demo",
account="personal",
metadata={"transport": "stdio", "command": "demo", "auth_ref": "demo.creds"},
)
provider = _provider(tmp_path, connection)
provider.auth_store.save_auth(
AuthRecord(
connection_id="demo.creds",
scheme="oauth_refresh_token",
payload={
"client_id": "client",
"client_secret": "secret",
"refresh_token": "refresh",
"token_url": "https://oauth2.example.test/token",
},
)
)
payload = provider.diagnose_source("demo.personal")
assert payload["status"] == "error"
assert payload["auth"]["transport_supported"] is False
assert payload["diagnostics"][0]["code"] == "auth_scheme_not_supported"
def test_source_diagnostics_reports_catalog_snapshot(tmp_path) -> None:
provider = _provider(tmp_path, _connection())
provider.catalog_store.save_catalog(
CatalogSnapshot(
connection_id="demo.personal",
fetched_at_epoch_ms=123,
max_age_seconds=60,
)
)
payload = provider.diagnose_source("demo.personal")
assert payload["catalog"] == {
"has_snapshot": True,
"fetched_at_epoch_ms": 123,
"max_age_seconds": 60,
"node_count": 0,
"resource_count": 0,
"prompt_count": 0,
}
```
- [ ] **Step 2: Run tests and confirm failure**
Run:
```bash
uv run pytest tests/wf_mcp/service/test_source_diagnostics.py -q
```
Expected: fails because `source_diagnostics.py` does not exist.
- [ ] **Step 3: Implement provider**
Create `src/wf_mcp/broker/service/source_diagnostics.py`:
```python
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from wf_api.auth import AuthRecord
from wf_core import DependencyDiagnostic, DiagnosticSeverity
from wf_sources_mcp.auth import connection_auth_diagnostic
from wf_sources_mcp.storage import AuthStore, CatalogStore
from ...models import ConnectionConfig
ConnectionLookup = Callable[[str], ConnectionConfig]
def _auth_ref(connection: ConnectionConfig) -> str | None:
value = connection.metadata.get("auth_ref")
return value if isinstance(value, str) and value else None
def _transport_kind(connection: ConnectionConfig) -> str | None:
value = connection.metadata.get("transport")
return value if isinstance(value, str) and value else None
def _auth_scheme_supported(
*,
transport_kind: str | None,
auth: AuthRecord | None,
) -> bool:
if auth is None:
return True
if transport_kind == "stdio":
return auth.scheme == "env"
if transport_kind == "http":
return auth.scheme in {"bearer", "headers", "oauth_refresh_token"}
return False
def _unsupported_auth_diagnostic(
*,
source_id: str,
auth_ref: str,
scheme: str,
transport_kind: str | None,
) -> dict[str, Any]:
return DependencyDiagnostic(
severity=DiagnosticSeverity.ERROR,
code="auth_scheme_not_supported",
logical_ref=auth_ref,
bound_source=source_id,
message=(
f"Source {source_id!r} uses {transport_kind or 'unknown'} transport, "
f"but auth record {auth_ref!r} has unsupported scheme {scheme!r}."
),
repair_hint=(
"Use env auth for stdio MCP sources, or bearer/headers/"
"oauth_refresh_token auth for HTTP MCP sources."
),
).model_dump(mode="json")
@dataclass(slots=True)
class SourceDiagnosticsProvider:
"""MCP broker diagnostics for source auth, transport, and catalog state."""
connection_lookup: ConnectionLookup
auth_store: AuthStore
catalog_store: CatalogStore
def diagnose_source(self, source_id: str) -> dict[str, Any]:
connection = self.connection_lookup(source_id)
auth_ref = _auth_ref(connection)
auth = self.auth_store.load_auth(auth_ref) if auth_ref else None
transport_kind = _transport_kind(connection)
snapshot = self.catalog_store.load_catalog(source_id)
diagnostics: list[dict[str, Any]] = []
missing_auth = connection_auth_diagnostic(
connection,
load_auth_ref=self.auth_store.load_auth,
)
if missing_auth is not None:
diagnostics.append(missing_auth.model_dump(mode="json"))
transport_supported = _auth_scheme_supported(
transport_kind=transport_kind,
auth=auth,
)
if auth_ref and auth is not None and not transport_supported:
diagnostics.append(
_unsupported_auth_diagnostic(
source_id=source_id,
auth_ref=auth_ref,
scheme=auth.scheme,
transport_kind=transport_kind,
)
)
return {
"source_id": source_id,
"status": "error" if diagnostics else "ok",
"enabled": connection.enabled,
"transport": {
"kind": transport_kind,
"configured": transport_kind is not None,
},
"auth": {
"auth_ref": auth_ref,
"record_present": auth is not None if auth_ref else None,
"scheme": None if auth is None else auth.scheme,
"transport_supported": transport_supported,
},
"catalog": {
"has_snapshot": snapshot is not None,
"fetched_at_epoch_ms": None
if snapshot is None
else snapshot.fetched_at_epoch_ms,
"max_age_seconds": None if snapshot is None else snapshot.max_age_seconds,
"node_count": 0 if snapshot is None else len(snapshot.nodes),
"resource_count": 0 if snapshot is None else len(snapshot.resources),
"prompt_count": 0 if snapshot is None else len(snapshot.prompts),
},
"diagnostics": diagnostics,
}
```
- [ ] **Step 4: Wire provider into MCP-backed server**
In `src/wf_mcp/broker/server.py`, import:
```python
from .service.source_diagnostics import SourceDiagnosticsProvider
```
Inside `workflow_server_from_service()`, construct:
```python
source_diagnostics = SourceDiagnosticsProvider(
connection_lookup=service.connections.get,
auth_store=service.auth_store,
catalog_store=service.catalog_store,
)
```
Change:
```python
source_admin = WorkflowSourceAdminApi(context)
```
to:
```python
source_admin = WorkflowSourceAdminApi(
context,
diagnostics=source_diagnostics,
)
```
If the function does not currently name `source_admin`, apply the same change where `WorkflowSourceAdminApi` is constructed.
- [ ] **Step 5: Add server wiring test**
In `tests/wf_mcp/test_mcp_workflow_server.py`, add:
```python
async def test_workflow_server_source_admin_reports_mcp_diagnostics(tmp_path) -> None:
service = _service(tmp_path)
payload = await service.workflow_server.source_admin.diagnose_source(
source_id="demo.personal"
)
assert payload["source_id"] == "demo.personal"
assert "auth" in payload
assert "catalog" in payload
```
If the local helper is named differently, use the existing helper that builds a `WfMcpService`/`WorkflowServer` with `demo.personal`; keep the assertions above.
- [ ] **Step 6: Run tests and commit**
Run:
```bash
uv run pytest tests/wf_mcp/service/test_source_diagnostics.py tests/wf_mcp/test_mcp_workflow_server.py -q
uv run basedpyright --level error src/wf_mcp/broker/service/source_diagnostics.py src/wf_mcp/broker/server.py tests/wf_mcp/service/test_source_diagnostics.py tests/wf_mcp/test_mcp_workflow_server.py
```
Expected: tests pass and typecheck has 0 errors.
Commit:
```bash
git add src/wf_mcp/broker/service/source_diagnostics.py src/wf_mcp/broker/server.py tests/wf_mcp/service/test_source_diagnostics.py tests/wf_mcp/test_mcp_workflow_server.py
git commit -m "feat: add mcp source diagnostics provider"
```
---
### Task 3: JSON-RPC Source Diagnose Method
**Files:**
- Modify: `src/wf_transport_rpc_http/models.py`
- Modify: `src/wf_transport_rpc_http/methods/sources.py`
- Modify: `src/wf_transport_rpc_http/client/sources.py`
- Test: `tests/wf_transport_rpc_http/test_app.py`
- Test: `tests/wf_transport_rpc_http/test_client.py`
- [ ] **Step 1: Add RPC tests**
In `tests/wf_transport_rpc_http/test_app.py`, add:
```python
async def test_rpc_diagnoses_source(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path)
client = TestClient(create_app(server))
response = client.post(
"/rpc",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "workflow.sources.diagnose",
"params": {"source_id": "wf.std"},
},
)
payload = response.json()
assert response.status_code == 200
assert payload["result"]["source_id"] == "wf.std"
assert payload["result"]["status"] == "unknown"
```
In `tests/wf_transport_rpc_http/test_client.py`, add:
```python
async def test_rpc_client_diagnoses_source(tmp_path) -> None:
calls: list[tuple[str, dict[str, object]]] = []
class Client(RpcSourceAdminClientMixin):
async def _call(self, method: str, params: dict[str, object]):
calls.append((method, params))
return {"source_id": params["source_id"], "status": "ok"}
payload = await Client().diagnose_source(source_id="demo.personal")
assert payload == {"source_id": "demo.personal", "status": "ok"}
assert calls == [
("workflow.sources.diagnose", {"source_id": "demo.personal"})
]
```
- [ ] **Step 2: Run tests and confirm failure**
Run:
```bash
uv run pytest tests/wf_transport_rpc_http/test_app.py::test_rpc_diagnoses_source tests/wf_transport_rpc_http/test_client.py::test_rpc_client_diagnoses_source -q
```
Expected: fails because model/method/client are missing.
- [ ] **Step 3: Implement RPC model/method/client**
In `src/wf_transport_rpc_http/models.py`, add:
```python
class DiagnoseSourceParams(RpcParamsModel):
source_id: str = Field(min_length=1)
```
In `src/wf_transport_rpc_http/methods/sources.py`, import `DiagnoseSourceParams` and add:
```python
@entrypoint.method(name="workflow.sources.diagnose", errors=[WorkflowRpcError])
async def workflow_sources_diagnose(
params: DiagnoseSourceParams = RpcParams(),
) -> dict[str, Any]:
try:
return await server.source_admin.diagnose_source(
source_id=params.source_id
)
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
raise_workflow_rpc_error(exc)
```
In `src/wf_transport_rpc_http/client/sources.py`, add:
```python
async def diagnose_source(self: RpcCaller, *, source_id: str) -> dict[str, Any]:
return await self._call(
"workflow.sources.diagnose",
{"source_id": source_id},
)
```
- [ ] **Step 4: Run tests and commit**
Run:
```bash
uv run pytest tests/wf_transport_rpc_http/test_app.py::test_rpc_diagnoses_source tests/wf_transport_rpc_http/test_client.py::test_rpc_client_diagnoses_source -q
uv run basedpyright --level error src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/sources.py src/wf_transport_rpc_http/client/sources.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py
```
Expected: tests pass and typecheck has 0 errors.
Commit:
```bash
git add src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/sources.py src/wf_transport_rpc_http/client/sources.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py
git commit -m "feat: expose source diagnostics over rpc"
```
---
### Task 4: CLI Source Diagnose Command
**Files:**
- Modify: `src/wf_cli/commands/sources.py`
- Test: `tests/wf_cli/test_remote_target.py`
- [ ] **Step 1: Write CLI tests**
In `tests/wf_cli/test_remote_target.py`, add:
```python
def test_wf_source_diagnose_uses_rpc_url_override(monkeypatch, tmp_path) -> None:
captured: dict[str, str] = {}
class FakeSourceAdmin:
async def diagnose_source(self, *, source_id: str) -> dict[str, object]:
captured["source_id"] = source_id
return {
"source_id": source_id,
"status": "ok",
"auth": {
"auth_ref": "demo.creds",
"record_present": True,
"scheme": "bearer",
"transport_supported": True,
},
"diagnostics": [],
}
class FakeContext:
source_admin = FakeSourceAdmin()
monkeypatch.setattr(
"wf_cli.commands.sources.load_cli_context_from_typer",
lambda ctx: FakeContext(),
)
result = runner.invoke(
app,
["--url", "http://127.0.0.1:8765/rpc", "source", "diagnose", "demo.personal"],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert captured == {"source_id": "demo.personal"}
assert payload["status"] == "ok"
assert payload["auth"]["scheme"] == "bearer"
```
If the file already has a `runner` or `app` helper, use the existing helper names.
- [ ] **Step 2: Run test and confirm failure**
Run:
```bash
uv run pytest tests/wf_cli/test_remote_target.py::test_wf_source_diagnose_uses_rpc_url_override -q
```
Expected: fails because `wf source diagnose` is not registered.
- [ ] **Step 3: Implement CLI command**
In `src/wf_cli/commands/sources.py`, add:
```python
@app.command("diagnose")
def diagnose_source(
ctx: typer.Context,
source_id: Annotated[str, typer.Argument(help="Workflow source id.")],
) -> None:
"""Diagnose source transport, auth, and catalog state."""
context = load_cli_context_from_typer(ctx)
payload = run_cli_operation(
context,
context.source_admin.diagnose_source(source_id=source_id),
)
emit_json(payload)
```
- [ ] **Step 4: Run tests and commit**
Run:
```bash
uv run pytest tests/wf_cli/test_remote_target.py::test_wf_source_diagnose_uses_rpc_url_override -q
uv run basedpyright --level error src/wf_cli/commands/sources.py tests/wf_cli/test_remote_target.py
```
Expected: tests pass and typecheck has 0 errors.
Commit:
```bash
git add src/wf_cli/commands/sources.py tests/wf_cli/test_remote_target.py
git commit -m "feat: add source diagnose cli"
```
---
### Task 5: Docs And Final Verification
**Files:**
- Modify: `docs/wf_cli.md`
- Modify: `docs/current_roadmap.md`
- [ ] **Step 1: Update CLI docs**
In `docs/wf_cli.md`, add a source diagnostics subsection near the source commands:
```markdown
### Diagnose A Source
Use `wf source diagnose <source_id>` to inspect source health before calling
capabilities:
```bash
wf --config wf.config.json source diagnose gdrive.personal
```
The output reports transport kind, auth reference, whether the auth record
exists, whether the auth scheme is compatible with the transport, catalog
snapshot counts, and non-secret diagnostics. Secret payload values are never
printed.
```
```
- [ ] **Step 2: Update roadmap**
In `docs/current_roadmap.md`, add a completed item under the current source/auth roadmap section:
```markdown
- Completed source auth diagnostics: `wf source diagnose <source_id>` now reports
transport/auth/catalog state without exposing secret payloads.
```
- [ ] **Step 3: Run focused verification**
Run:
```bash
uv run pytest tests/wf_api/test_source_admin_api.py tests/wf_mcp/service/test_source_diagnostics.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_remote_target.py -q
uv run ruff check src/wf_api/source_admin.py src/wf_api/surface.py src/wf_mcp/broker/service/source_diagnostics.py src/wf_mcp/broker/server.py src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/sources.py src/wf_transport_rpc_http/client/sources.py src/wf_cli/commands/sources.py tests/wf_api/test_source_admin_api.py tests/wf_mcp/service/test_source_diagnostics.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_remote_target.py
uv run basedpyright --level error src/wf_api/source_admin.py src/wf_api/surface.py src/wf_mcp/broker/service/source_diagnostics.py src/wf_mcp/broker/server.py src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/sources.py src/wf_transport_rpc_http/client/sources.py src/wf_cli/commands/sources.py tests/wf_api/test_source_admin_api.py tests/wf_mcp/service/test_source_diagnostics.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_remote_target.py
```
Expected:
- Focused tests pass.
- Ruff reports `All checks passed!`.
- Basedpyright reports `0 errors`.
- [ ] **Step 4: Commit docs**
```bash
git add docs/wf_cli.md docs/current_roadmap.md
git commit -m "docs: document source diagnostics"
```
---
## Self-Review
- Spec coverage: plan adds neutral API, MCP diagnostics provider, RPC/client method, CLI command, docs, and tests. It explicitly avoids secret payload output.
- Placeholder scan: no `TBD`, `TODO`, or "similar to" placeholders remain.
- Type consistency: method name is `diagnose_source` across API, surface, RPC client, CLI, and tests. RPC method name is `workflow.sources.diagnose`. Payload uses `diagnostics` for the diagnostic list and `auth`/`catalog` for structured summaries.