docs: archive completed superpowers plans
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
# 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 `NodeSpec`s 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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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 `NodeSpec`s 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/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:
|
||||
|
||||
```toml
|
||||
"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:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
```
|
||||
|
||||
Expected: lockfile updates successfully.
|
||||
|
||||
- [ ] **Step 3: Mark generated-client plan superseded**
|
||||
|
||||
Keep the superseded note at the top of:
|
||||
|
||||
```text
|
||||
docs/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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```text
|
||||
missing path parameter -> ValueError
|
||||
non-object query/header/cookie/path -> ValueError
|
||||
body passes through as json payload
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```python
|
||||
def validate_openapi_response(document, request_parts, response_parts) -> ValidationResult:
|
||||
...
|
||||
```
|
||||
|
||||
Test declared `200` response and undeclared status behavior.
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```text
|
||||
declared non-2xx -> http_error
|
||||
undeclared status -> unexpected_status
|
||||
invalid request -> validation_error
|
||||
httpx transport exception -> transport_error
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
document_path: Path
|
||||
source_id: str
|
||||
base_url: str
|
||||
```
|
||||
|
||||
It should not accept:
|
||||
|
||||
```text
|
||||
generated_package
|
||||
operation_modules
|
||||
parameter_arguments
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Preserve schema contracts**
|
||||
|
||||
Ensure each `NodeSpec` still exposes:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```markdown
|
||||
# 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:
|
||||
|
||||
```text
|
||||
auth integration: future
|
||||
binary/multipart request bodies: future
|
||||
rich outcome mapping: wrappers, not raw operations
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Final verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user