Files
lda-wf/docs/historical/superpowers/plans/2026-06-11-static-python-sources.md
T

759 lines
21 KiB
Markdown

# Static Python Sources 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 static config-driven Python workflow sources so local/project Python `NodeSpec`s can be exposed through `WorkflowServer`, `wf cap list`, `wf cap call`, and workflow runs without MCP.
**Architecture:** `wf_authoring` continues to own `NodeSpec` and `@node`. New package `wf_sources_python` owns module/object loading and projection into `CapabilitySource`. `wf_config` gains `kind: "python"` source entries, and `wf_server.config` composes Python sources into the local/static `WorkflowServer` path.
**Tech Stack:** Python 3.14, Pydantic v2 discriminated unions, `importlib`, `wf_authoring.NodeSpec`, `wf_platform.CapabilitySource`, pytest/pytest-asyncio, ruff, basedpyright.
---
## File Structure
- Create `src/wf_sources_python/__init__.py`
- Public exports for `PythonSourceConfigLike`, `load_python_source`, `python_capability_source`.
- Create `src/wf_sources_python/loader.py`
- Import `module`, resolve `registry`, normalize supported registry shapes.
- Create `tests/wf_sources_python/test_loader.py`
- Unit tests for loading mappings, sequences, callables, qualification, and errors.
- Modify `src/wf_config/models.py`
- Add `PythonSourceConfig` to `SourceConfig` union.
- Modify `src/wf_config/__init__.py`
- Export `PythonSourceConfig`.
- Modify `src/wf_server/context.py`
- Let `build_local_static_workflow_server(...)` accept extra capability sources.
- Modify `src/wf_server/config.py`
- Detect `kind: "python"` sources and pass loaded sources to local/static server construction.
- Modify tests:
- `tests/wf_config/test_config_models.py`
- `tests/wf_server/test_config_composition.py`
- Add or update an RPC/client smoke test proving `cap call` works.
- Modify docs:
- `docs/current_roadmap.md`
- `docs/superpowers/specs/2026-06-11-python-source-provider.md`
---
## Task 1: Config Model
**Files:**
- Modify: `src/wf_config/models.py`
- Modify: `src/wf_config/__init__.py`
- Modify: `tests/wf_config/test_config_models.py`
- [ ] **Step 1: Add failing config test**
Add to `tests/wf_config/test_config_models.py`:
```python
def test_workflow_config_parses_python_source() -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"sources": [
{
"kind": "python",
"id": "local.ops",
"module": "tests.fixtures.python_source_ops",
"registry": "registry",
}
]
},
}
)
source = config.server.sources[0]
assert source.kind == "python"
assert source.id == "local.ops"
assert source.module == "tests.fixtures.python_source_ops"
assert source.registry == "registry"
```
- [ ] **Step 2: Run failing test**
Run:
```bash
uv run pytest tests/wf_config/test_config_models.py::test_workflow_config_parses_python_source -q
```
Expected: fail because `PythonSourceConfig` is not in the source union.
- [ ] **Step 3: Add `PythonSourceConfig`**
In `src/wf_config/models.py`, add after `StdlibSourceConfig`:
```python
class PythonSourceConfig(WorkflowConfigModel):
"""Static config for trusted in-process Python workflow sources."""
kind: Literal["python"] = "python"
id: str
enabled: bool = True
module: str = Field(min_length=1)
registry: str = Field(default="registry", min_length=1)
@field_validator("id")
@classmethod
def validate_source_id(cls, value: str) -> str:
if not re.fullmatch(SOURCE_ID_PATTERN, value):
raise ValueError(
"source id must start with alphanumeric or underscore and contain "
"only [A-Za-z0-9_.-]"
)
if "." not in value:
raise ValueError("source id must look like '<namespace>.<name>'")
return value
```
Update `SourceConfig`:
```python
SourceConfig = Annotated[
StdlibSourceConfig | PythonSourceConfig | McpSourceConfig,
Field(discriminator="kind"),
]
```
Update `src/wf_config/__init__.py` to export `PythonSourceConfig`.
- [ ] **Step 4: Run config tests**
Run:
```bash
uv run pytest tests/wf_config/test_config_models.py -q
```
Expected: pass.
- [ ] **Step 5: Commit config slice**
Run:
```bash
git add src/wf_config/models.py src/wf_config/__init__.py tests/wf_config/test_config_models.py
git commit -m "feat: add python source config model"
```
---
## Task 2: Python Source Loader
**Files:**
- Create: `src/wf_sources_python/__init__.py`
- Create: `src/wf_sources_python/loader.py`
- Create: `tests/wf_sources_python/test_loader.py`
- Create: `tests/fixtures/python_source_ops.py`
- [ ] **Step 1: Add fixture module**
Create `tests/fixtures/python_source_ops.py`:
```python
from __future__ import annotations
from pydantic import BaseModel
from wf_authoring import node
class EchoInput(BaseModel):
text: str
class EchoOutput(BaseModel):
echoed: str
@node(name="echo")
def echo(payload: EchoInput) -> EchoOutput:
return EchoOutput(echoed=payload.text)
@node(name="authoring.upper")
def upper(payload: EchoInput) -> EchoOutput:
return EchoOutput(echoed=payload.text.upper())
registry = [echo, upper]
```
- [ ] **Step 2: Add failing loader tests**
Create `tests/wf_sources_python/test_loader.py`:
```python
from __future__ import annotations
import pytest
from wf_sources_python import load_python_source
def test_load_python_source_from_sequence_registry() -> None:
source = load_python_source(
source_id="local.ops",
module="tests.fixtures.python_source_ops",
registry="registry",
)
assert source.id == "local.ops"
assert source.kind == "python"
assert set(source.capabilities.node_specs) == {
"local.ops.echo",
"local.ops.upper",
}
assert source.permissions.safe_for_workflow is True
def test_load_python_source_rejects_missing_registry() -> None:
with pytest.raises(ValueError, match="missing registry object"):
load_python_source(
source_id="local.ops",
module="tests.fixtures.python_source_ops",
registry="missing",
)
def test_load_python_source_rejects_non_node_spec() -> None:
with pytest.raises(TypeError, match="expected NodeSpec"):
load_python_source(
source_id="local.ops",
module="math",
registry="pi",
)
```
- [ ] **Step 3: Run failing tests**
Run:
```bash
uv run pytest tests/wf_sources_python/test_loader.py -q
```
Expected: fail because `wf_sources_python` does not exist.
- [ ] **Step 4: Implement loader**
Create `src/wf_sources_python/loader.py`:
```python
from __future__ import annotations
from collections.abc import Callable, Mapping, Sequence
from importlib import import_module
from typing import Any, Protocol
from wf_authoring import NodeSpec, node
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
SourcePermissions,
SourceVisibility,
)
class PythonSourceConfigLike(Protocol):
id: str
module: str
registry: str
enabled: bool
def load_python_source(
*,
source_id: str,
module: str,
registry: str = "registry",
enabled: bool = True,
) -> CapabilitySource:
"""Load a trusted in-process Python source from a module registry object."""
module_obj = import_module(module)
if not hasattr(module_obj, registry):
raise ValueError(f"missing registry object {registry!r} in module {module!r}")
raw_registry = getattr(module_obj, registry)
if callable(raw_registry) and not isinstance(raw_registry, NodeSpec):
raw_registry = raw_registry()
specs = _coerce_specs(raw_registry)
qualified = [_qualify_spec(source_id, spec) for spec in specs]
names = [spec.name for spec in qualified]
if len(names) != len(set(names)):
raise ValueError(f"duplicate NodeSpec names in Python source {source_id!r}")
return CapabilitySource(
id=source_id,
kind="python",
enabled=enabled,
capabilities=CapabilityBuckets(
node_specs={spec.name: spec for spec in qualified},
),
visibility=SourceVisibility(
planner=True,
mcp_client=True,
admin_dashboard=True,
),
permissions=SourcePermissions(safe_for_workflow=True),
description=f"Python source loaded from {module}:{registry}.",
)
def python_capability_source(config: PythonSourceConfigLike) -> CapabilitySource:
return load_python_source(
source_id=config.id,
module=config.module,
registry=config.registry,
enabled=config.enabled,
)
def _coerce_specs(raw_registry: object) -> list[NodeSpec[Any, Any]]:
if isinstance(raw_registry, Mapping):
values = list(raw_registry.values())
elif isinstance(raw_registry, Sequence) and not isinstance(raw_registry, str):
values = list(raw_registry)
else:
values = [raw_registry]
specs: list[NodeSpec[Any, Any]] = []
for value in values:
if not isinstance(value, NodeSpec):
raise TypeError(f"expected NodeSpec in Python source registry, got {type(value).__name__}")
specs.append(value)
return specs
def _qualify_spec(source_id: str, spec: NodeSpec[Any, Any]) -> NodeSpec[Any, Any]:
local_name = spec.name.removeprefix("authoring.")
if local_name.startswith(f"{source_id}."):
return spec
return node(spec, name=f"{source_id}.{local_name}")
```
Create `src/wf_sources_python/__init__.py`:
```python
from __future__ import annotations
from .loader import PythonSourceConfigLike, load_python_source, python_capability_source
__all__ = [
"PythonSourceConfigLike",
"load_python_source",
"python_capability_source",
]
```
- [ ] **Step 5: Run loader tests**
Run:
```bash
uv run pytest tests/wf_sources_python/test_loader.py -q
```
Expected: pass.
- [ ] **Step 6: Commit loader slice**
Run:
```bash
git add src/wf_sources_python tests/wf_sources_python tests/fixtures/python_source_ops.py
git commit -m "feat: add python source loader"
```
---
## Task 3: Compose Python Sources Into Local WorkflowServer
**Files:**
- Modify: `src/wf_server/context.py`
- Modify: `src/wf_server/config.py`
- Modify: `tests/wf_server/test_config_composition.py`
- [ ] **Step 1: Add failing server composition test**
Add to `tests/wf_server/test_config_composition.py`:
```python
from wf_config import PythonSourceConfig
def test_workflow_config_with_python_source_exposes_capability(tmp_path) -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": str(tmp_path / "store")},
"sources": [
{
"kind": "python",
"id": "local.ops",
"module": "tests.fixtures.python_source_ops",
"registry": "registry",
}
],
},
}
)
server = build_workflow_server_from_workflow_config(config)
assert "local.ops" in server.specs.capability_sources
assert (
"local.ops.echo"
in server.specs.capability_sources["local.ops"].capabilities.node_specs
)
```
Use existing imports in that file; only add missing names.
- [ ] **Step 2: Run failing test**
Run:
```bash
uv run pytest tests/wf_server/test_config_composition.py::test_workflow_config_with_python_source_exposes_capability -q
```
Expected: fail because Python sources are ignored or unsupported.
- [ ] **Step 3: Extend `build_local_static_workflow_server`**
In `src/wf_server/context.py`, update `build_local_static_workflow_server` to accept extra sources:
```python
def build_local_static_workflow_server(
store_root: Path,
*,
extra_sources: Mapping[str, CapabilitySource] | None = None,
) -> WorkflowServer:
```
Inside the function, merge sources after built-ins:
```python
sources = builtin_sources()
if extra_sources:
overlap = set(sources) & set(extra_sources)
if overlap:
raise ValueError(f"duplicate workflow source ids: {sorted(overlap)}")
sources.update(extra_sources)
```
Use `sources` where the function currently passes `builtin_sources()` into `StaticWorkflowSpecProvider`.
- [ ] **Step 4: Wire Python sources in `wf_server.config`**
In `src/wf_server/config.py`, import:
```python
from wf_sources_python import python_capability_source
```
Add helper:
```python
def _python_sources(config: WorkflowConfigFile) -> dict[str, CapabilitySource]:
return {
source.id: python_capability_source(source)
for source in config.server.sources
if getattr(source, "kind", None) == "python"
}
```
Import `CapabilitySource` from `wf_platform` for the return annotation.
In `build_workflow_server_from_workflow_config`, change:
```python
return build_local_static_workflow_server(store.root)
```
to:
```python
return build_local_static_workflow_server(
store.root,
extra_sources=_python_sources(config),
)
```
- [ ] **Step 5: Run server composition tests**
Run:
```bash
uv run pytest tests/wf_server/test_config_composition.py tests/wf_server/test_local_static_server.py -q
```
Expected: pass.
- [ ] **Step 6: Commit composition slice**
Run:
```bash
git add src/wf_server/context.py src/wf_server/config.py tests/wf_server/test_config_composition.py
git commit -m "feat: compose python sources in workflow server"
```
---
## Task 4: Prove CLI/RPC Capability Call And Workflow Run
**Files:**
- Modify: `tests/wf_transport_rpc_http/test_app.py` or `tests/wf_server/test_config_composition.py`
- [ ] **Step 1: Add RPC capability call test**
Add to `tests/wf_transport_rpc_http/test_app.py`:
```python
async def test_rpc_calls_python_source_capability(tmp_path) -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": str(tmp_path / "store")},
"sources": [
{
"kind": "python",
"id": "local.ops",
"module": "tests.fixtures.python_source_ops",
"registry": "registry",
}
],
},
}
)
server = build_workflow_server_from_workflow_config(config)
app = create_rpc_app(server)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
listed = await _rpc(
client,
"workflow.capabilities.list",
{"source_id": "local.ops", "limit": 10},
)
called = await _rpc(
client,
"workflow.capabilities.call",
{
"qualified_name": "local.ops.echo",
"payload": {"text": "hello python"},
},
)
assert listed["result"]["total"] == 2
assert called["result"]["outcome"] == "ok"
assert called["result"]["output"] == {"echoed": "hello python"}
```
Add imports if missing:
```python
from wf_config import WorkflowConfigFile
from wf_server.config import build_workflow_server_from_workflow_config
```
- [ ] **Step 2: Run RPC test**
Run:
```bash
uv run pytest tests/wf_transport_rpc_http/test_app.py::test_rpc_calls_python_source_capability -q
```
Expected: pass.
- [ ] **Step 3: Add workflow run test if not covered by capability call**
If the capability call test passes but does not exercise deployment execution, add a direct server/API run test in `tests/wf_server/test_config_composition.py`:
```python
async def test_python_source_capability_runs_in_deployment(tmp_path) -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"store": {"kind": "filesystem", "root": str(tmp_path / "store")},
"sources": [
{
"kind": "python",
"id": "local.ops",
"module": "tests.fixtures.python_source_ops",
}
],
},
}
)
server = build_workflow_server_from_workflow_config(config)
await server.api.create_artifact_from_capability(
artifact_id="python_echo",
version=1,
capability_name="local.ops.echo",
title="Python Echo",
input_map={"text": "input.text"},
output_map={"echoed": "state.echoed"},
source_bindings={"local.ops": "local.ops"},
outcomes=("ok",),
)
await server.api.save_deployment(
{
"id": "python_echo.default",
"artifact_id": "python_echo",
"artifact_version": 1,
"bindings": [{"logical_source": "local.ops", "concrete_source": "local.ops"}],
}
)
result = await server.api.run_deployment(
deployment_id="python_echo.default",
workflow_input={"text": "run me"},
)
assert result["status"] == "completed"
assert result["output"]["echoed"] == "run me"
```
If `create_artifact_from_capability` has a different signature, follow the existing closest test in `tests/wf_api/test_capability_api.py` or `tests/wf_transport_rpc_http/test_app.py`.
- [ ] **Step 4: Run focused integration tests**
Run:
```bash
uv run pytest tests/wf_sources_python tests/wf_config/test_config_models.py tests/wf_server/test_config_composition.py tests/wf_transport_rpc_http/test_app.py::test_rpc_calls_python_source_capability -q
```
Expected: pass.
- [ ] **Step 5: Commit integration slice**
Run:
```bash
git add tests/wf_transport_rpc_http/test_app.py tests/wf_server/test_config_composition.py
git commit -m "test: prove python source capabilities over rpc"
```
---
## Task 5: Docs And Verification
**Files:**
- Modify: `docs/current_roadmap.md`
- Modify: `docs/superpowers/specs/2026-06-11-python-source-provider.md`
- Move: `docs/superpowers/plans/2026-06-11-static-python-sources.md` to `docs/historical/superpowers/plans/2026-06-11-static-python-sources.md`
- [ ] **Step 1: Update roadmap**
In `docs/current_roadmap.md`, under Priority 3, add:
```markdown
- Completed: static config `kind: "python"` sources can load trusted local
`NodeSpec` registries and expose them through WorkflowServer. Implementation:
[`static Python sources`](historical/superpowers/plans/2026-06-11-static-python-sources.md).
```
- [ ] **Step 2: Update spec status**
In `docs/superpowers/specs/2026-06-11-python-source-provider.md`, change:
```markdown
Status: planned first non-MCP source family
```
to:
```markdown
Status: first static config slice implemented
```
Add:
```markdown
## Implementation Status
Implemented:
- `wf_config` accepts `server.sources[]` entries with `kind: "python"`.
- `wf_sources_python` loads trusted in-process `NodeSpec` registries from
`module:registry`.
- `wf_server.config` composes Python sources into local/static servers.
- Capability listing/calling works over JSON-RPC.
Still deferred:
- mutable source registry/apply support
- hot reload
- reducer exports
- sandboxing/untrusted code
```
- [ ] **Step 3: Move completed plan**
Run:
```bash
git mv docs/superpowers/plans/2026-06-11-static-python-sources.md docs/historical/superpowers/plans/2026-06-11-static-python-sources.md
```
- [ ] **Step 4: Run verification**
Run:
```bash
uv run pytest tests/wf_sources_python tests/wf_config/test_config_models.py tests/wf_server/test_config_composition.py tests/wf_server/test_local_static_server.py tests/wf_transport_rpc_http/test_app.py -q
uv run ruff check src/wf_sources_python src/wf_config src/wf_server tests/wf_sources_python tests/wf_config/test_config_models.py tests/wf_server/test_config_composition.py tests/wf_transport_rpc_http/test_app.py
uv run ruff format --check src/wf_sources_python src/wf_config src/wf_server tests/wf_sources_python tests/wf_config/test_config_models.py tests/wf_server/test_config_composition.py tests/wf_transport_rpc_http/test_app.py
uv run basedpyright --level error src/wf_sources_python src/wf_config src/wf_server tests/wf_sources_python tests/wf_config/test_config_models.py tests/wf_server/test_config_composition.py tests/wf_transport_rpc_http/test_app.py
git diff --check
```
Expected:
- Tests pass.
- Ruff passes.
- Basedpyright reports 0 errors for changed files.
- `git diff --check` reports no whitespace errors; CRLF warnings on Windows are acceptable.
- [ ] **Step 5: Final commit**
Run:
```bash
git add src tests docs
git commit -m "docs: record static python source support"
```
If earlier task commits were skipped, use one final commit instead:
```bash
git add src tests docs
git commit -m "feat: add static python sources"
```
---
## Acceptance Criteria
- `wf_config` parses `kind: "python"` source entries.
- `wf_sources_python` imports a trusted module registry and projects `NodeSpec`s under the configured source id.
- `wf_server.config` includes Python sources in local/static `WorkflowServer` construction.
- Built-in `wf.std` and `wf.recipes` remain unchanged.
- No `wf_mcp` dependency is introduced.
- `workflow.capabilities.list` and `workflow.capabilities.call` work for Python source capabilities over JSON-RPC.
- Docs make clear this is trusted in-process code and not hot-reloaded or registry-mutable yet.