Files
lda-wf/docs/historical/superpowers/plans/2026-06-02-wf-api-store-ownership.md
T

16 KiB
Raw Blame History

wf_api Store Ownership 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: Make workflow artifact, draft workspace, and run store ownership explicit so wf_api, CLI, MCP, and future HTTP entrypoints can share stores without relying on WfMcpService.__post_init__.

Architecture: Add a protocol-neutral WorkflowStores bundle in wf_api that groups the three workflow stores. MCP config construction remains responsible for creating file-backed stores from BrokerConfig.store_root; WfMcpService receives stores but no longer manufactures them from its MCP Store. Existing process-local behavior stays intact through build_service_from_config.

Tech Stack: Python 3.14, dataclasses, wf_api, wf_artifacts, wf_mcp, pytest, ruff, basedpyright.


Current Problem

src/wf_mcp/broker/service/core.py currently does this in WfMcpService.__post_init__:

if self.artifact_store is None:
    self.artifact_store = FileWorkflowArtifactStore(_store_root(self.store))
if self.draft_workspace_store is None:
    self.draft_workspace_store = FileDraftWorkspaceStore(_store_root(self.store))
if self.run_store is None:
    self.run_store = FileRunStore(_store_root(self.store))

That makes a protocol-specific service decide protocol-neutral workflow persistence. It also hides missing-store tests because WfMcpService(store=FileStore(...)) silently creates workflow stores.

src/wf_mcp/broker/config.py::build_service_from_config already does the right thing by passing all three stores explicitly. This slice preserves that behavior and removes the fallback.

Target Ownership Rule

  • wf_artifacts owns store protocols and file store implementations.
  • wf_api may group protocol-neutral workflow stores into a small DTO.
  • wf_mcp owns MCP config loading and calls the DTO factory for file-backed process-local stores.
  • WfMcpService owns broker state, connections, adapters, source catalogs, events, and execution wiring.
  • WfMcpService does not create workflow artifact/draft/run stores by guessing from the MCP catalog/auth store.

Files

  • Create: src/wf_api/stores.py
  • Modify: src/wf_api/__init__.py
  • Modify: src/wf_mcp/broker/config.py
  • Modify: src/wf_mcp/broker/service/core.py
  • Modify: direct WfMcpService(...) tests only where they rely on implicit workflow stores
  • Test: tests/wf_api/test_stores.py
  • Test: tests/wf_mcp/service/test_catalog.py
  • Test: tests/wf_mcp/test_broker_server.py

Task 1: Add Protocol-Neutral Store Bundle

Files:

  • Create: src/wf_api/stores.py

  • Modify: src/wf_api/__init__.py

  • Test: tests/wf_api/test_stores.py

  • Step 1: Write failing store bundle tests

Create tests/wf_api/test_stores.py:

from __future__ import annotations

from wf_api.stores import WorkflowStores, file_workflow_stores
from wf_artifacts import (
    FileDraftWorkspaceStore,
    FileRunStore,
    FileWorkflowArtifactStore,
)

from tests.wf_mcp.test_support import local_temp_root


def test_file_workflow_stores_constructs_all_three_file_stores() -> None:
    root = local_temp_root() / "wf_api_file_workflow_stores"

    stores = file_workflow_stores(root)

    assert isinstance(stores, WorkflowStores)
    assert isinstance(stores.artifact_store, FileWorkflowArtifactStore)
    assert isinstance(stores.draft_workspace_store, FileDraftWorkspaceStore)
    assert isinstance(stores.run_store, FileRunStore)
    assert stores.artifact_store.root == root
    assert stores.draft_workspace_store.root == root
    assert stores.run_store.root == root


def test_wf_api_exports_workflow_stores() -> None:
    from wf_api import WorkflowStores as ExportedWorkflowStores
    from wf_api import file_workflow_stores as exported_file_workflow_stores

    assert ExportedWorkflowStores is WorkflowStores
    assert exported_file_workflow_stores is file_workflow_stores
  • Step 2: Run failing tests

Run:

uv run pytest tests\wf_api\test_stores.py -q

Expected: import failure for wf_api.stores.

  • Step 3: Implement wf_api.stores

Create src/wf_api/stores.py:

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

from wf_artifacts import (
    DraftWorkspaceStore,
    FileDraftWorkspaceStore,
    FileRunStore,
    FileWorkflowArtifactStore,
    RunStore,
    WorkflowArtifactStore,
)


@dataclass(frozen=True, slots=True)
class WorkflowStores:
    """Protocol-neutral persistence dependencies for workflow APIs."""

    artifact_store: WorkflowArtifactStore
    draft_workspace_store: DraftWorkspaceStore
    run_store: RunStore


def file_workflow_stores(root: str | Path) -> WorkflowStores:
    """Create process-local file-backed workflow stores under one root."""
    store_root = Path(root)
    return WorkflowStores(
        artifact_store=FileWorkflowArtifactStore(store_root),
        draft_workspace_store=FileDraftWorkspaceStore(store_root),
        run_store=FileRunStore(store_root),
    )


