Files
lda-wf/docs/historical/superpowers/plans/2026-05-27-openapi-core-capability-source.md
T

18 KiB

OpenAPI Core Capability Source 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: Let a saved OpenAPI document become a workflow capability source whose operations appear as workflow-facing NodeSpecs and execute through spec-driven HTTP requests, without parsing generated Python client code.

Architecture: The OpenAPI document is the source of truth for inventory, JSON Schemas, operation paths, parameters, request bodies, and response validation. openapi-core validates/unmarshals OpenAPI requests and responses; a small local httpx adapter builds and sends HTTP requests from public OpenAPI-shaped payloads. No generated Python client runtime, no AST parsing, and no case-conversion dependency.

Tech Stack: Python 3.14, openapi-core, httpx, existing jsonschema, wf_authoring.NodeSpec, wf_platform.CapabilitySource, wf_mcp service registration.


Why This Replaces The Generated-Client Plan

The first plan made the ugly part parameter-name recovery:

OpenAPI/public input: path.petId, header.X-Trace-ID
generated Python fn:  pet_id, x_trace_id

Because openapi-python-client did not expose a stable operation manifest for those mappings, the implementation parsed generated endpoint functions. That is the wrong dependency direction. We should not inspect generated Python to recover metadata already present in the OpenAPI document.

The revised runtime shape is:

workflow input
  -> OpenAPI-shaped request parts
  -> openapi-core validates/unmarshals request
  -> local httpx request builder sends request
  -> openapi-core validates/unmarshals response
  -> generic workflow outcome

This makes validation and execution boring. Outcome mapping remains intentionally generic until saved wrappers add business semantics.

Non-Goals

  • Do not generate Python clients for v1 runtime execution.
  • Do not parse generated Python, generated docstrings, or generated function signatures.
  • Do not invent a full OpenAPI validator. Use openapi-core for request/response validation where possible and jsonschema for existing schema-boundary checks.
  • Do not make every HTTP status a business outcome. Raw OpenAPI operations expose generic transport outcomes.
  • Do not implement custom auth UX in this slice. Leave auth as explicit configuration fields and later integrate with the existing auth/store layer.
  • Do not expose every OpenAPI operation as a top-level MCP tool. Expose operations as workflow capabilities first.

V1 Public Payload Shape

Workflow inputs stay OpenAPI-shaped:

{
  "path": {"petId": "pet-1"},
  "query": {"includeOwner": true},
  "header": {"X-Trace-ID": "abc"},
  "cookie": {},
  "body": {"name": "Ada"}
}

No petId -> pet_id translation exists because there is no generated Python function.

V1 Outcome Semantics

Every raw OpenAPI operation node exposes:

ok
http_error
unexpected_status
validation_error
transport_error

Rules:

  • ok: response status is a declared 2xx response and response validation passes.
  • http_error: response status is a declared non-2xx response and response validation passes.
  • unexpected_status: response status is not declared and no default response covers it.
  • validation_error: request or response does not match the OpenAPI document.
  • transport_error: HTTP client raises before a response exists.

Output shape:

{
  "status_code": 200,
  "headers": {},
  "body": {},
  "validation_errors": []
}

body is JSON when the response is JSON, text for text responses, bytes/base64 later if needed. Keep binary response support out of v1 unless a test fixture forces it.

Planned File Structure

  • Keep: src/wf_openapi/__init__.py
    • Public exports for the optional OpenAPI capability-source package.
  • Keep/modify: src/wf_openapi/models.py
    • Operation/source/execution models. Remove generated-client metadata.
  • Keep/modify: src/wf_openapi/spec.py
    • Load OpenAPI documents, normalize operations, merge inherited path-item parameters with operation-local overrides.
  • Keep/modify: src/wf_openapi/schemas.py
    • Produce JSON Schema contracts from effective OpenAPI operation inputs/outputs.
  • Replace: src/wf_openapi/executor.py
    • Generic httpx + openapi-core operation executor.
  • Remove or repurpose: src/wf_openapi/codegen.py
    • Delete generated-client runtime helpers. If kept temporarily, it must not be used by runtime/source tests.
  • Create: src/wf_openapi/request.py
    • Build method, URL, headers, cookies, query params, and JSON body from OpenAPI-shaped payload.
  • Create: src/wf_openapi/validation.py
    • Thin adapter between local request/response objects and openapi-core protocols.
  • Keep/modify: src/wf_openapi/source.py
    • Build CapabilitySource and NodeSpecs using generic execution config.
  • Tests:
    • tests/openapi/test_spec_inventory.py
    • tests/openapi/test_schemas.py if split becomes useful.
    • tests/openapi/test_request_builder.py
    • tests/openapi/test_executor.py
    • tests/openapi/test_source.py

