242 lines
7.4 KiB
Markdown
242 lines
7.4 KiB
Markdown
# Runtime Source Lifecycle
|
|
|
|
Date: 2026-06-09
|
|
|
|
Status: design direction for next source families
|
|
|
|
Related:
|
|
|
|
- [Workflow config targets and sources](./2026-06-03-workflow-config-targets-and-sources.md)
|
|
- [Store-backed source registry](./2026-06-03-store-backed-source-registry-design.md)
|
|
- [Long-lived workflow API boundary](./2026-06-03-long-lived-workflow-api-boundary.md)
|
|
- [Project map](../../project_map.md)
|
|
|
|
## Purpose
|
|
|
|
The current mutable source registry and apply/reload path are concrete for MCP
|
|
sources. The next source families should not be forced through MCP
|
|
`ConnectionConfig` language. This spec defines the generic lifecycle that MCP,
|
|
Python, HTTP/OpenAPI, and future source providers should share.
|
|
|
|
The rule:
|
|
|
|
> Admin mutation changes desired source state. Apply/reload reconciles desired
|
|
> source state into live runtime sources. `wf_api` keeps seeing only
|
|
> `CapabilitySource`, `NodeSpec`, resources, prompts, and protocol surfaces.
|
|
|
|
## Current State
|
|
|
|
MCP has a working implementation:
|
|
|
|
```text
|
|
source registry desired MCP entries
|
|
-> wf admin registry apply
|
|
-> WfMcpService connections/adapters/catalog
|
|
-> WorkflowApi capability/source surfaces
|
|
```
|
|
|
|
This works, but it is still MCP-shaped internally:
|
|
|
|
- registry entries are `McpSourceRegistryEntry`
|
|
- runtime apply mutates `ConnectionService`
|
|
- execution uses `McpRuntimePool`
|
|
- compatibility glue still translates through legacy `ConnectionConfig`
|
|
|
|
That is acceptable for MCP. It should not become the required model for Python
|
|
or HTTP sources.
|
|
|
|
## Desired Generic Lifecycle
|
|
|
|
All source families should fit this lifecycle:
|
|
|
|
```text
|
|
config seed sources + store-backed desired sources
|
|
-> source provider manager
|
|
-> live source instances
|
|
-> capability inventory and executable NodeSpecs
|
|
-> WorkflowApi and transports
|
|
```
|
|
|
|
Definitions:
|
|
|
|
- **Source config**: deployment bootstrap in `wf_config`.
|
|
- **Source registry**: mutable desired state stored by the server.
|
|
- **Source provider**: implementation package for one source family, such as
|
|
`wf_sources_mcp`, `wf_sources_python`, or `wf_sources_openapi`.
|
|
- **Live source instance**: in-memory runtime object created from desired state.
|
|
- **Apply/reload**: reconciliation from desired state to live source instances.
|
|
- **Capability projection**: `CapabilitySource` plus executable `NodeSpec`s,
|
|
resources, prompts, reducers, or workflows exposed to `wf_api`.
|
|
|
|
## Provider Boundary
|
|
|
|
A future neutral provider boundary should be closer to this shape than to MCP
|
|
connections:
|
|
|
|
```python
|
|
from collections.abc import Mapping, Sequence
|
|
from typing import Any, Protocol
|
|
|
|
from wf_authoring import NodeSpec
|
|
from wf_platform import CapabilitySource
|
|
|
|
|
|
class SourceEntryLike(Protocol):
|
|
id: str
|
|
kind: str
|
|
enabled: bool
|
|
|
|
|
|
class SourceProvider(Protocol):
|
|
kind: str
|
|
|
|
async def apply_sources(
|
|
self,
|
|
*,
|
|
desired: Sequence[SourceEntryLike],
|
|
) -> dict[str, Any]:
|
|
"""Reconcile desired entries into live source instances."""
|
|
...
|
|
|
|
@property
|
|
def capability_sources(self) -> Mapping[str, CapabilitySource]:
|
|
"""Return current workflow-visible source inventory."""
|
|
...
|
|
|
|
def get_qualified_spec(self, qualified_name: str) -> NodeSpec[Any, Any]:
|
|
"""Return the executable spec for one qualified capability."""
|
|
...
|
|
```
|
|
|
|
This is not an immediate API contract. It is the direction: the provider owns
|
|
source-family semantics, while `wf_api` remains source-family neutral.
|
|
|
|
## Source Family Expectations
|
|
|
|
### MCP Sources
|
|
|
|
MCP sources already have:
|
|
|
|
- desired registry entries
|
|
- config seed/locked ownership
|
|
- explicit apply/reload
|
|
- persistent runtime sessions through `McpRuntimePool`
|
|
- catalog snapshots for tools/resources/prompts
|
|
|
|
MCP may continue to use `ConnectionConfig` behind its compatibility boundary
|
|
until the old `wf_mcp` facade is retired.
|
|
|
|
### Python Sources
|
|
|
|
Python sources should be the first non-MCP proof that the architecture is not
|
|
MCP-shaped.
|
|
|
|
Expected config example:
|
|
|
|
```json
|
|
{
|
|
"kind": "python",
|
|
"id": "local.ops",
|
|
"module": "my_project.workflow_ops",
|
|
"registry": "registry"
|
|
}
|
|
```
|
|
|
|
Expected behavior:
|
|
|
|
- load a module/object containing `NodeSpec`s or Python functions convertible to
|
|
`NodeSpec`s
|
|
- expose them under the configured source id
|
|
- execute in-process without MCP sessions
|
|
- support reload by re-importing/re-reading the configured registry
|
|
|
|
Python source reload should be explicit. Do not silently reload modules on every
|
|
capability call.
|
|
|
|
### HTTP/OpenAPI Sources
|
|
|
|
HTTP/OpenAPI sources should be a separate source family, not a special MCP
|
|
transport.
|
|
|
|
Expected behavior:
|
|
|
|
- parse OpenAPI or explicit operation definitions
|
|
- expose operations as `NodeSpec`s
|
|
- use auth records where appropriate
|
|
- define a clear HTTP status/outcome policy before implementation
|
|
- keep large response/body handling bounded for CLI and workflow safety
|
|
|
|
This source family needs more design than Python because error semantics,
|
|
timeouts, idempotency, and output normalization are product decisions.
|
|
|
|
## Config vs Registry
|
|
|
|
Config and registry should keep their current distinction:
|
|
|
|
- Config is bootstrap, deployment intent, and portable local setup.
|
|
- Registry is mutable server-owned desired state.
|
|
- `locked` config entries own their ids and shadow registry entries.
|
|
- `seed` config entries initialize missing registry entries, then registry owns
|
|
later admin changes.
|
|
|
|
For future source families, this ownership model should apply to source ids
|
|
independently of MCP provider/account fields.
|
|
|
|
## Apply Semantics
|
|
|
|
Apply/reload should:
|
|
|
|
- validate desired source entries before mutating live state
|
|
- add newly desired sources
|
|
- update changed source instances
|
|
- disable or remove deleted desired sources according to provider policy
|
|
- preserve diagnostics for referenced missing/disabled sources
|
|
- avoid mutating config files
|
|
- emit admin-visible events
|
|
|
|
Apply/reload should not:
|
|
|
|
- delete auth records by default
|
|
- delete old catalog/cache files by default
|
|
- rewrite deployments or artifacts
|
|
- turn live source failure into workflow interrupts
|
|
|
|
## Workflow API Boundary
|
|
|
|
`wf_api` should not know whether a capability came from MCP, Python, HTTP, or a
|
|
future source family. It should depend on:
|
|
|
|
- `WorkflowSpecProvider`
|
|
- `CapabilitySource`
|
|
- `NodeSpec`
|
|
- store/admin protocols
|
|
|
|
If a future source needs a family-specific operation, put it behind that
|
|
provider package or admin surface. Do not add source-family-specific branches to
|
|
workflow run execution.
|
|
|
|
## First Implementation Direction
|
|
|
|
Recommended next implementation slice:
|
|
|
|
1. Add `PythonSourceConfig` to `wf_config` as a tagged `server.sources` entry.
|
|
2. Create `wf_sources_python` with a loader for `module:object` registries.
|
|
3. Project loaded `NodeSpec`s into `CapabilitySource`.
|
|
4. Wire local/static `WorkflowServer` construction to include Python sources.
|
|
5. Prove `wf cap list`, `wf cap call`, and deployment `run start` work without
|
|
importing `wf_mcp`.
|
|
|
|
This slice should not implement source registry mutation for Python yet. Static
|
|
config first proves the provider shape. Mutable registry/apply can follow once
|
|
the provider interface is real.
|
|
|
|
## Open Questions
|
|
|
|
- Should generic source registry entries live in `wf_api`, `wf_server`, or a new
|
|
`wf_sources` package?
|
|
- Should provider managers be composed by `wf_server.config` or by a separate
|
|
source runtime package?
|
|
- How much hot-reload behavior should Python sources support in development?
|
|
- What are the safe defaults for HTTP timeouts, retries, and large response
|
|
handling?
|