Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,36 @@ status, terminal evidence receipts, and signed decision and terminal webhooks.
It keeps `waiting_for_reconciliation` as a lifecycle state and
`reconciliation_required` as a terminal outcome.

## OpenAdapt Execute v1

OpenAdapt Execute is the public asynchronous contract for a qualified
workflow. A partner sends an authorized request, receives an execution ID, and
then reads a terminal receipt or receives a signed webhook. The contract does
not expose a runner, customer data, evidence bytes, application recipes, or
Cloud control-plane internals.

The generated OpenAPI document is packaged at
`openadapt_types/schemas/execute-v1-openapi.json`. The package also includes a
small Python client. The reference Python and TypeScript clients are in
[`examples/execute`](examples/execute/). Use `https://app.openadapt.ai/api` as
the OpenAdapt Cloud base URL. It uses a partner-provisioned bearer token and
the client appends the stable `/v1` paths.
The client requires an HTTPS base URL and rejects redirects before it can send
the bearer token to another endpoint.

```python
from openadapt_types import ExecuteClient

client = ExecuteClient(
base_url="https://app.openadapt.ai/api",
bearer_token="partner-provisioned-token",
)
```

The client does not poll forever. A workflow can wait for a human decision or
reconciliation. Use the terminal receipt or signed webhook as the completion
signal.

## Design principles

- **Pydantic v2**: runtime validation, JSON Schema export, fast serialization
Expand Down
13 changes: 13 additions & 0 deletions examples/execute/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# OpenAdapt Execute v1 reference clients

The public contract is the generated
[`execute-v1-openapi.json`](../../openadapt_types/schemas/execute-v1-openapi.json)
document. These small clients show the three stable resource paths.

Use a partner-provisioned endpoint and bearer token. OpenAdapt Cloud uses
`https://app.openadapt.ai/api` as its base URL. The clients add `/v1` paths.

The clients do not expose Cloud internals. They do not create qualifications,
issue authority, select application connectors, or handle webhooks. Generate a
complete client from the OpenAPI document when your integration needs full
schema types or webhook receiver support.
55 changes: 55 additions & 0 deletions examples/execute/execute-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Minimal TypeScript client for the public OpenAdapt Execute v1 resource paths.
*
* Generate complete language bindings from
* openadapt_types/schemas/execute-v1-openapi.json when your application needs
* all schema types. This example keeps the runtime dependency-free.
*/

export type ExecuteStatus = {
schema_version: "openadapt.execute-status/v1";
execution_id: string;
state: "queued" | "running" | "decision_required" | "waiting_for_reconciliation" | "terminal";
terminal_outcome?: "verified" | "halted_before_effect" | "reconciliation_required" | "rejected_policy" | "failed_platform" | "rolled_back_verified";
evidence_receipt_id?: string;
updated_at: string;
};

export type ExecuteAccepted = {
schema_version: "openadapt.execute-accepted/v1";
execution_id: string;
state: "queued";
};

