Skip to content
Open
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
79 changes: 69 additions & 10 deletions frontend/README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dev": "vite",
"build": "node scripts/build.mjs",
"test": "node --test tests/*.test.mjs && npm run test:mpa-agent-info",
"test:mpa-agent-info": "vitest run tests/mpaAgentInfoRail.test.tsx tests/skillDocumentEditor.test.tsx tests/mpaCreation.test.tsx tests/mpaBasicDetails.test.tsx",
"test:mpa-agent-info": "vitest run tests/mpaAgentInfoRail.test.tsx tests/skillDocumentEditor.test.tsx tests/mpaCreation.test.tsx tests/mpaBasicDetails.test.tsx tests/deploymentCategory.test.tsx tests/agentCategoryList.test.tsx tests/customCreateCategory.test.tsx tests/mpaDeletion.test.tsx",
"check:i18n": "node scripts/check-i18n.mjs",
"test:sandbox-download-coverage": "vitest run --coverage -c vitest.sandbox-download.config.ts",
"test:mpa-cron-coverage": "vitest run --coverage -c vitest.mpa-cron.config.ts",
Expand Down
35 changes: 35 additions & 0 deletions frontend/server/deployment_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import json
from collections.abc import Callable, Mapping
from contextlib import contextmanager
from types import SimpleNamespace
from typing import Any, cast
from urllib.parse import urlsplit

