feat: add server store role overrides
This commit is contained in:
@@ -0,0 +1,890 @@
|
||||
# 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`: add `ServerStoresConfig` with optional role-specific store overrides.
|
||||
- `src/wf_config/loader.py`: resolve relative paths for `server.store` and every configured role store.
|
||||
- `src/wf_config/__init__.py`: export `ServerStoresConfig`.
|
||||
- `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 runtime `BrokerStoreRoots` so the legacy bridge can carry separate roots internally.
|
||||
- `src/wf_transport_rpc_http/cli.py`: preserve `--store-root` behavior and avoid accidental role-store bypass for MCP configs.
|
||||
- `src/wf_cli/context.py`: local `--local` target should use `stores.workflow` when 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:
|
||||
|
||||
```python
|
||||
workflow
|
||||
auth
|
||||
source_registry
|
||||
catalog_cache
|
||||
```
|
||||
|
||||
Resolution rule:
|
||||
|
||||
```python
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
@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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
store = config.server.store
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
store = config.server.workflow_store
|
||||
```
|
||||
|
||||
In `src/wf_cli/context.py`, in the local target branch, change:
|
||||
|
||||
```python
|
||||
store = config.server.store
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
store = config.server.workflow_store
|
||||
```
|
||||
|
||||
In `src/wf_transport_rpc_http/cli.py`, in the neutral config path, change:
|
||||
|
||||
```python
|
||||
store = workflow_config.server.store
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
store = workflow_config.server.workflow_store
|
||||
```
|
||||
|
||||
Keep the existing `FilesystemStoreConfig` guards unchanged.
|
||||
|
||||
- [ ] **Step 5: Verify local/static store override tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
@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:
|
||||
|
||||
```python
|
||||
store_roots: BrokerStoreRoots | None = None
|
||||
```
|
||||
|
||||
Add a `__post_init__`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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(...)`:
|
||||
|
||||
```python
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```markdown
|
||||
- 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`:
|
||||
|
||||
```markdown
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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: `FileStore` still 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`.
|
||||
Reference in New Issue
Block a user