27 KiB
Server Store Role Overrides 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 optional role-specific filesystem store overrides to neutral workflow config while preserving server.store as the default for every missing role.
Architecture: wf_config owns the neutral config shape and path resolution. wf_server and MCP bridge composition consume resolved role roots instead of assuming every persistence role uses server.store.root. This slice stays filesystem-only and does not add SQL, object storage, or secret-manager backends.
Tech Stack: Pydantic v2 discriminated models, Typer CLI config loading, existing file stores (wf_api.file_workflow_stores, wf_mcp.storage.FileStore, wf_mcp.source_registry.FileSourceRegistryStore), pytest, ruff, basedpyright.
File Map
src/wf_config/models.py: addServerStoresConfigwith optional role-specific store overrides.src/wf_config/loader.py: resolve relative paths forserver.storeand every configured role store.src/wf_config/__init__.py: exportServerStoresConfig.src/wf_server/config.py: add helper for local/static workflow store root selection.src/wf_mcp/broker/config.py: consume role-specific roots when building MCP-backed services from neutral config.src/wf_mcp/broker/models.py: add runtimeBrokerStoreRootsso the legacy bridge can carry separate roots internally.src/wf_transport_rpc_http/cli.py: preserve--store-rootbehavior and avoid accidental role-store bypass for MCP configs.src/wf_cli/context.py: local--localtarget should usestores.workflowwhen provided.tests/wf_config/test_config_models.py: config parsing and path-resolution tests.tests/wf_mcp/test_workflow_config_bridge.py: neutral config to broker runtime store-root tests.tests/wf_transport_rpc_http/test_cli.py: server CLI behavior with role-store config.tests/wf_cli/test_context.py: local CLI store-root selection test.docs/current_roadmap.md,docs/superpowers/specs/2026-06-03-workflow-config-targets-and-sources.md,docs/wf_cli.md: mark first implementation slice complete.
Role Names
Use these exact field names in config:
workflow
auth
source_registry
catalog_cache
Resolution rule:
effective_store(role) = server.stores.<role> if present else server.store
Use catalog_cache in config even though the current MCP compatibility FileStore stores auth and catalog in one class. The implementation may pass the same root to FileStore for auth and catalog_cache until the underlying store classes split.
Task 1: Add Store Role Config Model
Files:
-
Modify:
src/wf_config/models.py -
Modify:
src/wf_config/__init__.py -
Test:
tests/wf_config/test_config_models.py -
Step 1: Add failing config parse test
Append this test to tests/wf_config/test_config_models.py:
def test_workflow_config_parses_role_specific_store_overrides() -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": ".wf_store"},
"stores": {
"workflow": {"kind": "filesystem", "root": ".wf_workflow"},
"auth": {"kind": "filesystem", "root": ".wf_auth"},
"source_registry": {
"kind": "filesystem",
"root": ".wf_sources",
},
"catalog_cache": {
"kind": "filesystem",
"root": ".wf_catalog",
},
},
},
}
)
assert isinstance(config.server.stores.workflow, FilesystemStoreConfig)
assert config.server.stores.workflow.root.as_posix() == ".wf_workflow"
assert isinstance(config.server.stores.auth, FilesystemStoreConfig)
assert config.server.stores.auth.root.as_posix() == ".wf_auth"
assert isinstance(config.server.stores.source_registry, FilesystemStoreConfig)
assert config.server.stores.source_registry.root.as_posix() == ".wf_sources"
assert isinstance(config.server.stores.catalog_cache, FilesystemStoreConfig)
assert config.server.stores.catalog_cache.root.as_posix() == ".wf_catalog"
- Step 2: Run the failing test
Run:
uv run pytest tests/wf_config/test_config_models.py::test_workflow_config_parses_role_specific_store_overrides -q
Expected: fail because ServerConfig rejects unknown field stores.
- Step 3: Implement
ServerStoresConfig
In src/wf_config/models.py, after StoreConfig, add:
class ServerStoresConfig(WorkflowConfigModel):
"""Optional role-specific store overrides.
Missing roles fall back to `ServerConfig.store`. Keep this config role-based
so future backends can split workflow records, auth, desired sources, and
cache storage independently.
"""
workflow: StoreConfig | None = None
auth: StoreConfig | None = None
source_registry: StoreConfig | None = None
catalog_cache: StoreConfig | None = None
In ServerConfig, add:
stores: ServerStoresConfig = Field(default_factory=ServerStoresConfig)
- Step 4: Export the new model
In src/wf_config/__init__.py, import ServerStoresConfig from .models and add it to __all__.
- Step 5: Verify task tests
Run:
uv run pytest tests/wf_config/test_config_models.py::test_workflow_config_parses_role_specific_store_overrides -q
Expected: pass.
Task 2: Resolve Role Store Paths Relative To Config File
Files:
-
Modify:
src/wf_config/loader.py -
Test:
tests/wf_config/test_config_models.py -
Step 1: Add failing path resolution test
Append this test to tests/wf_config/test_config_models.py:
def test_load_workflow_config_resolves_role_store_paths_relative_to_config(
tmp_path: Path,
) -> None:
config_path = tmp_path / "nested" / "wf.json"
config_path.parent.mkdir()
config_path.write_text(
json.dumps(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": ".default_store"},
"stores": {
"workflow": {
"kind": "filesystem",
"root": ".workflow_store",
},
"auth": {
"kind": "filesystem",
"root": ".auth_store",
},
"source_registry": {
"kind": "filesystem",
"root": ".source_store",
},
"catalog_cache": {
"kind": "filesystem",
"root": ".catalog_store",
},
},
},
}
),
encoding="utf-8",
)
config = load_workflow_config(config_path)
assert config.server.store.root == (
config_path.parent / ".default_store"
).resolve()
assert config.server.stores.workflow is not None
assert config.server.stores.workflow.root == (
config_path.parent / ".workflow_store"
).resolve()
assert config.server.stores.auth is not None
assert config.server.stores.auth.root == (
config_path.parent / ".auth_store"
).resolve()
assert config.server.stores.source_registry is not None
assert config.server.stores.source_registry.root == (
config_path.parent / ".source_store"
).resolve()
assert config.server.stores.catalog_cache is not None
assert config.server.stores.catalog_cache.root == (
config_path.parent / ".catalog_store"
).resolve()
- Step 2: Run the failing test
Run:
uv run pytest tests/wf_config/test_config_models.py::test_load_workflow_config_resolves_role_store_paths_relative_to_config -q
Expected: fail because role store paths are not resolved yet.
- Step 3: Add path resolver helpers
Replace src/wf_config/loader.py with this implementation:
from __future__ import annotations
import json
from pathlib import Path
from .models import (
FilesystemStoreConfig,
ServerStoresConfig,
StoreConfig,
WorkflowConfigFile,
)
def load_workflow_config(path: str | Path) -> WorkflowConfigFile:
"""Load neutral workflow config and resolve local filesystem paths.
Relative filesystem store roots are config-file relative so `wf --config`
behaves the same regardless of the caller's current working directory.
Role-specific store overrides follow the same rule.
"""
config_path = Path(path)
data = json.loads(config_path.read_text(encoding="utf-8"))
config = WorkflowConfigFile.model_validate(data)
return _resolve_store_paths(config, base_dir=config_path.parent)
def _resolve_store_paths(
config: WorkflowConfigFile,
*,
base_dir: Path,
) -> WorkflowConfigFile:
server = config.server
resolved_stores = ServerStoresConfig(
workflow=_resolve_store(server.stores.workflow, base_dir=base_dir),
auth=_resolve_store(server.stores.auth, base_dir=base_dir),
source_registry=_resolve_store(
server.stores.source_registry,
base_dir=base_dir,
),
catalog_cache=_resolve_store(
server.stores.catalog_cache,
base_dir=base_dir,
),
)
return config.model_copy(
update={
"server": server.model_copy(
update={
"store": _resolve_store(server.store, base_dir=base_dir),
"stores": resolved_stores,
}
)
}
)
def _resolve_store(
store: StoreConfig | None,
*,
base_dir: Path,
) -> StoreConfig | None:
if isinstance(store, FilesystemStoreConfig) and not store.root.is_absolute():
return store.model_copy(update={"root": (base_dir / store.root).resolve()})
return store
- Step 4: Verify wf_config tests
Run:
uv run pytest tests/wf_config/test_config_models.py -q
Expected: all wf_config model tests pass.
Task 3: Add Store Role Resolver Helpers
Files:
-
Modify:
src/wf_config/models.py -
Test:
tests/wf_config/test_config_models.py -
Step 1: Add fallback behavior test
Append this test to tests/wf_config/test_config_models.py:
def test_server_config_resolves_missing_role_stores_to_default_store() -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": ".default"},
"stores": {
"auth": {"kind": "filesystem", "root": ".auth"},
},
},
}
)
assert config.server.workflow_store.root.as_posix() == ".default"
assert config.server.auth_store.root.as_posix() == ".auth"
assert config.server.source_registry_store.root.as_posix() == ".default"
assert config.server.catalog_cache_store.root.as_posix() == ".default"
- Step 2: Run the failing test
Run:
uv run pytest tests/wf_config/test_config_models.py::test_server_config_resolves_missing_role_stores_to_default_store -q
Expected: fail because workflow_store, auth_store, source_registry_store, and catalog_cache_store do not exist.
- Step 3: Add properties on
ServerConfig
In src/wf_config/models.py, inside ServerConfig, add these properties after sources:
@property
def workflow_store(self) -> StoreConfig:
return self.stores.workflow or self.store
@property
def auth_store(self) -> StoreConfig:
return self.stores.auth or self.store
@property
def source_registry_store(self) -> StoreConfig:
return self.stores.source_registry or self.store
@property
def catalog_cache_store(self) -> StoreConfig:
return self.stores.catalog_cache or self.store
- Step 4: Verify resolver tests
Run:
uv run pytest tests/wf_config/test_config_models.py::test_server_config_resolves_missing_role_stores_to_default_store -q
Expected: pass.
Task 4: Use Workflow Role Store For Local/Static Server Composition
Files:
-
Modify:
src/wf_server/config.py -
Modify:
src/wf_cli/context.py -
Modify:
src/wf_transport_rpc_http/cli.py -
Test:
tests/wf_transport_rpc_http/test_cli.py -
Test:
tests/wf_cli/test_context.py -
Step 1: Add CLI server test for workflow store override
In tests/wf_transport_rpc_http/test_cli.py, find the test that captures store_root for wf-rpc-server --config. Add this new test next to it:
def test_rpc_server_cli_config_uses_workflow_store_override(
tmp_path: Path,
monkeypatch,
) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": ".default"},
"stores": {
"workflow": {
"kind": "filesystem",
"root": ".workflow",
}
},
},
}
),
encoding="utf-8",
)
captured: dict[str, object] = {}
def fake_build_local_static_workflow_server(root: Path):
captured["store_root"] = root
return _server()
monkeypatch.setattr(
rpc_cli,
"build_local_static_workflow_server",
fake_build_local_static_workflow_server,
)
monkeypatch.setattr(rpc_cli.uvicorn, "run", lambda *args, **kwargs: None)
result = runner.invoke(rpc_cli.app, ["--config", str(config_path)])
assert result.exit_code == 0, result.output
assert captured["store_root"] == (tmp_path / ".workflow").resolve()
Adjust helper names (runner, rpc_cli, _server) to match existing tests/wf_transport_rpc_http/test_cli.py conventions exactly.
- Step 2: Add local CLI context test
In tests/wf_cli/test_context.py, add:
def test_load_cli_context_local_uses_workflow_store_override(
tmp_path: Path,
monkeypatch,
) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {"target": {"kind": "local"}},
"server": {
"store": {"kind": "filesystem", "root": ".default"},
"stores": {
"workflow": {
"kind": "filesystem",
"root": ".workflow",
}
},
},
}
),
encoding="utf-8",
)
captured: dict[str, object] = {}
def fake_build_local_static_workflow_server(root: Path):
captured["store_root"] = root
return build_local_static_workflow_server(tmp_path / "actual")
monkeypatch.setattr(
"wf_cli.context.build_local_static_workflow_server",
fake_build_local_static_workflow_server,
)
context = load_cli_context(config_path)
assert context.service is None
assert captured["store_root"] == (tmp_path / ".workflow").resolve()
If imports are missing, add import json, from pathlib import Path, and build_local_static_workflow_server/load_cli_context using the style already in the file.
- Step 3: Run failing tests
Run:
uv run pytest tests/wf_transport_rpc_http/test_cli.py::test_rpc_server_cli_config_uses_workflow_store_override tests/wf_cli/test_context.py::test_load_cli_context_local_uses_workflow_store_override -q
Expected: fail because both call sites still use config.server.store.
- Step 4: Update local/static composition helpers
In src/wf_server/config.py, change:
store = config.server.store
to:
store = config.server.workflow_store
In src/wf_cli/context.py, in the local target branch, change:
store = config.server.store
to:
store = config.server.workflow_store
In src/wf_transport_rpc_http/cli.py, in the neutral config path, change:
store = workflow_config.server.store
to:
store = workflow_config.server.workflow_store
Keep the existing FilesystemStoreConfig guards unchanged.
- Step 5: Verify local/static store override tests
Run:
uv run pytest tests/wf_transport_rpc_http/test_cli.py::test_rpc_server_cli_config_uses_workflow_store_override tests/wf_cli/test_context.py::test_load_cli_context_local_uses_workflow_store_override -q
Expected: pass.
Task 5: Carry Role Store Roots Through MCP Bridge Runtime Config
Files:
-
Modify:
src/wf_mcp/broker/models.py -
Modify:
src/wf_mcp/broker/config.py -
Test:
tests/wf_mcp/test_workflow_config_bridge.py -
Step 1: Add failing bridge test
Append this test to tests/wf_mcp/test_workflow_config_bridge.py:
def test_broker_config_from_workflow_config_carries_role_store_roots() -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": ".default"},
"stores": {
"workflow": {"kind": "filesystem", "root": ".workflow"},
"auth": {"kind": "filesystem", "root": ".auth"},
"source_registry": {
"kind": "filesystem",
"root": ".sources",
},
"catalog_cache": {
"kind": "filesystem",
"root": ".catalog",
},
},
},
}
)
broker = broker_config_from_workflow_config(config)
assert broker.store_roots.workflow_root == Path(".workflow")
assert broker.store_roots.auth_root == Path(".auth")
assert broker.store_roots.source_registry_root == Path(".sources")
assert broker.store_roots.catalog_cache_root == Path(".catalog")
Ensure imports include Path, WorkflowConfigFile, and broker_config_from_workflow_config.
- Step 2: Run failing test
Run:
uv run pytest tests/wf_mcp/test_workflow_config_bridge.py::test_broker_config_from_workflow_config_carries_role_store_roots -q
Expected: fail because BrokerConfig.store_roots does not exist.
- Step 3: Add runtime store-root bundle
In src/wf_mcp/broker/models.py, add:
@dataclass(frozen=True, slots=True)
class BrokerStoreRoots:
"""Resolved filesystem roots for MCP compatibility stores.
`default_root` preserves legacy `store_root` behavior. Role roots let
neutral config split workflow records, auth, desired source registry, and
catalog/cache storage without changing legacy config files.
"""
default_root: Path
workflow_root: Path
auth_root: Path
source_registry_root: Path
catalog_cache_root: Path
@classmethod
def from_default(cls, root: Path) -> BrokerStoreRoots:
return cls(
default_root=root,
workflow_root=root,
auth_root=root,
source_registry_root=root,
catalog_cache_root=root,
)
Update BrokerConfig to include:
store_roots: BrokerStoreRoots | None = None
Add a __post_init__:
def __post_init__(self) -> None:
if self.store_roots is None:
self.store_roots = BrokerStoreRoots.from_default(self.store_root)
If BrokerConfig is frozen, use object.__setattr__. If it is not frozen, direct assignment is fine. Do not remove store_root; legacy code still expects it.
- Step 4: Build store roots from neutral config
In src/wf_mcp/broker/config.py, import BrokerStoreRoots. Add helper:
def _filesystem_store_root(store: object, *, role: str) -> Path:
if not isinstance(store, FilesystemStoreConfig):
raise ValueError(f"MCP-backed workflow server requires filesystem {role} store")
return store.root
In broker_config_from_workflow_config, pass:
store_roots=BrokerStoreRoots(
default_root=_filesystem_store_root(config.server.store, role="default"),
workflow_root=_filesystem_store_root(
config.server.workflow_store,
role="workflow",
),
auth_root=_filesystem_store_root(config.server.auth_store, role="auth"),
source_registry_root=_filesystem_store_root(
config.server.source_registry_store,
role="source_registry",
),
catalog_cache_root=_filesystem_store_root(
config.server.catalog_cache_store,
role="catalog_cache",
),
),
Keep store_root=config.server.store.root for compatibility.
- Step 5: Verify bridge test
Run:
uv run pytest tests/wf_mcp/test_workflow_config_bridge.py::test_broker_config_from_workflow_config_carries_role_store_roots -q
Expected: pass.
Task 6: Use Role Roots In MCP Service Construction
Files:
-
Modify:
src/wf_mcp/broker/config.py -
Test:
tests/wf_mcp/test_workflow_config_bridge.py -
Step 1: Add service construction test
Append this test to tests/wf_mcp/test_workflow_config_bridge.py:
def test_build_service_from_neutral_config_uses_role_store_roots(
tmp_path: Path,
) -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": str(tmp_path / "default")},
"stores": {
"workflow": {
"kind": "filesystem",
"root": str(tmp_path / "workflow"),
},
"auth": {
"kind": "filesystem",
"root": str(tmp_path / "auth"),
},
"source_registry": {
"kind": "filesystem",
"root": str(tmp_path / "sources"),
},
"catalog_cache": {
"kind": "filesystem",
"root": str(tmp_path / "catalog"),
},
},
"sources": [
{
"kind": "mcp",
"id": "everything.default",
"provider": "everything",
"account": "default",
"transport": {
"kind": "stdio",
"command": "uvx",
"args": ["mcp-server-everything"],
},
}
],
},
}
)
broker = broker_config_from_workflow_config(config)
service = build_service_from_config(broker)
assert service.store.root == tmp_path / "auth"
assert service.artifact_store.root == tmp_path / "workflow"
assert service.draft_workspace_store.root == tmp_path / "workflow"
assert service.run_store.root == tmp_path / "workflow"
assert (tmp_path / "sources").exists()
If artifact_store.root is not public on the concrete store, assert by writing a draft/artifact/run through the store or inspect the concrete type used in nearby tests. Keep the assertion specific to the existing file store implementation.
- Step 2: Run failing test
Run:
uv run pytest tests/wf_mcp/test_workflow_config_bridge.py::test_build_service_from_neutral_config_uses_role_store_roots -q
Expected: fail because build_service_from_config still uses config.store_root for every store.
- Step 3: Wire role roots into
build_service_from_config
In src/wf_mcp/broker/config.py, update build_service_from_config:
store_roots = config.store_roots or BrokerStoreRoots.from_default(config.store_root)
workflow_stores = file_workflow_stores(store_roots.workflow_root)
service = WfMcpService(
store=FileStore(store_roots.auth_root),
artifact_store=workflow_stores.artifact_store,
draft_workspace_store=workflow_stores.draft_workspace_store,
run_store=workflow_stores.run_store,
...
)
source_registry_store = FileSourceRegistryStore(store_roots.source_registry_root)
Catalog/cache still goes through FileStore(store_roots.auth_root) in this slice unless there is already a clean constructor seam to split auth and catalog. Add this comment above store=FileStore(...):
# FileStore still owns both auth files and catalog snapshots. Role roots are
# carried separately so a later FileStore split can move catalog_cache without a
# config migration.
This comment is required because docs mention the limitation and future agents see code first.
- Step 4: Verify MCP bridge tests
Run:
uv run pytest tests/wf_mcp/test_workflow_config_bridge.py -q
Expected: all tests pass.
Task 7: Docs And Final Verification
Files:
-
Modify:
docs/current_roadmap.md -
Modify:
docs/superpowers/specs/2026-06-03-workflow-config-targets-and-sources.md -
Modify:
docs/wf_cli.md -
Step 1: Update docs status
In docs/current_roadmap.md, update the role-specific server stores bullet to say:
- First role-specific store slice complete: neutral config now accepts optional
`server.stores.workflow`, `server.stores.auth`,
`server.stores.source_registry`, and `server.stores.catalog_cache`
filesystem overrides. Missing roles still fall back to `server.store`.
MCP compatibility still uses one `FileStore` class for auth and catalog
snapshots internally; the separate catalog root is carried for the future
store split.
In docs/superpowers/specs/2026-06-03-workflow-config-targets-and-sources.md, add a short status sentence under ### Store Roles:
Implementation status: first filesystem-only slice implemented. Role overrides
are optional and fall back to `server.store`.
In docs/wf_cli.md, update the store paragraph to say role-specific overrides are supported for filesystem stores, not only future.
- Step 2: Run focused tests
Run:
uv run pytest tests/wf_config/test_config_models.py tests/wf_mcp/test_workflow_config_bridge.py tests/wf_transport_rpc_http/test_cli.py tests/wf_cli/test_context.py -q
Expected: pass.
- Step 3: Run lint and type checks
Run:
uv run ruff check src tests
uv run basedpyright --level error src
Expected: ruff passes and basedpyright reports 0 errors.
- Step 4: Optional full suite
Run if time allows:
uv run pytest -q
Expected: existing suite shape, currently around 1195 passed, 1 skipped, 1 xfailed.
- Step 5: Final report
Report:
- files changed
- tests run and exact output
- whether full suite was run
- any deviations
- the remaining limitation:
FileStorestill combines auth and catalog snapshot storage, even though config now carries separate role roots
Self-Review Notes
- Scope is one slice: config model, path resolution, local/static composition, MCP bridge role roots.
- No SQL/object/secret-manager backend implementation.
- No source registry behavior changes except choosing the configured registry root.
- No automatic migration edits; existing configs keep working because every role falls back to
server.store.