fix: add _stored_to_legacy helper, load_auth compat, and old-format roundtrip test
This commit is contained in:
@@ -102,9 +102,10 @@ auth admin are implemented. The next work is polish, not new broad surfaces.
|
|||||||
- Completed: server startup policy moved to `wf_server.cli`; JSON-RPC HTTP
|
- Completed: server startup policy moved to `wf_server.cli`; JSON-RPC HTTP
|
||||||
remains in `wf_transport_rpc_http`:
|
remains in `wf_transport_rpc_http`:
|
||||||
[`server CLI and transport boundary`](superpowers/specs/2026-06-10-server-cli-transport-boundary.md).
|
[`server CLI and transport boundary`](superpowers/specs/2026-06-10-server-cli-transport-boundary.md).
|
||||||
- Next auth work: typed/discriminated auth records, source-owned auth binders
|
- Next auth work: typed/discriminated auth records and source-owned auth binders
|
||||||
(`McpAuthBinder` first), OAuth refresh-token support, and Google Drive MCP smoke
|
(`McpAuthBinder` first) are now completed. Remaining: OAuth refresh-token
|
||||||
through `https://drivemcp.googleapis.com/mcp/v1`. Production secret manager
|
support and Google Drive MCP smoke through
|
||||||
|
`https://drivemcp.googleapis.com/mcp/v1`. Production secret manager
|
||||||
integration and encrypted-at-rest file format remain deferred.
|
integration and encrypted-at-rest file format remain deferred.
|
||||||
- Active specs:
|
- Active specs:
|
||||||
- [`workflow config targets and sources`](superpowers/specs/2026-06-03-workflow-config-targets-and-sources.md)
|
- [`workflow config targets and sources`](superpowers/specs/2026-06-03-workflow-config-targets-and-sources.md)
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ diagnostics and source registry apply summaries. Slice 3 exposes read-only auth
|
|||||||
admin summaries without secret payload values. Slice 4 adds local/dev file-backed
|
admin summaries without secret payload values. Slice 4 adds local/dev file-backed
|
||||||
auth save/delete through neutral admin, JSON-RPC, and CLI. Responses still
|
auth save/delete through neutral admin, JSON-RPC, and CLI. Responses still
|
||||||
expose only ids, schemes, metadata, and payload keys; secret payload values
|
expose only ids, schemes, metadata, and payload keys; secret payload values
|
||||||
remain write-only.
|
remain write-only. Slice 5 introduces typed stored auth records and MCP auth
|
||||||
|
binding while preserving old `scheme + payload` compatibility input.
|
||||||
|
|
||||||
Next auth work should replace the stringly `scheme + payload` record with typed
|
Next auth work should replace the stringly `scheme + payload` record with typed
|
||||||
auth variants and source-owned auth binders. Google Drive's remote HTTP MCP
|
auth variants and source-owned auth binders. Google Drive's remote HTTP MCP
|
||||||
|
|||||||
@@ -11,13 +11,41 @@ from pathlib import Path
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from wf_api.auth import AuthRecord as NeutralAuthRecord
|
from wf_api.auth import AuthRecord as NeutralAuthRecord
|
||||||
from wf_api.auth import StoredAuthRecord, validate_auth_id
|
from wf_api.auth import (
|
||||||
|
BearerAuth,
|
||||||
|
EnvAuth,
|
||||||
|
HeaderAuth,
|
||||||
|
OAuthRefreshTokenAuth,
|
||||||
|
OpaqueAuth,
|
||||||
|
StoredAuthRecord,
|
||||||
|
validate_auth_id,
|
||||||
|
)
|
||||||
from wf_sources_mcp.auth import AuthRecord, mcp_auth_from_neutral, neutral_auth_from_mcp
|
from wf_sources_mcp.auth import AuthRecord, mcp_auth_from_neutral, neutral_auth_from_mcp
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from wf_sources_mcp.catalog.models import CatalogSnapshot
|
from wf_sources_mcp.catalog.models import CatalogSnapshot
|
||||||
|
|
||||||
|
|
||||||
|
def _stored_to_legacy(record: StoredAuthRecord) -> AuthRecord:
|
||||||
|
"""Convert a typed StoredAuthRecord back to legacy AuthRecord for compat."""
|
||||||
|
auth = record.auth
|
||||||
|
if isinstance(auth, BearerAuth):
|
||||||
|
return AuthRecord(record.id, "bearer", {"token": auth.access_token})
|
||||||
|
if isinstance(auth, HeaderAuth):
|
||||||
|
return AuthRecord(record.id, "headers", {"headers": dict(auth.headers)})
|
||||||
|
if isinstance(auth, EnvAuth):
|
||||||
|
return AuthRecord(record.id, "env", {"env": dict(auth.env)})
|
||||||
|
if isinstance(auth, OAuthRefreshTokenAuth):
|
||||||
|
return AuthRecord(
|
||||||
|
record.id,
|
||||||
|
"oauth_refresh_token",
|
||||||
|
auth.model_dump(mode="json", exclude={"kind"}),
|
||||||
|
)
|
||||||
|
if isinstance(auth, OpaqueAuth):
|
||||||
|
return AuthRecord(record.id, auth.scheme, dict(auth.payload))
|
||||||
|
raise TypeError(f"unsupported auth variant {type(auth).__name__}")
|
||||||
|
|
||||||
|
|
||||||
class AuthStore:
|
class AuthStore:
|
||||||
def save_auth(self, record: AuthRecord) -> None:
|
def save_auth(self, record: AuthRecord) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -95,6 +123,8 @@ class FileAuthStore(AuthStore):
|
|||||||
if not path.exists():
|
if not path.exists():
|
||||||
return None
|
return None
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if "kind" in data.get("auth", {}):
|
||||||
|
return _stored_to_legacy(StoredAuthRecord.model_validate(data))
|
||||||
return AuthRecord(**data)
|
return AuthRecord(**data)
|
||||||
|
|
||||||
def list_auth_refs(self) -> list[str]:
|
def list_auth_refs(self) -> list[str]:
|
||||||
|
|||||||
@@ -127,3 +127,47 @@ def test_file_auth_store_writes_new_stored_auth_record_shape(tmp_path: Path) ->
|
|||||||
assert data["auth"]["kind"] == "bearer"
|
assert data["auth"]["kind"] == "bearer"
|
||||||
assert data["auth"]["access_token"] == "token"
|
assert data["auth"]["access_token"] == "token"
|
||||||
assert data["metadata"]["provider"] == "google"
|
assert data["metadata"]["provider"] == "google"
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_auth_store_loads_old_format_through_load_auth_record(tmp_path: Path) -> None:
|
||||||
|
store = FileAuthStore(tmp_path)
|
||||||
|
path = tmp_path / "auth" / "github.work.json"
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"connection_id": "github.work",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"payload": {"token": "secret"},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
record = store.load_auth_record("github.work")
|
||||||
|
|
||||||
|
assert isinstance(record, NeutralAuthRecord)
|
||||||
|
assert record.id == "github.work"
|
||||||
|
assert record.scheme == "bearer"
|
||||||
|
assert record.payload["token"] == "secret"
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_auth_store_loads_new_format_through_load_auth(tmp_path: Path) -> None:
|
||||||
|
store = FileAuthStore(tmp_path)
|
||||||
|
path = tmp_path / "auth" / "google.drive.personal.json"
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"id": "google.drive.personal",
|
||||||
|
"auth": {"kind": "bearer", "access_token": "token"},
|
||||||
|
"metadata": {"provider": "google"},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
legacy = store.load_auth("google.drive.personal")
|
||||||
|
|
||||||
|
assert legacy is not None
|
||||||
|
assert legacy.connection_id == "google.drive.personal"
|
||||||
|
assert legacy.scheme == "bearer"
|
||||||
|
assert legacy.payload["token"] == "token"
|
||||||
|
|||||||
Reference in New Issue
Block a user