Task 1: Dependency And Plan Reset

Files:

  • Modify: pyproject.toml

  • Modify: uv.lock

  • Modify: docs/historical/superpowers/plans/2026-05-27-openapi-capability-source.md

  • Test: tests/openapi/test_codegen_executor.py may be removed or replaced later.

  • Step 1: Replace runtime dependency

In pyproject.toml, remove openapi-python-client unless another committed package already uses it. Add:

"openapi-core>=0.19",

Keep httpx if already present transitively or directly; add it directly if wf_openapi imports it.

  • Step 2: Refresh lockfile

Run:

uv lock

Expected: lockfile updates successfully.

  • Step 3: Mark generated-client plan superseded

Keep the superseded note at the top of:

docs/historical/superpowers/plans/2026-05-27-openapi-capability-source.md

Expected: future agents do not continue Task 5/6 AST parsing work.

  • Step 4: Verify import availability

Run:

uv run python -c "import openapi_core, httpx; print(openapi_core.__name__, httpx.__name__)"

Expected: prints openapi_core httpx.


Task 2: Remove Generated-Client Runtime Coupling

Files:

  • Modify: src/wf_openapi/codegen.py

  • Modify: src/wf_openapi/executor.py

  • Modify: tests/openapi/test_codegen_executor.py

  • Step 1: Write failing guard test

Add a test that proves runtime no longer imports generated-client metadata:

def test_openapi_runtime_does_not_require_generated_manifest() -> None:
    from wf_openapi.executor import OpenApiExecutionConfig

    config = OpenApiExecutionConfig(base_url="https://api.example.test")

    assert config.base_url == "https://api.example.test"
    assert not hasattr(config, "generated_package")
    assert not hasattr(config, "operation_modules")
    assert not hasattr(config, "parameter_arguments")

Run:

uv run pytest -q tests/openapi/test_codegen_executor.py::test_openapi_runtime_does_not_require_generated_manifest

Expected before implementation: FAIL because generated-client fields still exist.

  • Step 2: Simplify execution config

Replace generated-client config with:

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class OpenApiExecutionConfig:
    """Runtime config for spec-driven OpenAPI HTTP execution."""

    base_url: str
    timeout_seconds: float = 30.0

Do not include generated package/module/parameter mapping fields.

  • Step 3: Remove generated manifest helpers from runtime path

Delete or quarantine:

GeneratedOperationMetadata
load_generated_operation_manifest
generate_openapi_client

If codegen.py remains, its module docstring must say it is experimental/offline tooling and not used by source/executor runtime.

  • Step 4: Run focused tests

Run:

uv run pytest -q tests/openapi
uv run ruff check src/wf_openapi tests/openapi
uv run basedpyright --level error src/wf_openapi tests/openapi

Expected: generated-client tests that no longer match are removed/replaced; remaining tests pass.


Task 3: Generic Request Builder

Files:

  • Create: src/wf_openapi/request.py

  • Test: tests/openapi/test_request_builder.py

  • Step 1: Write request builder tests

Create tests/openapi/test_request_builder.py:

from wf_openapi.request import build_http_request_parts
from wf_openapi.spec import load_openapi_operations

FIXTURE = "tests/openapi/fixtures/petstore_minimal.openapi.json"