export class OpenAdaptExecuteClient {
constructor(
private readonly baseUrl: string,
private readonly bearerToken: string,
) {}

async createExecution(request: Record<string, unknown>): Promise<ExecuteAccepted> {
return this.request("/v1/executions", { method: "POST", body: JSON.stringify(request) });
}

async getExecution(executionId: string): Promise<ExecuteStatus> {
return this.request(`/v1/executions/${encodeURIComponent(executionId)}`);
}

async getReceipt(executionId: string): Promise<Record<string, unknown>> {
return this.request(`/v1/executions/${encodeURIComponent(executionId)}/receipt`);
}

private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${this.baseUrl.replace(/\/$/, "")}${path}`, {
...init,
headers: {
Accept: "application/json",
Authorization: `Bearer ${this.bearerToken}`,
...(init.body ? { "Content-Type": "application/json" } : {}),
...init.headers,
},
});
if (!response.ok) throw new Error(`OpenAdapt Execute request failed (${response.status})`);
return response.json() as Promise<T>;
}
}
29 changes: 29 additions & 0 deletions examples/execute/python_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Use the published Python reference client against a partner endpoint."""

from openadapt_types import (
EffectStrengthV1,
ExecuteClient,
ExecuteRequestV1,
)

client = ExecuteClient(
base_url="https://app.openadapt.ai/api",
bearer_token="replace-with-a-partner-provisioned-token",
)

accepted = client.create_execution(
ExecuteRequestV1(
qualification_id="qualification_12345678",
workflow_version="workflow_20260729",
workflow_digest="sha256:" + "a" * 64,
environment_id="environment_12345678",
parameters={"request_reference": "external-reference"},
idempotency_key="caller_key_12345678",
authorization_context={
"actor_id": "caller_agent_12345678",
"authorization_reference": "authorization_12345678",
},
minimum_effect_strength=EffectStrengthV1.INDEPENDENT_SYSTEM_OF_RECORD,
)
)
print(accepted.execution_id)
3 changes: 3 additions & 0 deletions openadapt_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
sign_execute_webhook_hmac,
)
from openadapt_types.execute_openapi import execute_openapi_document
from openadapt_types.execute_client import ExecuteApiError, ExecuteClient
from openadapt_types.execution_requirements import (
CAPABILITY_MATCH_SCHEMA,
EXECUTION_REQUIREMENTS_SCHEMA,
Expand Down Expand Up @@ -250,6 +251,8 @@
"ExecuteWebhookV1",
"sign_execute_webhook_hmac",
"execute_openapi_document",
"ExecuteApiError",
"ExecuteClient",
# runner capability and execution requirements
"CAPABILITY_MATCH_SCHEMA",
"EXECUTION_REQUIREMENTS_SCHEMA",
Expand Down
139 changes: 139 additions & 0 deletions openadapt_types/execute_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Small standard-library client for the public OpenAdapt Execute v1 contract.

The client only submits and reads the versioned public resources. It does not
implement partner enrollment, webhook delivery, permit issuance, runners, or
application connectors.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import Any, Final
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener

from openadapt_types.execute import (
ExecuteAcceptedV1,
ExecuteEvidenceReceiptV1,
ExecuteRequestV1,
ExecuteStatusV1,
)

_JSON_CONTENT_TYPE: Final = "application/json"


class _RejectRedirectHandler(HTTPRedirectHandler):
"""Stop before urllib can forward a bearer token to another origin."""

def redirect_request( # type: ignore[override]
self,
req: Request,
fp: Any,
code: int,
msg: str,
headers: Any,
newurl: str,
) -> Request:
raise HTTPError(req.full_url, code, "Execute redirects are not allowed", headers, fp)


_NO_REDIRECT_OPENER: Final = build_opener(_RejectRedirectHandler())


class ExecuteApiError(RuntimeError):
"""A transport or HTTP error returned by an Execute endpoint."""

def __init__(self, status_code: int | None, message: str) -> None:
self.status_code = status_code
super().__init__(message)


@dataclass(frozen=True)
class ExecuteClient:
"""Call a partner-provisioned Execute endpoint with its bearer token.

``base_url`` is the provider base URL. For OpenAdapt Cloud, use
``https://app.openadapt.ai/api``; this client appends the stable ``/v1``
resource paths from the public contract.
"""

base_url: str
bearer_token: str = field(repr=False)
timeout_seconds: float = 30.0

def __post_init__(self) -> None:
parsed = urlsplit(self.base_url)
if (
parsed.scheme != "https"
or not parsed.netloc
or parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
raise ValueError(
"base_url must be an HTTPS origin or path prefix without credentials, query, or fragment"
)
if not self.bearer_token.strip():
raise ValueError("bearer_token must not be empty")
if self.timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive")

def create_execution(self, request: ExecuteRequestV1) -> ExecuteAcceptedV1:
"""Submit one qualified execution for durable processing."""

response = self._json_request(
method="POST",
path="/v1/executions",
payload=request.model_dump(mode="json"),
)
return ExecuteAcceptedV1.model_validate(response)

def get_execution(self, execution_id: str) -> ExecuteStatusV1:
"""Return the current public lifecycle state."""

response = self._json_request(
method="GET", path=f"/v1/executions/{quote(execution_id, safe='')}"
)
return ExecuteStatusV1.model_validate(response)

def get_receipt(self, execution_id: str) -> ExecuteEvidenceReceiptV1:
"""Return the terminal receipt after the execution reaches TERMINAL."""

response = self._json_request(
method="GET", path=f"/v1/executions/{quote(execution_id, safe='')}/receipt"
)
return ExecuteEvidenceReceiptV1.model_validate(response)

def _json_request(
self, *, method: str, path: str, payload: dict[str, Any] | None = None
) -> dict[str, Any]:
data = None if payload is None else json.dumps(payload).encode("utf-8")
request = Request(
self.base_url.rstrip("/") + path,
data=data,
method=method,
headers={
"Accept": _JSON_CONTENT_TYPE,
"Authorization": f"Bearer {self.bearer_token}",
**({"Content-Type": _JSON_CONTENT_TYPE} if data is not None else {}),
},
)
try:
with _NO_REDIRECT_OPENER.open(request, timeout=self.timeout_seconds) as response:
raw = response.read()
except HTTPError as exc:
detail = "" if exc.fp is None else exc.read().decode("utf-8", errors="replace")
raise ExecuteApiError(exc.code, detail or exc.reason) from exc
except URLError as exc:
raise ExecuteApiError(None, str(exc.reason)) from exc

try:
decoded = json.loads(raw)
except json.JSONDecodeError as exc:
raise ExecuteApiError(None, "Execute returned a non-JSON response") from exc
if not isinstance(decoded, dict):
raise ExecuteApiError(None, "Execute returned a non-object JSON response")
return decoded
33 changes: 32 additions & 1 deletion openadapt_types/execute_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def execute_openapi_document() -> dict[str, Any]:
"/v1/executions": {
"post": {
"operationId": "createExecution",
"security": [{"bearerAuth": []}],
"requestBody": {
"required": True,
"content": {
Expand All @@ -79,6 +80,7 @@ def execute_openapi_document() -> dict[str, Any]:
"/v1/executions/{execution_id}": {
"get": {
"operationId": "getExecution",
"security": [{"bearerAuth": []}],
"parameters": [execution_id],
"responses": {
"200": {
Expand All @@ -95,6 +97,7 @@ def execute_openapi_document() -> dict[str, Any]:
"/v1/executions/{execution_id}/receipt": {
"get": {
"operationId": "getExecutionReceipt",
"security": [{"bearerAuth": []}],
"parameters": [execution_id],
"responses": {
"200": {
Expand All @@ -112,6 +115,11 @@ def execute_openapi_document() -> dict[str, Any]:
"webhooks": {
"executionStateChanged": {
"post": {
"security": [],
"description": (
"Verify the body HMAC with the published Execute v1 "
"webhook contract before accepting this delivery."
),
"requestBody": {
"required": True,
"content": {
Expand All @@ -127,6 +135,11 @@ def execute_openapi_document() -> dict[str, Any]:
},
"executionTerminal": {
"post": {
"security": [],
"description": (
"Verify the body HMAC with the published Execute v1 "
"webhook contract before accepting this delivery."
),
"requestBody": {
"required": True,
"content": {
Expand All @@ -142,6 +155,11 @@ def execute_openapi_document() -> dict[str, Any]:
},
"executionDecisionRequired": {
"post": {
"security": [],
"description": (
"Verify the body HMAC with the published Execute v1 "
"webhook contract before accepting this delivery."
),
"requestBody": {
"required": True,
"content": {
Expand All @@ -156,5 +174,18 @@ def execute_openapi_document() -> dict[str, Any]:
}
},
},
"components": {"schemas": schemas},
"components": {
"schemas": schemas,
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "OpenAdapt Execute partner token",
"description": (
"A partner-provisioned, scope-limited service token. "
"Do not put it in browser code or workflow parameters."
),
}
},
},
}
Loading
Loading