feat(web): expose provider-neutral execution plan APIs

This commit is contained in:
ChatGPT Release Governance
2026-07-22 01:51:12 +08:00
parent 3161470bd2
commit 5b0c3eb404
@@ -0,0 +1,130 @@
"""HTTP facade for portable multi-provider execution plans."""
from __future__ import annotations
import urllib.parse
from http import HTTPStatus
from typing import Any, Mapping
from aios_v2.control_plane import RequestContext, Unauthorized
from aios_v2.execution_plan import ExecutionPlanCoordinator
from aios_v2.executor_protocol import ExecutorProviderRegistry, normalize_executor
from .capabilities import require
class ExecutionPlanAPI:
"""Portal read model and provider-authenticated plan discovery API."""
INTERNAL_PREFIX = "/api/v2/internal/execution-plans"
PORTAL_PATH = "/api/v2/portal/execution"
def __init__(
self,
coordinator: ExecutionPlanCoordinator,
*,
providers: ExecutorProviderRegistry,
) -> None:
self.coordinator = coordinator
self.providers = providers
@classmethod
def is_route(cls, raw_path: str) -> bool:
path = urllib.parse.urlparse(raw_path).path.rstrip("/") or "/"
return path == cls.PORTAL_PATH or path.startswith(cls.INTERNAL_PREFIX)
def _require_provider(
self,
context: RequestContext,
executor_type: str,
) -> str:
normalized = normalize_executor(executor_type)
if not self.providers.trusted(
executor_type=normalized,
roles=tuple(context.roles),
platform_admin="platform_admin" in context.roles,
):
raise Unauthorized(
f"trusted role for executor Provider is required: {normalized}"
)
return normalized
def _portal_snapshot(self) -> Mapping[str, Any]:
plans = []
for plan in self.coordinator.plan_store.list():
plans.append(
{
"plan_id": plan.plan_id,
"target_version": plan.target_version,
"source_branch": plan.source_branch,
"source_oid": plan.source_oid,
"snapshot": self.coordinator.snapshot(plan.plan_id),
"gate": self.coordinator.gate(plan.plan_id),
}
)
return {
"protocol_version": "aios.executor.v1",
"plan_schema_version": "aios.execution-plan.v2",
"providers": [
item.as_dict() for item in self.providers.list_descriptors()
],
"plans": plans,
}
def handle(
self,
method: str,
raw_path: str,
context: RequestContext,
body: Any,
) -> tuple[int, Mapping[str, Any]]:
del body
parsed = urllib.parse.urlparse(raw_path)
path = parsed.path.rstrip("/") or "/"
method = method.upper()
if method == "GET" and path == self.PORTAL_PATH:
require(context, "external_task.read")
return HTTPStatus.OK, self._portal_snapshot()
if not path.startswith(f"{self.INTERNAL_PREFIX}/"):
return HTTPStatus.NOT_FOUND, {
"error": "not_found",
"message": "execution plan route not found",
}
remainder = urllib.parse.unquote(path[len(self.INTERNAL_PREFIX) + 1 :])
parts = tuple(item for item in remainder.split("/") if item)
if len(parts) != 2:
return HTTPStatus.NOT_FOUND, {
"error": "not_found",
"message": "execution plan route not found",
}
plan_id, action = parts
query = urllib.parse.parse_qs(parsed.query)
executor_type = self._require_provider(
context,
str(query.get("executor", [""])[0]),
)
if method == "GET" and action == "discover":
items = self.coordinator.discover_ready(
plan_id=plan_id,
executor_type=executor_type,
)
return HTTPStatus.OK, {
"plan_id": plan_id,
"executor_type": executor_type,
"count": len(items),
"items": items,
}
if method == "GET" and action == "snapshot":
return HTTPStatus.OK, self.coordinator.snapshot(plan_id)
if method == "GET" and action == "gate":
return HTTPStatus.OK, self.coordinator.gate(plan_id)
return HTTPStatus.NOT_FOUND, {
"error": "not_found",
"message": "execution plan route not found",
}