def test_build_http_request_parts_uses_public_openapi_names() -> None:
    operation = next(op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet")

    parts = build_http_request_parts(
        operation,
        base_url="https://api.example.test/v1",
        payload={
            "path": {"petId": "pet-1"},
            "query": {"includeOwner": True},
            "header": {"X-Trace-ID": "trace-1"},
        },
    )

    assert parts.method == "GET"
    assert parts.url == "https://api.example.test/v1/pets/pet-1"
    assert parts.params["includeOwner"] is True
    assert parts.headers["X-Trace-ID"] == "trace-1"

Expected before implementation: FAIL because wf_openapi.request does not exist.

  • Step 2: Implement request parts

Create:

from dataclasses import dataclass, field
from typing import Any, Mapping
from urllib.parse import quote

from wf_openapi.models import OpenApiOperation


@dataclass(frozen=True, slots=True)
class HttpRequestParts:
    """OpenAPI-shaped request parts ready for httpx."""

    method: str
    url: str
    params: dict[str, Any] = field(default_factory=dict)
    headers: dict[str, str] = field(default_factory=dict)
    cookies: dict[str, str] = field(default_factory=dict)
    json: Any | None = None


def build_http_request_parts(
    operation: OpenApiOperation,
    *,
    base_url: str,
    payload: Mapping[str, Any],
) -> HttpRequestParts:
    """Build an HTTP request without renaming public OpenAPI fields."""
    path_values = _mapping(payload, "path")
    path = operation.path
    for parameter in operation.effective_parameters:
        if parameter.get("in") != "path":
            continue
        name = parameter["name"]
        if name not in path_values:
            raise ValueError(f"missing path parameter {name!r}")
        path = path.replace("{" + name + "}", quote(str(path_values[name]), safe=""))

    return HttpRequestParts(
        method=operation.method.upper(),
        url=base_url.rstrip("/") + path,
        params=dict(_mapping(payload, "query")),
        headers={str(k): str(v) for k, v in _mapping(payload, "header").items()},
        cookies={str(k): str(v) for k, v in _mapping(payload, "cookie").items()},
        json=payload.get("body"),
    )


def _mapping(payload: Mapping[str, Any], key: str) -> Mapping[str, Any]:
    value = payload.get(key, {})
    if not isinstance(value, Mapping):
        raise ValueError(f"{key} must be an object")
    return value
  • Step 3: Add edge tests

Add tests for:

missing path parameter -> ValueError
non-object query/header/cookie/path -> ValueError
body passes through as json payload
  • Step 4: Verify

Run:

uv run pytest -q tests/openapi/test_request_builder.py

Expected: PASS.


Task 4: openapi-core Validation Adapter

Files:

  • Create: src/wf_openapi/validation.py

  • Test: tests/openapi/test_validation.py

  • Step 1: Write validation tests

Create tests that load the fixture and validate a request built from public payload:

from wf_openapi.request import build_http_request_parts
from wf_openapi.spec import load_openapi, load_openapi_operations
from wf_openapi.validation import validate_openapi_request

FIXTURE = "tests/openapi/fixtures/petstore_minimal.openapi.json"


def test_validate_openapi_request_accepts_public_payload() -> None:
    document = load_openapi(FIXTURE)
    operation = next(op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet")
    parts = build_http_request_parts(
        operation,
        base_url="https://api.example.test",
        payload={"path": {"petId": "pet-1"}},
    )

    result = validate_openapi_request(document, parts)

    assert result.valid is True
    assert result.errors == []

Expected before implementation: FAIL because validation adapter does not exist.

  • Step 2: Implement minimal protocol objects

Implement local request/response protocol adapters required by openapi-core. Keep them in validation.py and document that they are intentionally thin protocol shims.

The adapter must carry:

method
full_url_pattern or path pattern if required by openapi-core
parameters/path/query/header/cookie
body
mimetype

If openapi-core requires a different protocol shape, adapt only this file.

  • Step 3: Validate response path

Add:

def validate_openapi_response(document, request_parts, response_parts) -> ValidationResult:
    ...

Test declared 200 response and undeclared status behavior.

  • Step 4: Verify

Run:

uv run pytest -q tests/openapi/test_validation.py
uv run basedpyright --level error src/wf_openapi/validation.py tests/openapi/test_validation.py

Expected: PASS.


Task 5: Generic HTTP Executor

Files:

  • Modify: src/wf_openapi/executor.py

  • Test: tests/openapi/test_executor.py

  • Step 1: Write executor tests with mocked transport

Use httpx.MockTransport:

import httpx

from wf_openapi.executor import OpenApiExecutionConfig, call_openapi_operation
from wf_openapi.spec import load_openapi, load_openapi_operations

FIXTURE = "tests/openapi/fixtures/petstore_minimal.openapi.json"


async def test_call_openapi_operation_maps_success() -> None:
    document = load_openapi(FIXTURE)
    operation = next(op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet")

    async def handler(request: httpx.Request) -> httpx.Response:
        assert request.url.path == "/pets/pet-1"
        return httpx.Response(200, json={"id": "pet-1"})

    result = await call_openapi_operation(
        document,
        operation,
        OpenApiExecutionConfig(base_url="https://api.example.test"),
        {"path": {"petId": "pet-1"}},
        client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
    )

    assert result.outcome == "ok"
    assert result.value.status_code == 200
    assert result.value.body["id"] == "pet-1"

Expected before implementation: FAIL because executor still uses generated client or wrong signature.

  • Step 2: Implement executor

call_openapi_operation(...) should:

build request parts
validate/unmarshal request
send with httpx.AsyncClient
parse response body by content-type
validate/unmarshal response
return NodeReturn with generic outcome

Transport exceptions become transport_error. Validation failures become validation_error.

  • Step 3: Add outcome tests

Add tests for:

declared non-2xx -> http_error
undeclared status -> unexpected_status
invalid request -> validation_error
httpx transport exception -> transport_error
  • Step 4: Verify

Run:

uv run pytest -q tests/openapi/test_executor.py

Expected: PASS.


Task 6: Source Integration

Files:

  • Modify: src/wf_openapi/source.py

  • Test: tests/openapi/test_source.py

  • Step 1: Write source execution test

Build a CapabilitySource, get source.capabilities.node_specs["petstore.default.get_pet"], call its async handler with public payload, and use httpx.MockTransport through runtime/config injection.

Expected before implementation: FAIL because source still uses generated metadata or does not pass executor dependencies.

  • Step 2: Update source builder

build_openapi_capability_source(...) should accept:

document_path: Path
source_id: str
base_url: str

It should not accept:

generated_package
operation_modules
parameter_arguments
  • Step 3: Preserve schema contracts

Ensure each NodeSpec still exposes:

input_schema_contract from operation input schema
output_schema_contract from operation output schema
outcomes = ("ok", "http_error", "unexpected_status", "validation_error", "transport_error")
  • Step 4: Verify

Run:

uv run pytest -q tests/openapi/test_source.py

Expected: PASS.


Task 7: Docs And Final Cleanup

Files:

  • Create: docs/openapi_capability_source.md

  • Modify: docs/current_roadmap.md

  • Delete or rewrite: generated-client-only tests/files if no longer used.

  • Step 1: Document the boundary

Create docs/openapi_capability_source.md with:

# OpenAPI Capability Sources

OpenAPI sources expose raw API operations as workflow capabilities.

The OpenAPI document is the source of truth. Runtime execution uses a generic
httpx request builder and openapi-core validation. The runtime does not parse
generated Python clients and does not rename public OpenAPI fields.

Raw OpenAPI nodes expose generic transport outcomes. Saved wrappers should add
business-specific outcomes such as `not_found`, `rate_limited`, or
`needs_input`.
  • Step 2: Note deferred auth/body/binary support

Document:

auth integration: future
binary/multipart request bodies: future
rich outcome mapping: wrappers, not raw operations
  • Step 3: Final verification

Run:

uv run pytest -q tests/openapi
uv run ruff check src/wf_openapi tests/openapi
uv run ruff format --check src/wf_openapi tests/openapi
uv run basedpyright --level error src/wf_openapi tests/openapi

Expected: all pass.


Self-Review

  • The plan no longer requires generated Python client parsing.
  • Public OpenAPI names remain public workflow names.
  • Validation is library-backed through openapi-core, not hand-rolled.
  • HTTP execution is locally owned but small and testable with httpx.MockTransport.
  • Outcome mapping stays generic and wrapper-friendly.
  • Auth, multipart/binary, and business outcomes are deferred explicitly.