refactor: add static source provider seam
This commit is contained in:
@@ -96,6 +96,9 @@ auth admin are implemented. The next work is polish, not new broad surfaces.
|
|||||||
`ops.py` source config, capability call, draft artifact creation, deployment,
|
`ops.py` source config, capability call, draft artifact creation, deployment,
|
||||||
and workflow run. Runbook:
|
and workflow run. Runbook:
|
||||||
[`Python source`](runbooks/python-source.md).
|
[`Python source`](runbooks/python-source.md).
|
||||||
|
- Completed: static source inventory providers now have an explicit
|
||||||
|
`WorkflowSourceProvider.load_sources()` seam in `wf_server`, and Python
|
||||||
|
source loading is behind `PythonSourceProvider`.
|
||||||
- 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).
|
||||||
|
|||||||
@@ -65,6 +65,18 @@ wf_config.server.sources[]
|
|||||||
-> transport or CLI
|
-> transport or CLI
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The first shared provider seam is intentionally static:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WorkflowSourceProvider(Protocol):
|
||||||
|
def load_sources(self) -> Mapping[str, CapabilitySource]: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This covers source families that can project configured inventory into
|
||||||
|
workflow-facing `CapabilitySource` objects. Provider-specific runtime pools,
|
||||||
|
admin/apply hooks, auth, catalog caches, and live health checks stay outside
|
||||||
|
this narrow seam until a source family needs them.
|
||||||
|
|
||||||
For MCP, the provider also owns stateful upstream sessions:
|
For MCP, the provider also owns stateful upstream sessions:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -78,6 +90,7 @@ For Python, the provider is simpler:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
PythonSourceConfig(path, module, registry)
|
PythonSourceConfig(path, module, registry)
|
||||||
|
-> PythonSourceProvider
|
||||||
-> import module
|
-> import module
|
||||||
-> load NodeSpec registry
|
-> load NodeSpec registry
|
||||||
-> qualify specs under source id
|
-> qualify specs under source id
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ Implemented:
|
|||||||
- `wf_config` accepts `server.sources[]` entries with `kind: "python"`.
|
- `wf_config` accepts `server.sources[]` entries with `kind: "python"`.
|
||||||
- `wf_sources_python` loads trusted in-process `NodeSpec` registries from
|
- `wf_sources_python` loads trusted in-process `NodeSpec` registries from
|
||||||
`path` plus `module:registry`.
|
`path` plus `module:registry`.
|
||||||
- `wf_server.config` composes Python sources into local/static servers.
|
- `wf_sources_python.PythonSourceProvider` implements the static
|
||||||
|
`WorkflowSourceProvider.load_sources()` seam used by `wf_server.config`.
|
||||||
- `wf config validate` imports configured trusted Python sources and reports
|
- `wf config validate` imports configured trusted Python sources and reports
|
||||||
missing modules, missing registries, invalid registry shapes, and duplicate
|
missing modules, missing registries, invalid registry shapes, and duplicate
|
||||||
specs before server startup.
|
specs before server startup.
|
||||||
|
|||||||
+14
-14
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from wf_config import FilesystemStoreConfig, PythonSourceConfig, WorkflowConfigFile
|
from wf_config import FilesystemStoreConfig, PythonSourceConfig, WorkflowConfigFile
|
||||||
from wf_platform import CapabilitySource
|
|
||||||
|
|
||||||
from .context import WorkflowServer, build_local_static_workflow_server
|
from .context import WorkflowServer, build_local_static_workflow_server
|
||||||
|
from .sources import WorkflowSourceProvider, collect_static_sources
|
||||||
|
|
||||||
|
|
||||||
def _has_mcp_sources(config: WorkflowConfigFile) -> bool:
|
def _has_mcp_sources(config: WorkflowConfigFile) -> bool:
|
||||||
@@ -14,20 +14,20 @@ def _has_mcp_sources(config: WorkflowConfigFile) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _python_sources(config: WorkflowConfigFile) -> dict[str, CapabilitySource]:
|
def _static_source_providers(
|
||||||
from wf_sources_python import load_python_source
|
config: WorkflowConfigFile,
|
||||||
|
) -> list[WorkflowSourceProvider]:
|
||||||
return {
|
providers: list[WorkflowSourceProvider] = []
|
||||||
source.id: load_python_source(
|
python_configs = [
|
||||||
source_id=source.id,
|
source
|
||||||
path=source.path,
|
|
||||||
module=source.module,
|
|
||||||
registry=source.registry,
|
|
||||||
enabled=source.enabled,
|
|
||||||
)
|
|
||||||
for source in config.server.sources
|
for source in config.server.sources
|
||||||
if isinstance(source, PythonSourceConfig)
|
if isinstance(source, PythonSourceConfig)
|
||||||
}
|
]
|
||||||
|
if python_configs:
|
||||||
|
from wf_sources_python import PythonSourceProvider
|
||||||
|
|
||||||
|
providers.append(PythonSourceProvider(python_configs))
|
||||||
|
return providers
|
||||||
|
|
||||||
|
|
||||||
def _build_mcp_workflow_server_from_workflow_config(
|
def _build_mcp_workflow_server_from_workflow_config(
|
||||||
@@ -68,7 +68,7 @@ def build_workflow_server_from_workflow_config(
|
|||||||
raise ValueError("wf-rpc-server currently requires filesystem store")
|
raise ValueError("wf-rpc-server currently requires filesystem store")
|
||||||
return build_local_static_workflow_server(
|
return build_local_static_workflow_server(
|
||||||
store.root,
|
store.root,
|
||||||
extra_sources=_python_sources(config),
|
extra_sources=collect_static_sources(_static_source_providers(config)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from wf_platform import CapabilitySource
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowSourceProvider(Protocol):
|
||||||
|
"""Static source inventory provider for server composition.
|
||||||
|
|
||||||
|
This seam is intentionally narrow: providers return workflow-facing
|
||||||
|
`CapabilitySource` objects. Runtime pools, admin/apply hooks, and live
|
||||||
|
health checks remain separate provider-specific concerns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def load_sources(self) -> Mapping[str, CapabilitySource]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StaticSourceProvider:
|
||||||
|
"""Adapter for already-materialized capability sources."""
|
||||||
|
|
||||||
|
sources: Mapping[str, CapabilitySource]
|
||||||
|
|
||||||
|
def load_sources(self) -> Mapping[str, CapabilitySource]:
|
||||||
|
return dict(self.sources)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_static_sources(
|
||||||
|
providers: Sequence[WorkflowSourceProvider],
|
||||||
|
) -> dict[str, CapabilitySource]:
|
||||||
|
"""Merge static provider inventories while rejecting ambiguous source ids."""
|
||||||
|
collected: dict[str, CapabilitySource] = {}
|
||||||
|
for provider in providers:
|
||||||
|
for source_id, source in provider.load_sources().items():
|
||||||
|
if source_id in collected:
|
||||||
|
raise ValueError(f"duplicate workflow source ids: {[source_id]}")
|
||||||
|
collected[source_id] = source
|
||||||
|
return collected
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"StaticSourceProvider",
|
||||||
|
"WorkflowSourceProvider",
|
||||||
|
"collect_static_sources",
|
||||||
|
]
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .loader import PythonSourceConfigLike, load_python_source, python_capability_source
|
from .loader import (
|
||||||
|
PythonSourceConfigLike,
|
||||||
|
PythonSourceProvider,
|
||||||
|
load_python_source,
|
||||||
|
python_capability_source,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"PythonSourceConfigLike",
|
"PythonSourceConfigLike",
|
||||||
|
"PythonSourceProvider",
|
||||||
"load_python_source",
|
"load_python_source",
|
||||||
"python_capability_source",
|
"python_capability_source",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Protocol
|
from typing import Any, Protocol
|
||||||
@@ -23,6 +24,16 @@ class PythonSourceConfigLike(Protocol):
|
|||||||
enabled: bool
|
enabled: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PythonSourceProvider:
|
||||||
|
"""Static source provider for trusted Python source config entries."""
|
||||||
|
|
||||||
|
configs: Sequence[PythonSourceConfigLike]
|
||||||
|
|
||||||
|
def load_sources(self) -> Mapping[str, CapabilitySource]:
|
||||||
|
return {config.id: python_capability_source(config) for config in self.configs}
|
||||||
|
|
||||||
|
|
||||||
def load_python_source(
|
def load_python_source(
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
|
|||||||
@@ -5,11 +5,24 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from wf_config import WorkflowConfigFile
|
from wf_config import WorkflowConfigFile
|
||||||
|
from wf_platform import CapabilityBuckets, CapabilitySource
|
||||||
from wf_server.config import (
|
from wf_server.config import (
|
||||||
build_workflow_server_from_legacy_mcp_config,
|
build_workflow_server_from_legacy_mcp_config,
|
||||||
build_workflow_server_from_workflow_config,
|
build_workflow_server_from_workflow_config,
|
||||||
)
|
)
|
||||||
from wf_server.context import WorkflowServer
|
from wf_server.context import WorkflowServer
|
||||||
|
from wf_server.sources import StaticSourceProvider, collect_static_sources
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSourceProvider:
|
||||||
|
def load_sources(self):
|
||||||
|
return {
|
||||||
|
"fake.ops": CapabilitySource(
|
||||||
|
id="fake.ops",
|
||||||
|
kind="python",
|
||||||
|
capabilities=CapabilityBuckets(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_config_with_python_source_exposes_capability(tmp_path: Path) -> None:
|
def test_workflow_config_with_python_source_exposes_capability(tmp_path: Path) -> None:
|
||||||
@@ -39,6 +52,27 @@ def test_workflow_config_with_python_source_exposes_capability(tmp_path: Path) -
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_source_provider_protocol_collects_sources() -> None:
|
||||||
|
sources = collect_static_sources([FakeSourceProvider()])
|
||||||
|
|
||||||
|
assert set(sources) == {"fake.ops"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_source_provider_rejects_duplicate_source_ids() -> None:
|
||||||
|
provider = StaticSourceProvider(
|
||||||
|
{
|
||||||
|
"fake.ops": CapabilitySource(
|
||||||
|
id="fake.ops",
|
||||||
|
kind="python",
|
||||||
|
capabilities=CapabilityBuckets(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="duplicate workflow source ids"):
|
||||||
|
collect_static_sources([provider, FakeSourceProvider()])
|
||||||
|
|
||||||
|
|
||||||
def test_build_workflow_server_from_workflow_config_uses_local_static_for_no_mcp_sources(
|
def test_build_workflow_server_from_workflow_config_uses_local_static_for_no_mcp_sources(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from wf_sources_python import load_python_source
|
from wf_sources_python import PythonSourceProvider, load_python_source
|
||||||
|
|
||||||
|
|
||||||
|
class PythonSourceConfigFixture:
|
||||||
|
id = "local.ops"
|
||||||
|
path = Path(".")
|
||||||
|
module = "tests.fixtures.python_source_ops"
|
||||||
|
registry = "registry"
|
||||||
|
enabled = True
|
||||||
|
|
||||||
|
|
||||||
def test_load_python_source_from_sequence_registry() -> None:
|
def test_load_python_source_from_sequence_registry() -> None:
|
||||||
@@ -23,6 +31,13 @@ def test_load_python_source_from_sequence_registry() -> None:
|
|||||||
assert source.permissions.safe_for_workflow is True
|
assert source.permissions.safe_for_workflow is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_source_provider_loads_configured_sources() -> None:
|
||||||
|
sources = PythonSourceProvider([PythonSourceConfigFixture()]).load_sources()
|
||||||
|
|
||||||
|
assert set(sources) == {"local.ops"}
|
||||||
|
assert "local.ops.echo" in sources["local.ops"].capabilities.node_specs
|
||||||
|
|
||||||
|
|
||||||
def test_load_python_source_from_callable_registry() -> None:
|
def test_load_python_source_from_callable_registry() -> None:
|
||||||
source = load_python_source(
|
source = load_python_source(
|
||||||
source_id="local.ops",
|
source_id="local.ops",
|
||||||
|
|||||||
Reference in New Issue
Block a user