__all__ = ["WorkflowStores", "file_workflow_stores"]
  • Step 4: Export from wf_api

Update src/wf_api/__init__.py:

from .stores import WorkflowStores, file_workflow_stores

Add both names to __all__.

  • Step 5: Verify Task 1

Run:

uv run pytest tests\wf_api\test_stores.py -q
uv run ruff check src\wf_api\stores.py tests\wf_api\test_stores.py
uv run ruff format --check src\wf_api\stores.py tests\wf_api\test_stores.py

Expected: tests pass, lint pass, format pass.


Task 2: Move Config Store Construction Through the Bundle

Files:

  • Modify: src/wf_mcp/broker/config.py

  • Test: tests/wf_mcp/test_broker_server.py

  • Step 1: Strengthen config construction test

Find tests/wf_mcp/test_broker_server.py::test_build_service_from_config_uses_store_root_for_artifacts.

Update it to assert all three stores use the configured root:

def test_build_service_from_config_uses_store_root_for_workflow_stores() -> None:
    store_root = local_temp_root() / "broker_config_workflow_stores"
    config = BrokerConfig(store_root=store_root, connections=[])

    service = build_service_from_config(config)

    assert isinstance(service.artifact_store, FileWorkflowArtifactStore)
    assert isinstance(service.draft_workspace_store, FileDraftWorkspaceStore)
    assert isinstance(service.run_store, FileRunStore)
    assert service.artifact_store.root == store_root
    assert service.draft_workspace_store.root == store_root
    assert service.run_store.root == store_root

Ensure the test imports:

from wf_artifacts import FileDraftWorkspaceStore, FileRunStore, FileWorkflowArtifactStore
  • Step 2: Run the focused test

Run:

uv run pytest tests\wf_mcp\test_broker_server.py::test_build_service_from_config_uses_store_root_for_workflow_stores -q

Expected: pass before the config refactor, proving current behavior is covered.

  • Step 3: Update build_service_from_config to use file_workflow_stores

In src/wf_mcp/broker/config.py, replace direct file store imports:

from wf_api import file_workflow_stores

Remove:

from wf_artifacts import (
    FileDraftWorkspaceStore,
    FileRunStore,
    FileWorkflowArtifactStore,
)

Then update build_service_from_config:

def build_service_from_config(config: BrokerConfig) -> WfMcpService:
    """Create a broker service with SDK adapters for configured connections."""
    runtime_factory = PersistentSessionFactory()
    workflow_stores = file_workflow_stores(config.store_root)
    service = WfMcpService(
        store=FileStore(config.store_root),
        artifact_store=workflow_stores.artifact_store,
        draft_workspace_store=workflow_stores.draft_workspace_store,
        run_store=workflow_stores.run_store,
        # Discovery can use short-lived SDK sessions. Workflow execution needs
        # a persistent runtime so stateful MCP servers keep session/page state
        # across sequential workflow nodes.
        tool_executor=McpRuntimePool(runtime_factory.create),
    )
  • Step 4: Verify Task 2

Run:

uv run pytest tests\wf_mcp\test_broker_server.py::test_build_service_from_config_uses_store_root_for_workflow_stores -q
uv run ruff check src\wf_mcp\broker\config.py tests\wf_mcp\test_broker_server.py
uv run ruff format --check src\wf_mcp\broker\config.py tests\wf_mcp\test_broker_server.py

Expected: tests pass, lint pass, format pass.


Task 3: Remove Implicit Workflow Store Creation from WfMcpService

Files:

  • Modify: src/wf_mcp/broker/service/core.py

  • Modify: tests/wf_mcp/service/test_catalog.py

  • Step 1: Replace the old default-store test

Find tests/wf_mcp/service/test_catalog.py::test_service_installs_default_draft_workspace_store.

Replace it with:

def test_service_does_not_install_workflow_stores_implicitly() -> None:
    root = local_temp_root() / "service_no_implicit_workflow_stores"
    service = WfMcpService(store=FileStore(root))

    assert service.artifact_store is None
    assert service.draft_workspace_store is None
    assert service.run_store is None
  • Step 2: Run failing test

Run:

uv run pytest tests\wf_mcp\service\test_catalog.py::test_service_does_not_install_workflow_stores_implicitly -q

Expected: fail because WfMcpService.__post_init__ still creates stores.

  • Step 3: Remove implicit creation from WfMcpService.__post_init__

In src/wf_mcp/broker/service/core.py, remove the imports:

FileDraftWorkspaceStore,
FileRunStore,
FileWorkflowArtifactStore,

Remove the _store_root helper entirely:

def _store_root(store: Store) -> Path:
    """Return the file root for stores that expose one, else use local default."""
    root = getattr(store, "root", None)
    return root if isinstance(root, Path) else Path(".wf_mcp_store")

Update WfMcpService.__post_init__ to:

def __post_init__(self) -> None:
    """Install broker-local system specs when enabled.

    Workflow stores are injected by entrypoint/config construction. This service
    must not guess workflow persistence from the MCP catalog/auth store because
    CLI, MCP, and future HTTP frontends may share or swap those stores.
    """
    if self.include_builtin_specs:
        for source in builtin_sources().values():
            self.register_capability_source(source)
    self.register_capability_source(admin_source())