Expand Down Expand Up @@ -486,6 +487,34 @@ def __init__(

def _tos_client(self):
if self._tos is None:
access_key, secret_key, session_token = self.credentials or ("", "", None)
if access_key and secret_key:
import tos

from agentkit.platform import VolcConfiguration

config = VolcConfiguration(
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
region=self.region,
provider=self.provider,
)
endpoint = config.get_service_endpoint("tos")
# TOSService does not accept explicit credentials. Preserve the
# shape consumed by _tos_buckets without SDK rediscovery.
self._tos = SimpleNamespace(
client=tos.TosClientV2(
access_key,
secret_key,
endpoint.host,
endpoint.region,
security_token=session_token or "",
),
actual_region=endpoint.region,
)
return self._tos

from agentkit.toolkit.volcengine.services.tos_service import (
TOSService,
TOSServiceConfig,
Expand Down Expand Up @@ -543,6 +572,7 @@ def list_resources(
namespace: str = "",
workspace_id: str = "",
search: str = "",
name_prefix: str = "",
page_number: int = 1,
page_size: int = 100,
) -> dict[str, Any]:
Expand All @@ -562,6 +592,10 @@ def list_resources(
for item in _tos_buckets(client)
if not item.get("Location") or item.get("Location") == service_region
]
if name_prefix:
all_items = [
item for item in all_items if item["name"].startswith(name_prefix)
]
if search:
normalized_search = search.casefold()
all_items = [
Expand Down Expand Up @@ -988,6 +1022,7 @@ async def _deployment_resources(request: Request):
namespace=str(request.query_params.get("namespace") or "").strip(),
workspace_id=str(request.query_params.get("workspaceId") or "").strip(),
search=str(request.query_params.get("search") or "").strip(),
name_prefix=str(request.query_params.get("namePrefix") or "").strip(),
page_number=page_number,
page_size=page_size,
)
Expand Down
108 changes: 61 additions & 47 deletions frontend/server/mpa/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@

from __future__ import annotations

import asyncio
import inspect
import sys
from collections.abc import Callable
from dataclasses import dataclass
import inspect
from typing import Any
from typing import Any, Literal, NoReturn

if sys.version_info >= (3, 11):
from asyncio import timeout
else:
from async_timeout import timeout

from fastapi import Header, HTTPException, Query, Request, Response, status
from pydantic import BaseModel, ConfigDict, Field

from frontend.server.mpa.operations import MpaOperationConflict, MpaOperationNotFound
from frontend.server.mpa.runtime_client import (
MpaExecutionConfigChange,
MpaProfile,
Expand All @@ -35,12 +43,12 @@
MpaAgentOperationRequest,
MpaAgentOperationService,
)
from frontend.server.mpa.operations import MpaOperationConflict
from frontend.server.mpa.operations import MpaOperationNotFound
from veadk.integrations.mpa.control_plane_client import (
MpaAgentOperationRequest as MpaAgentOperationBody,
)

MPA_DELETE_PREVIEW_TIMEOUT_SECONDS = 25.0

RuntimeAuthorizer = Callable[[Request, str, str], Any]
RuntimeConnectionResolver = Callable[
[Request, str, str, Any], tuple[str, str, str, str]
Expand Down Expand Up @@ -93,6 +101,7 @@ def mount_mpa_profile_routes(
operation_service: MpaAgentOperationService | None = None,
owner_resolver: Callable[[Request], str] | None = None,
resolve_runtime_bindings: RuntimeBindingResolver | None = None,
resolve_delete_resources: Callable[[Any, str, str], Any] | None = None,
) -> None:
def owner_id(request: Request) -> str:
return owner_resolver(request) if owner_resolver is not None else "local"
Expand All @@ -102,7 +111,7 @@ def operation_request(
request: Request,
mpa_instance_id: str,
body: MpaAgentOperationBody,
operation_kind: str,
operation_kind: Literal["create", "update"],
create: bool,
) -> MpaAgentOperationRequest:
runtime = authorize_runtime(request, body.runtime_id, body.region)
Expand Down Expand Up @@ -344,18 +353,29 @@ async def mpa_agent_delete_preview(
runtime_id: str = Query(default="", alias="runtimeId"),
region: str = Query(default="all"),
) -> dict[str, Any]:
return await _mpa_agent_delete_preview_payload(
request=request,
mpa_instance_id=mpa_instance_id,
runtime_id=runtime_id,
region=region,
runtime_client=runtime_client,
authorize_runtime=authorize_runtime,
resolve_runtime_connection=resolve_runtime_connection,
operation_service=operation_service,
owner=owner_id(request),
resolve_runtime_bindings=resolve_runtime_bindings,
)
try:
async with timeout(MPA_DELETE_PREVIEW_TIMEOUT_SECONDS):
return await _mpa_agent_delete_preview_payload(
request=request,
mpa_instance_id=mpa_instance_id,
runtime_id=runtime_id,
region=region,
runtime_client=runtime_client,
authorize_runtime=authorize_runtime,
resolve_runtime_connection=resolve_runtime_connection,
operation_service=operation_service,
owner=owner_id(request),
resolve_runtime_bindings=resolve_runtime_bindings,
resolve_delete_resources=resolve_delete_resources,
)
except asyncio.TimeoutError:
raise HTTPException(
status_code=504,
detail={
"code": "mpa_delete_preview_timeout",
"message": "MPA deletion checks timed out. Check the management database, Runtime API and cloud read permissions, then retry.",
},
) from None

@app.post(
"/web/mpa/agent-operations/{operation_id}/retry",
Expand All @@ -370,11 +390,14 @@ async def retry_agent_operation(
raise HTTPException(
status_code=503, detail="mpa_operation_service_unavailable"
)
operation_kind = str(body.operation_kind or "").strip() or (
operation_kind_value = str(body.operation_kind or "").strip() or (
"update" if body.runtime_revision.strip() else "create"
)
if operation_kind not in {"create", "update"}:
if operation_kind_value not in {"create", "update"}:
raise HTTPException(status_code=400, detail="invalid operationKind")
operation_kind: Literal["create", "update"] = (
"create" if operation_kind_value == "create" else "update"
)
mpa_instance_id = body.mpa_instance_id or (
body.target_key if operation_kind == "update" else body.runtime_id
)
Expand Down Expand Up @@ -604,8 +627,8 @@ def _result_payload(result: Any) -> dict[str, Any]:
raise HTTPException(status_code=502, detail="runtime_invalid_response")


def _raise_runtime_error(error: MpaRuntimeError) -> None:
detail = {
def _raise_runtime_error(error: MpaRuntimeError) -> NoReturn:
detail: dict[str, Any] = {
"code": error.code,
"message": error.code,
"requestId": error.request_id,
Expand All @@ -615,7 +638,7 @@ def _raise_runtime_error(error: MpaRuntimeError) -> None:
raise HTTPException(status_code=error.status_code, detail=detail) from error


def _raise_operation_conflict(error: MpaOperationConflict) -> None:
def _raise_operation_conflict(error: MpaOperationConflict) -> NoReturn:
raise HTTPException(
status_code=409,
detail={
Expand Down Expand Up @@ -661,16 +684,7 @@ def _operation_blocks_delete(
"""Return whether a lifecycle operation still represents unsafe deletion work."""
if not operation:
return False
status = str(operation.get("status") or "")
if status == "active":
return True
if status != "failed_retryable":
return False
if binding_status != "bound":
return True
if not profile:
return True
return str(profile.get("status") or "").casefold() != "applied"
return str(operation.get("status") or "") == "active"


async def _runtime_bindings(
Expand Down Expand Up @@ -762,6 +776,7 @@ async def _mpa_agent_delete_preview_payload(
operation_service: MpaAgentOperationService | None,
owner: str,
resolve_runtime_bindings: RuntimeBindingResolver | None,
resolve_delete_resources: Callable[[Any, str, str], Any] | None = None,
) -> dict[str, Any]:
active_operation = await _active_operation(
operation_service, owner, mpa_instance_id
Expand Down Expand Up @@ -804,24 +819,14 @@ async def _mpa_agent_delete_preview_payload(
binding.region,
binding.runtime,
)
profile = _result_payload(
await runtime_client.profile_status(
endpoint=endpoint,
mpa_instance_id=mpa_instance_id,
bearer_token=_bearer_value(bearer_token),
)
)
sessions = await runtime_client.list_sessions(
endpoint=endpoint,
bearer_token=_bearer_value(bearer_token),
include_a2a=True,
user_id=owner,
)
except MpaRuntimeError as error:
if error.status_code == 404:
blockers.append("orphan_runtime")
binding_status = "orphan_runtime"
else:
_raise_runtime_error(error)
_raise_runtime_error(error)
if _operation_blocks_delete(
active_operation,
binding_status=binding_status,
Expand All @@ -830,7 +835,7 @@ async def _mpa_agent_delete_preview_payload(
blockers.append("active_operation")
if _active_session_payloads(sessions):
blockers.append("active_sessions")
return _delete_preview_payload(
payload = _delete_preview_payload(
mpa_instance_id=mpa_instance_id,
binding_status=binding_status,
runtime=binding.visible,
Expand All @@ -840,6 +845,15 @@ async def _mpa_agent_delete_preview_payload(
blockers=blockers,
sessions=sessions,
)
if resolve_delete_resources is not None and not blockers:
resources = resolve_delete_resources(
binding.runtime, mpa_instance_id, binding.region
)
if inspect.isawaitable(resources):
resources = await resources
payload["cleanupPlan"][-1:-1] = resources["cleanupPlan"]
payload["retainedResources"] = resources["retainedResources"]
return payload


def _session_payload(session: Any) -> dict[str, Any]:
Expand All @@ -858,7 +872,7 @@ def _session_payload(session: Any) -> dict[str, Any]:
session_id = str(
payload.get("sessionId") or payload.get("session_id") or payload.get("id") or ""
)
result = {
result: dict[str, Any] = {
"sessionId": session_id,
"status": str(payload.get("status") or ""),
"appName": str(payload.get("appName") or payload.get("app_name") or ""),
Expand Down Expand Up @@ -1037,13 +1051,13 @@ def _mpa_agent_view_payload(


__all__ = [
"_ACTIVE_SESSION_STATUSES",
"MpaAgentOperationBody",
"MpaProfileApplyBody",
"MpaRuntimeBinding",
"MpaRuntimeSessionBody",
"MpaSessionExecutionConfigPatchBody",
"MpaSessionProfileUpgradeBody",
"_ACTIVE_SESSION_STATUSES",
"_active_operation",
"_delete_preview_payload",
"_mpa_agent_delete_preview_payload",
Expand Down
12 changes: 10 additions & 2 deletions frontend/server/mpa/runtime_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,15 @@ async def list_sessions(
endpoint: str,
bearer_token: str,
include_a2a: bool = True,
user_id: str = "",
) -> list[MpaRuntimeSessionSummary]:
response = await self._request(
"GET",
f"{endpoint.rstrip('/')}/api/v1/sessions",
headers={"Authorization": f"Bearer {bearer_token}"},
headers={
"Authorization": f"Bearer {bearer_token}",
**({"x-user-id": user_id} if user_id else {}),
},
params={"include_a2a": "true" if include_a2a else "false"},
)
return self._session_list_result(response)
Expand All @@ -220,11 +224,15 @@ async def delete_session(
endpoint: str,
session_id: str,
bearer_token: str,
user_id: str = "",
) -> None:
await self._request(
"DELETE",
f"{endpoint.rstrip('/')}/api/v1/sessions/{session_id}",
headers={"Authorization": f"Bearer {bearer_token}"},
headers={
"Authorization": f"Bearer {bearer_token}",
**({"x-user-id": user_id} if user_id else {}),
},
)

async def patch_session_execution_config(
Expand Down
Loading
Loading