feat: add neutral auth store boundary
This commit is contained in:
@@ -1,929 +0,0 @@
|
||||
# Auth Store Boundary Slice 1 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 a neutral auth record/store boundary and make current MCP auth loading prefer source `auth_ref` while preserving legacy connection-id behavior.
|
||||
|
||||
**Architecture:** `wf_api` owns neutral auth ids, records, and the read-only auth-store protocol. `wf_mcp` keeps its current `AuthRecord` compatibility type, but adds adapter helpers that convert neutral records into MCP runtime records and centralize MCP payload interpretation. No diagnostics, admin mutation, OAuth, or secret-manager behavior in this slice.
|
||||
|
||||
**Tech Stack:** Python 3.14, dataclasses, protocols, pytest, ruff, basedpyright.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
Implement only:
|
||||
|
||||
- neutral auth model/protocol in `wf_api`
|
||||
- MCP file-store adapter bridge
|
||||
- MCP runtime auth resolution by `metadata["auth_ref"]` with legacy fallback
|
||||
- central MCP auth payload interpretation helper
|
||||
|
||||
Do not implement:
|
||||
|
||||
- auth admin CLI/RPC/MCP tools
|
||||
- auth diagnostics in deployment validation
|
||||
- OAuth/browser flows
|
||||
- encrypted file format
|
||||
- source-provider packages beyond current `wf_mcp`
|
||||
|
||||
## Files
|
||||
|
||||
- Create: `src/wf_api/auth.py`
|
||||
- neutral `AUTH_ID_PATTERN`
|
||||
- `validate_auth_id`
|
||||
- frozen `AuthRecord`
|
||||
- read-only `AuthStore` protocol
|
||||
- Modify: `src/wf_api/__init__.py`
|
||||
- export neutral auth symbols
|
||||
- Create: `tests/wf_api/test_auth.py`
|
||||
- neutral auth model/protocol tests
|
||||
- Create: `src/wf_mcp/auth.py`
|
||||
- adapter helpers between neutral auth and `wf_mcp.models.AuthRecord`
|
||||
- centralized MCP header/env extraction
|
||||
- Modify: `src/wf_mcp/storage/store.py`
|
||||
- add neutral `load_auth_record(auth_ref)` / `save_auth_record(record)` methods
|
||||
- keep old `save_auth(AuthRecord)` / `load_auth(connection_id)` methods
|
||||
- Modify: `src/wf_mcp/broker/service/upstream_transport.py`
|
||||
- add `load_connection_auth(connection)` helper
|
||||
- update all `load_auth(connection.id)` call sites to use the helper
|
||||
- keep `load_auth(connection_id)` facade for compatibility
|
||||
- Modify: `src/wf_mcp/broker/service/source_catalog.py`
|
||||
- change `AuthLoader` from `Callable[[str], AuthRecord | None]` to `Callable[[ConnectionConfig], AuthRecord | None]`
|
||||
- update hydrated wrapper calls to pass the connection object
|
||||
- Modify: `src/wf_mcp/broker/service/core.py`
|
||||
- pass `self.upstream.load_connection_auth` to `SourceCatalogService`
|
||||
- Modify: `src/wf_mcp/sdk/adapter.py`
|
||||
- replace local `_auth_headers` / env payload reads with `wf_mcp.auth` helpers
|
||||
- Modify: `src/wf_mcp/runtime/factory.py`
|
||||
- replace local `_auth_headers` / env payload reads with `wf_mcp.auth` helpers
|
||||
- Test: `tests/wf_mcp/test_auth.py`
|
||||
- adapter bridge tests
|
||||
- Test: `tests/wf_mcp/service/test_upstream_transport.py`
|
||||
- `auth_ref` lookup / fallback tests
|
||||
- Test: existing focused suites listed below
|
||||
|
||||
## Task 1: Neutral auth model and store protocol
|
||||
|
||||
**Files:**
|
||||
- Create: `src/wf_api/auth.py`
|
||||
- Modify: `src/wf_api/__init__.py`
|
||||
- Create: `tests/wf_api/test_auth.py`
|
||||
|
||||
- [ ] **Step 1: Write neutral auth tests**
|
||||
|
||||
Create `tests/wf_api/test_auth.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import assert_type
|
||||
|
||||
import pytest
|
||||
|
||||
from wf_api.auth import AuthRecord, AuthStore, validate_auth_id
|
||||
|
||||
|
||||
def test_validate_auth_id_accepts_safe_dotted_ids() -> None:
|
||||
assert validate_auth_id("github.work") == "github.work"
|
||||
assert validate_auth_id("api_ci-1") == "api_ci-1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("auth_id", ["", ".hidden", "../secret", "bad/id"])
|
||||
def test_validate_auth_id_rejects_unsafe_ids(auth_id: str) -> None:
|
||||
with pytest.raises(ValueError, match="auth id must start"):
|
||||
validate_auth_id(auth_id)
|
||||
|
||||
|
||||
def test_auth_record_is_immutable_and_mapping_typed() -> None:
|
||||
record = AuthRecord(
|
||||
id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "secret"},
|
||||
metadata={"owner": "test"},
|
||||
)
|
||||
|
||||
assert record.id == "github.work"
|
||||
assert record.scheme == "bearer"
|
||||
assert record.payload["token"] == "secret"
|
||||
assert_type(record.payload, Mapping[str, object])
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
record.scheme = "headers" # type: ignore[misc]
|
||||
|
||||
|
||||
class MemoryAuthStore:
|
||||
def __init__(self, records: dict[str, AuthRecord]) -> None:
|
||||
self.records = records
|
||||
|
||||
def load_auth(self, auth_ref: str) -> AuthRecord | None:
|
||||
return self.records.get(auth_ref)
|
||||
|
||||
|
||||
def test_auth_store_protocol_is_read_only_lookup() -> None:
|
||||
record = AuthRecord(id="github.work", scheme="opaque", payload={"x": 1})
|
||||
store: AuthStore = MemoryAuthStore({"github.work": record})
|
||||
|
||||
assert store.load_auth("github.work") is record
|
||||
assert store.load_auth("missing") is None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_api/test_auth.py -q
|
||||
```
|
||||
|
||||
Expected: fails because `wf_api.auth` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement neutral auth module**
|
||||
|
||||
Create `src/wf_api/auth.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Protocol
|
||||
|
||||
AUTH_ID_PATTERN = r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$"
|
||||
|
||||
|
||||
def validate_auth_id(value: str) -> str:
|
||||
"""Validate auth refs that are safe as store keys and path segments.
|
||||
|
||||
Auth refs deliberately carry no provider semantics. Source providers decide
|
||||
how a resolved auth record is interpreted.
|
||||
"""
|
||||
|
||||
if not re.fullmatch(AUTH_ID_PATTERN, value):
|
||||
raise ValueError(
|
||||
"auth id must start with alphanumeric or underscore and contain "
|
||||
"only [A-Za-z0-9_.-]"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AuthRecord:
|
||||
"""Neutral credential record resolved by auth ref.
|
||||
|
||||
`scheme + payload` is a compatibility bridge, not the long-term taxonomy.
|
||||
Keep payload interpretation inside provider adapters so a future
|
||||
discriminated union can replace this without touching workflow/config code.
|
||||
"""
|
||||
|
||||
id: str
|
||||
scheme: str
|
||||
payload: Mapping[str, object]
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
validate_auth_id(self.id)
|
||||
if not self.scheme:
|
||||
raise ValueError("auth scheme must be non-empty")
|
||||
|
||||
|
||||
class AuthStore(Protocol):
|
||||
"""Read-only runtime credential lookup by auth ref."""
|
||||
|
||||
def load_auth(self, auth_ref: str) -> AuthRecord | None: ...
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AUTH_ID_PATTERN",
|
||||
"AuthRecord",
|
||||
"AuthStore",
|
||||
"validate_auth_id",
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Export from `wf_api`**
|
||||
|
||||
Modify `src/wf_api/__init__.py`:
|
||||
|
||||
```python
|
||||
from .auth import AUTH_ID_PATTERN, AuthRecord, AuthStore, validate_auth_id
|
||||
```
|
||||
|
||||
Add these entries to `__all__`:
|
||||
|
||||
```python
|
||||
"AUTH_ID_PATTERN",
|
||||
"AuthRecord",
|
||||
"AuthStore",
|
||||
"validate_auth_id",
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_api/test_auth.py tests/wf_api/test_import_direction.py -q
|
||||
uv run ruff check src/wf_api/auth.py tests/wf_api/test_auth.py
|
||||
uv run basedpyright --level error src/wf_api tests/wf_api/test_auth.py
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
## Task 2: MCP auth adapter helpers
|
||||
|
||||
**Files:**
|
||||
- Create: `src/wf_mcp/auth.py`
|
||||
- Create: `tests/wf_mcp/test_auth.py`
|
||||
|
||||
- [ ] **Step 1: Write MCP adapter tests**
|
||||
|
||||
Create `tests/wf_mcp/test_auth.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from wf_api.auth import AuthRecord as NeutralAuthRecord
|
||||
from wf_mcp.auth import (
|
||||
mcp_auth_env,
|
||||
mcp_auth_headers,
|
||||
mcp_auth_from_neutral,
|
||||
neutral_auth_from_mcp,
|
||||
)
|
||||
from wf_mcp.models import AuthRecord as McpAuthRecord
|
||||
|
||||
|
||||
def test_mcp_auth_from_neutral_preserves_scheme_and_payload() -> None:
|
||||
neutral = NeutralAuthRecord(
|
||||
id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "secret"},
|
||||
metadata={"owner": "test"},
|
||||
)
|
||||
|
||||
mcp = mcp_auth_from_neutral(neutral)
|
||||
|
||||
assert mcp == McpAuthRecord(
|
||||
connection_id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "secret"},
|
||||
)
|
||||
|
||||
|
||||
def test_neutral_auth_from_mcp_preserves_payload() -> None:
|
||||
mcp = McpAuthRecord(
|
||||
connection_id="github.work",
|
||||
scheme="headers",
|
||||
payload={"headers": {"X-Test": "yes"}},
|
||||
)
|
||||
|
||||
neutral = neutral_auth_from_mcp(mcp)
|
||||
|
||||
assert neutral.id == "github.work"
|
||||
assert neutral.scheme == "headers"
|
||||
assert neutral.payload == {"headers": {"X-Test": "yes"}}
|
||||
|
||||
|
||||
def test_mcp_auth_headers_extracts_explicit_headers_and_bearer_token() -> None:
|
||||
auth = McpAuthRecord(
|
||||
connection_id="api.work",
|
||||
scheme="bearer",
|
||||
payload={"headers": {"X-Test": "yes"}, "token": "secret"},
|
||||
)
|
||||
|
||||
assert mcp_auth_headers(auth) == {
|
||||
"X-Test": "yes",
|
||||
"Authorization": "Bearer secret",
|
||||
}
|
||||
|
||||
|
||||
def test_mcp_auth_headers_does_not_override_authorization_header() -> None:
|
||||
auth = McpAuthRecord(
|
||||
connection_id="api.work",
|
||||
scheme="bearer",
|
||||
payload={
|
||||
"headers": {"Authorization": "Basic already"},
|
||||
"token": "secret",
|
||||
},
|
||||
)
|
||||
|
||||
assert mcp_auth_headers(auth) == {"Authorization": "Basic already"}
|
||||
|
||||
|
||||
def test_mcp_auth_env_returns_string_map_only() -> None:
|
||||
auth = McpAuthRecord(
|
||||
connection_id="mcp.local",
|
||||
scheme="env",
|
||||
payload={"env": {"TOKEN": "secret", "BAD": 123}},
|
||||
)
|
||||
|
||||
assert mcp_auth_env(auth) == {"TOKEN": "secret"}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/test_auth.py -q
|
||||
```
|
||||
|
||||
Expected: fails because `wf_mcp.auth` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement MCP adapter module**
|
||||
|
||||
Create `src/wf_mcp/auth.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from wf_api.auth import AuthRecord as NeutralAuthRecord
|
||||
|
||||
from .models import AuthRecord as McpAuthRecord
|
||||
|
||||
|
||||
def mcp_auth_from_neutral(record: NeutralAuthRecord) -> McpAuthRecord:
|
||||
"""Adapt neutral auth to the current MCP compatibility record."""
|
||||
|
||||
return McpAuthRecord(
|
||||
connection_id=record.id,
|
||||
scheme=record.scheme,
|
||||
payload=dict(record.payload),
|
||||
)
|
||||
|
||||
|
||||
def neutral_auth_from_mcp(record: McpAuthRecord) -> NeutralAuthRecord:
|
||||
"""Adapt legacy MCP auth into the neutral record shape."""
|
||||
|
||||
return NeutralAuthRecord(
|
||||
id=record.connection_id,
|
||||
scheme=record.scheme,
|
||||
payload=dict(record.payload),
|
||||
)
|
||||
|
||||
|
||||
def mcp_auth_headers(auth: McpAuthRecord | None) -> dict[str, str]:
|
||||
"""Return HTTP headers understood by MCP HTTP transports.
|
||||
|
||||
This is intentionally MCP-specific. Neutral code must not inspect payload
|
||||
keys such as `headers` or `token`.
|
||||
"""
|
||||
|
||||
if auth is None:
|
||||
return {}
|
||||
raw_headers = auth.payload.get("headers", {})
|
||||
headers = {
|
||||
str(key): str(value)
|
||||
for key, value in raw_headers.items()
|
||||
if isinstance(key, str) and isinstance(value, str)
|
||||
} if isinstance(raw_headers, dict) else {}
|
||||
token = auth.payload.get("token")
|
||||
if isinstance(token, str) and "Authorization" not in headers:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
return headers
|
||||
|
||||
|
||||
def mcp_auth_env(auth: McpAuthRecord | None) -> dict[str, str]:
|
||||
"""Return environment variables understood by MCP stdio transports."""
|
||||
|
||||
if auth is None:
|
||||
return {}
|
||||
raw_env = auth.payload.get("env", {})
|
||||
if not isinstance(raw_env, dict):
|
||||
return {}
|
||||
return {
|
||||
str(key): str(value)
|
||||
for key, value in raw_env.items()
|
||||
if isinstance(key, str) and isinstance(value, str)
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"mcp_auth_env",
|
||||
"mcp_auth_from_neutral",
|
||||
"mcp_auth_headers",
|
||||
"neutral_auth_from_mcp",
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/test_auth.py -q
|
||||
uv run ruff check src/wf_mcp/auth.py tests/wf_mcp/test_auth.py
|
||||
uv run basedpyright --level error src/wf_mcp/auth.py tests/wf_mcp/test_auth.py
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
## Task 3: Store adapter bridge
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_mcp/storage/store.py`
|
||||
- Modify: `tests/wf_mcp/test_auth.py`
|
||||
|
||||
- [ ] **Step 1: Add store bridge tests**
|
||||
|
||||
Append to `tests/wf_mcp/test_auth.py`:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
from wf_mcp.storage import FileStore
|
||||
|
||||
|
||||
def test_file_store_saves_and_loads_neutral_auth_record(tmp_path: Path) -> None:
|
||||
store = FileStore(tmp_path)
|
||||
record = NeutralAuthRecord(
|
||||
id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "secret"},
|
||||
metadata={"owner": "test"},
|
||||
)
|
||||
|
||||
store.save_auth_record(record)
|
||||
|
||||
loaded = store.load_auth_record("github.work")
|
||||
assert loaded == record
|
||||
|
||||
|
||||
def test_file_store_legacy_auth_methods_still_work(tmp_path: Path) -> None:
|
||||
store = FileStore(tmp_path)
|
||||
legacy = McpAuthRecord(
|
||||
connection_id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "secret"},
|
||||
)
|
||||
|
||||
store.save_auth(legacy)
|
||||
|
||||
assert store.load_auth("github.work") == legacy
|
||||
assert store.load_auth_record("github.work") == NeutralAuthRecord(
|
||||
id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "secret"},
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/test_auth.py::test_file_store_saves_and_loads_neutral_auth_record tests/wf_mcp/test_auth.py::test_file_store_legacy_auth_methods_still_work -q
|
||||
```
|
||||
|
||||
Expected: fails because `FileStore.save_auth_record` / `load_auth_record` do not exist.
|
||||
|
||||
- [ ] **Step 3: Extend store interface and file store**
|
||||
|
||||
Modify `src/wf_mcp/storage/store.py`:
|
||||
|
||||
Add imports:
|
||||
|
||||
```python
|
||||
from wf_api.auth import AuthRecord as NeutralAuthRecord
|
||||
|
||||
from ..auth import mcp_auth_from_neutral, neutral_auth_from_mcp
|
||||
```
|
||||
|
||||
Add to `class Store`:
|
||||
|
||||
```python
|
||||
def save_auth_record(self, record: NeutralAuthRecord) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
|
||||
raise NotImplementedError
|
||||
```
|
||||
|
||||
Add to `class FileStore`:
|
||||
|
||||
```python
|
||||
def save_auth_record(self, record: NeutralAuthRecord) -> None:
|
||||
"""Save neutral auth through the legacy MCP file shape."""
|
||||
|
||||
self.save_auth(mcp_auth_from_neutral(record))
|
||||
|
||||
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
|
||||
"""Load neutral auth from the legacy MCP file shape."""
|
||||
|
||||
record = self.load_auth(auth_ref)
|
||||
if record is None:
|
||||
return None
|
||||
return neutral_auth_from_mcp(record)
|
||||
```
|
||||
|
||||
Do not change the existing on-disk JSON shape in this task.
|
||||
|
||||
- [ ] **Step 4: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/test_auth.py -q
|
||||
uv run ruff check src/wf_mcp/storage/store.py tests/wf_mcp/test_auth.py
|
||||
uv run basedpyright --level error src/wf_mcp/storage/store.py tests/wf_mcp/test_auth.py
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
## Task 4: Runtime auth resolution by auth_ref with fallback
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_mcp/broker/service/upstream_transport.py`
|
||||
- Modify: `src/wf_mcp/broker/service/source_catalog.py`
|
||||
- Modify: `src/wf_mcp/broker/service/core.py`
|
||||
- Modify: `tests/wf_mcp/service/test_upstream_transport.py`
|
||||
|
||||
- [ ] **Step 1: Add upstream resolution tests**
|
||||
|
||||
Open `tests/wf_mcp/service/test_upstream_transport.py`. Add imports if missing:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
from wf_mcp.models import AuthRecord, ConnectionConfig
|
||||
from wf_mcp.storage import FileStore
|
||||
```
|
||||
|
||||
Add helper if the file does not already have one:
|
||||
|
||||
```python
|
||||
def _transport(root: Path) -> UpstreamTransportService:
|
||||
events: list[object] = []
|
||||
return UpstreamTransportService(
|
||||
store=FileStore(root),
|
||||
event_sink=events.append,
|
||||
)
|
||||
```
|
||||
|
||||
Add tests:
|
||||
|
||||
```python
|
||||
def test_upstream_load_connection_auth_prefers_auth_ref(tmp_path: Path) -> None:
|
||||
service = _transport(tmp_path)
|
||||
service.save_auth(
|
||||
AuthRecord(
|
||||
connection_id="github.creds",
|
||||
scheme="bearer",
|
||||
payload={"token": "secret"},
|
||||
)
|
||||
)
|
||||
service.save_auth(
|
||||
AuthRecord(
|
||||
connection_id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "wrong"},
|
||||
)
|
||||
)
|
||||
connection = ConnectionConfig(
|
||||
id="github.work",
|
||||
server="github",
|
||||
account="work",
|
||||
metadata={"auth_ref": "github.creds"},
|
||||
)
|
||||
|
||||
assert service.load_connection_auth(connection) == AuthRecord(
|
||||
connection_id="github.creds",
|
||||
scheme="bearer",
|
||||
payload={"token": "secret"},
|
||||
)
|
||||
|
||||
|
||||
def test_upstream_load_connection_auth_falls_back_to_connection_id(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = _transport(tmp_path)
|
||||
service.save_auth(
|
||||
AuthRecord(
|
||||
connection_id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "legacy"},
|
||||
)
|
||||
)
|
||||
connection = ConnectionConfig(
|
||||
id="github.work",
|
||||
server="github",
|
||||
account="work",
|
||||
)
|
||||
|
||||
assert service.load_connection_auth(connection) == AuthRecord(
|
||||
connection_id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "legacy"},
|
||||
)
|
||||
|
||||
|
||||
def test_upstream_load_connection_auth_ignores_non_string_auth_ref(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = _transport(tmp_path)
|
||||
service.save_auth(
|
||||
AuthRecord(
|
||||
connection_id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "legacy"},
|
||||
)
|
||||
)
|
||||
connection = ConnectionConfig(
|
||||
id="github.work",
|
||||
server="github",
|
||||
account="work",
|
||||
metadata={"auth_ref": 123},
|
||||
)
|
||||
|
||||
assert service.load_connection_auth(connection) == AuthRecord(
|
||||
connection_id="github.work",
|
||||
scheme="bearer",
|
||||
payload={"token": "legacy"},
|
||||
)
|
||||
```
|
||||
|
||||
If the file already has a helper for `UpstreamTransportService`, use it instead
|
||||
of adding `_transport`.
|
||||
|
||||
- [ ] **Step 2: Run tests to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/service/test_upstream_transport.py -q
|
||||
```
|
||||
|
||||
Expected: fails because `load_connection_auth` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement `load_connection_auth`**
|
||||
|
||||
Modify `src/wf_mcp/broker/service/upstream_transport.py`.
|
||||
|
||||
Add method to `UpstreamTransportService` after `load_auth`:
|
||||
|
||||
```python
|
||||
def load_connection_auth(self, connection: ConnectionConfig) -> AuthRecord | None:
|
||||
"""Resolve auth for a connection, preferring explicit source auth_ref.
|
||||
|
||||
Legacy MCP auth records are keyed by connection id. New source registry
|
||||
and neutral config entries carry `auth_ref`; keep both paths until the
|
||||
old compatibility surface has no callers.
|
||||
"""
|
||||
|
||||
auth_ref = connection.metadata.get("auth_ref")
|
||||
if isinstance(auth_ref, str):
|
||||
return self.load_auth(auth_ref)
|
||||
return self.load_auth(connection.id)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Replace direct connection-id auth lookups in upstream transport**
|
||||
|
||||
In `src/wf_mcp/broker/service/upstream_transport.py`, replace these patterns:
|
||||
|
||||
```python
|
||||
auth = self.load_auth(connection.id)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
auth = self.load_connection_auth(connection)
|
||||
```
|
||||
|
||||
Also replace the live-check lookup:
|
||||
|
||||
```python
|
||||
auth = self.load_auth(source_id)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
auth = self.load_connection_auth(connection)
|
||||
```
|
||||
|
||||
Keep `load_auth(connection_id)` unchanged as a compatibility facade.
|
||||
|
||||
- [ ] **Step 5: Update source catalog auth callback**
|
||||
|
||||
Modify `src/wf_mcp/broker/service/source_catalog.py`.
|
||||
|
||||
Change:
|
||||
|
||||
```python
|
||||
AuthLoader = Callable[[str], AuthRecord | None]
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
AuthLoader = Callable[[ConnectionConfig], AuthRecord | None]
|
||||
```
|
||||
|
||||
Inside `spec_from_snapshot_entry.invoke_tool`, change:
|
||||
|
||||
```python
|
||||
connection = self.connection_lookup(entry.connection_id)
|
||||
auth = self.load_auth(entry.connection_id)
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
connection = self.connection_lookup(entry.connection_id)
|
||||
auth = self.load_auth(connection)
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Wire source catalog to the new resolver**
|
||||
|
||||
Modify `src/wf_mcp/broker/service/core.py`.
|
||||
|
||||
In `SourceCatalogService(...)` construction, change:
|
||||
|
||||
```python
|
||||
load_auth=self.upstream.load_auth,
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
load_auth=self.upstream.load_connection_auth,
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_catalog.py tests/wf_mcp/service/test_workflow_runtime.py -q
|
||||
uv run ruff check src/wf_mcp/broker/service/upstream_transport.py src/wf_mcp/broker/service/source_catalog.py src/wf_mcp/broker/service/core.py tests/wf_mcp/service/test_upstream_transport.py
|
||||
uv run basedpyright --level error src/wf_mcp/broker/service tests/wf_mcp/service/test_upstream_transport.py
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
## Task 5: Centralize MCP payload interpretation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_mcp/sdk/adapter.py`
|
||||
- Modify: `src/wf_mcp/runtime/factory.py`
|
||||
- Test: existing MCP auth/runtime tests
|
||||
|
||||
- [ ] **Step 1: Update SDK adapter imports**
|
||||
|
||||
Modify `src/wf_mcp/sdk/adapter.py`.
|
||||
|
||||
Add import:
|
||||
|
||||
```python
|
||||
from ..auth import mcp_auth_env, mcp_auth_headers
|
||||
```
|
||||
|
||||
Delete local `_auth_headers`.
|
||||
|
||||
In stdio branch, replace:
|
||||
|
||||
```python
|
||||
if auth is not None:
|
||||
auth_env = auth.payload.get("env")
|
||||
if isinstance(auth_env, dict):
|
||||
env = {**(env or {}), **auth_env}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
auth_env = mcp_auth_env(auth)
|
||||
if auth_env:
|
||||
env = {**(env or {}), **auth_env}
|
||||
```
|
||||
|
||||
In HTTP branch, replace:
|
||||
|
||||
```python
|
||||
headers = _auth_headers(auth)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
headers = mcp_auth_headers(auth)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update runtime factory imports**
|
||||
|
||||
Modify `src/wf_mcp/runtime/factory.py`.
|
||||
|
||||
Add import:
|
||||
|
||||
```python
|
||||
from ..auth import mcp_auth_env, mcp_auth_headers
|
||||
```
|
||||
|
||||
Delete local `_auth_headers`.
|
||||
|
||||
In stdio branch, replace:
|
||||
|
||||
```python
|
||||
if auth is not None:
|
||||
auth_env = auth.payload.get("env")
|
||||
if isinstance(auth_env, dict):
|
||||
env = {**(env or {}), **auth_env}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
auth_env = mcp_auth_env(auth)
|
||||
if auth_env:
|
||||
env = {**(env or {}), **auth_env}
|
||||
```
|
||||
|
||||
In HTTP branch, replace:
|
||||
|
||||
```python
|
||||
httpx.AsyncClient(headers=_auth_headers(auth) or None)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
httpx.AsyncClient(headers=mcp_auth_headers(auth) or None)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/test_auth.py tests/wf_mcp/service/test_adapters.py tests/wf_mcp/service/test_workflow_runtime.py tests/wf_mcp/service/test_upstream_transport.py -q
|
||||
uv run ruff check src/wf_mcp/auth.py src/wf_mcp/sdk/adapter.py src/wf_mcp/runtime/factory.py
|
||||
uv run basedpyright --level error src/wf_mcp/auth.py src/wf_mcp/sdk/adapter.py src/wf_mcp/runtime/factory.py
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
## Task 6: Docs and final verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
- Modify: `docs/superpowers/specs/2026-06-06-auth-source-secrets-boundary.md`
|
||||
|
||||
- [ ] **Step 1: Update roadmap status**
|
||||
|
||||
In `docs/current_roadmap.md`, under the auth/source secrets boundary bullet,
|
||||
append:
|
||||
|
||||
```markdown
|
||||
First implementation slice complete: neutral auth records/store protocol
|
||||
exist in `wf_api`, MCP runtime auth resolution prefers explicit `auth_ref`
|
||||
with legacy connection-id fallback, and MCP payload interpretation is
|
||||
isolated in provider-specific adapter helpers.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update spec status**
|
||||
|
||||
In `docs/superpowers/specs/2026-06-06-auth-source-secrets-boundary.md`, add
|
||||
after `## Purpose`:
|
||||
|
||||
```markdown
|
||||
## Status
|
||||
|
||||
Slice 1 implements the neutral auth record/store protocol and MCP compatibility
|
||||
bridge. Diagnostics, auth admin surfaces, and provider-specific auth unions are
|
||||
future slices.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run final verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_api/test_auth.py tests/wf_api/test_import_direction.py tests/wf_mcp/test_auth.py tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_catalog.py tests/wf_mcp/service/test_workflow_runtime.py tests/wf_mcp/service/test_adapters.py -q
|
||||
uv run ruff check src/wf_api src/wf_mcp tests/wf_api/test_auth.py tests/wf_mcp/test_auth.py tests/wf_mcp/service/test_upstream_transport.py
|
||||
uv run basedpyright --level error src/wf_api src/wf_mcp tests/wf_api/test_auth.py tests/wf_mcp/test_auth.py tests/wf_mcp/service/test_upstream_transport.py
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
- [ ] **Step 4: Final report**
|
||||
|
||||
Report:
|
||||
|
||||
- files created/modified
|
||||
- verification outputs
|
||||
- whether any old `auth.payload[...]` reads remain outside `src/wf_mcp/auth.py`
|
||||
- deviations from this plan
|
||||
|
||||
Search command for the payload check:
|
||||
|
||||
```bash
|
||||
rg -n 'auth\.payload|get\("headers"|get\("token"|get\("env"' src/wf_mcp
|
||||
```
|
||||
|
||||
Expected: payload interpretation is either in `src/wf_mcp/auth.py` or unrelated
|
||||
connection/source metadata handling. If adapter/runtime files still read
|
||||
`auth.payload`, fix before reporting.
|
||||
|
||||
@@ -11,6 +11,12 @@ This spec defines the next boundary before expanding auth behavior. The
|
||||
important choice is interface first: file-backed auth is one implementation, not
|
||||
the architecture.
|
||||
|
||||
## Status
|
||||
|
||||
Slice 1 implements the neutral auth record/store protocol and MCP compatibility
|
||||
bridge. Diagnostics, auth admin surfaces, and provider-specific auth unions are
|
||||
future slices.
|
||||
|
||||
## Current State
|
||||
|
||||
Existing MCP runtime auth is connection-id keyed:
|
||||
|
||||
Reference in New Issue
Block a user