If Path becomes unused in core.py, remove from pathlib import Path.

  • Step 4: Verify Task 3

Run:

uv run pytest tests\wf_mcp\service\test_catalog.py::test_service_does_not_install_workflow_stores_implicitly -q
uv run ruff check src\wf_mcp\broker\service\core.py tests\wf_mcp\service\test_catalog.py
uv run ruff format --check src\wf_mcp\broker\service\core.py tests\wf_mcp\service\test_catalog.py

Expected: tests pass, lint pass, format pass.


Task 4: Fix Direct Service Tests That Need Workflow Stores

Files:

  • Modify only tests that fail after Task 3.

  • Step 1: Run targeted workflow API/service tests

Run:

uv run pytest tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service -q

Expected: if failures appear, they should be tests that constructed WfMcpService(store=...) but then used workflow artifact/draft/run operations.

  • Step 2: Patch only failing tests by injecting stores explicitly

For any failing direct WfMcpService(...) test that needs workflow stores, use this pattern:

from wf_api import file_workflow_stores


root = local_temp_root() / "test_specific_name"
workflow_stores = file_workflow_stores(root)
service = WfMcpService(
    store=FileStore(root / "mcp"),
    artifact_store=workflow_stores.artifact_store,
    draft_workspace_store=workflow_stores.draft_workspace_store,
    run_store=workflow_stores.run_store,
)

Do not add stores to tests that only exercise broker catalog/admin/source behavior.

  • Step 3: Keep no-store behavior tests intact

Tests like these should keep artifact_store=None through WorkflowOperationContext or direct service construction because they prove graceful no-store behavior:

assert result["nodes"] == []
assert result["deployments"] == []

Do not “fix” these by injecting stores unless the test is explicitly about stored workflow data.

  • Step 4: Verify targeted tests

Run:

uv run pytest tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service -q
uv run ruff check tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service
uv run ruff format --check tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service

Expected: targeted tests pass and no broad fixture churn.


Task 5: Document the Store Ownership Rule

Files:

  • Modify: docs/superpowers/research/2026-06-01-wf-api-extraction-map.md

  • Modify: docs/current_roadmap.md if it already has a wf_api section

  • Step 1: Update the extraction map

In docs/superpowers/research/2026-06-01-wf-api-extraction-map.md, under ### Store Ownership Ambiguity, replace the section with:

### Store Ownership

- `wf_artifacts` owns workflow store protocols and file-backed implementations.
- `wf_api.stores.WorkflowStores` groups the artifact, draft workspace, and run stores as protocol-neutral API dependencies.
- MCP config construction creates file-backed workflow stores from `BrokerConfig.store_root` and injects them into `WfMcpService`.
- `WfMcpService.__post_init__` no longer creates workflow stores from its MCP `Store`; direct service tests must inject stores when they exercise workflow persistence.
- Future HTTP/API entrypoints should construct or receive the same `WorkflowStores` bundle instead of importing `wf_mcp`.
  • Step 2: Update roadmap only if there is a matching section

If docs/current_roadmap.md has a wf_api or API extraction section, add:

- Workflow store ownership is explicit: entrypoints construct/inject `WorkflowStores`; `WfMcpService` no longer guesses stores from the MCP store root.

If there is no matching section, skip this file.

  • Step 3: Verify docs are not stale

Run:

rg -n "_store_root\\(|creates default `FileWorkflowArtifactStore`|installs default.*store" src docs tests

Expected: no current docs/tests claim WfMcpService installs default workflow stores. Historical plans may still mention old implementation; leave historical plans alone unless they are current roadmap/research docs.


Task 6: Final Verification

Files:

  • All touched files.

  • Step 1: Run focused suite

Run:

uv run pytest tests\wf_api tests\wf_mcp\workflow_surface tests\wf_mcp\test_broker_server.py tests\wf_mcp\service -q

Expected: all selected tests pass.

  • Step 2: Run full suite

Run:

uv run pytest -q

Expected: full suite passes with the repos known skip/xfail counts.

  • Step 3: Run lint and format checks

Run:

uv run ruff check src\wf_api src\wf_mcp tests\wf_api tests\wf_mcp
uv run ruff format --check src\wf_api src\wf_mcp tests\wf_api tests\wf_mcp

Expected: all checks pass.

  • Step 4: Run typecheck

Run:

uv run basedpyright --level error

Expected: 0 errors, 0 warnings, 0 notes. If the command exits nonzero only because of the known workspace enumeration warning, report that exactly.


Self-Review

  • Spec coverage: This plan covers explicit store creation, WfMcpService.__post_init__ cleanup, config behavior preservation, test migration, and docs.
  • Placeholder scan: No TODO/TBD placeholders remain. Historical-plan references are explicitly scoped.
  • Type consistency: WorkflowStores uses protocol types from wf_artifacts; file_workflow_stores() returns file-backed implementations; WfMcpService field types do not change.
  • Scope check: This does not implement FastAPI, persisted-run conflict handling, or store locking. Those are separate slices.