mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
263 lines
7.9 KiB
Python
263 lines
7.9 KiB
Python
"""
|
|
Checkpoints API Handlers (R47).
|
|
Exposes endpoints for listing, creating, retrieving, and deleting workflow checkpoints.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from typing import Any, Dict
|
|
|
|
try:
|
|
from aiohttp import web
|
|
except ImportError:
|
|
web = None # type: ignore
|
|
|
|
if __package__ and "." in __package__:
|
|
from ..models.schemas import MAX_BODY_SIZE
|
|
from ..services.access_control import is_loopback, require_admin_token
|
|
from ..services.checkpoints import (
|
|
create_checkpoint,
|
|
delete_checkpoint,
|
|
get_checkpoint,
|
|
list_checkpoints,
|
|
)
|
|
from ..services.rate_limit import build_rate_limit_payload, check_rate_limit
|
|
from ..services.request_ip import get_client_ip
|
|
else: # pragma: no cover (test-only import mode)
|
|
from models.schemas import MAX_BODY_SIZE # type: ignore
|
|
from services.access_control import is_loopback, require_admin_token # type: ignore
|
|
from services.checkpoints import ( # type: ignore
|
|
create_checkpoint,
|
|
delete_checkpoint,
|
|
get_checkpoint,
|
|
list_checkpoints,
|
|
)
|
|
from services.rate_limit import ( # type: ignore
|
|
build_rate_limit_payload,
|
|
check_rate_limit,
|
|
)
|
|
from services.request_ip import get_client_ip # type: ignore
|
|
|
|
|
|
# R98: Endpoint Metadata
|
|
if __package__ and "." in __package__:
|
|
from ..services.endpoint_manifest import (
|
|
AuthTier,
|
|
RiskTier,
|
|
RoutePlane,
|
|
endpoint_metadata,
|
|
)
|
|
else:
|
|
from services.endpoint_manifest import (
|
|
AuthTier,
|
|
RiskTier,
|
|
RoutePlane,
|
|
endpoint_metadata,
|
|
)
|
|
|
|
|
|
logger = logging.getLogger("ComfyUI-OpenClaw.api.checkpoints")
|
|
|
|
|
|
def _json_resp(data: Dict[str, Any], status: int = 200) -> web.Response:
|
|
return web.json_response(data, status=status)
|
|
|
|
|
|
def _remote_admin_allowed() -> bool:
|
|
val = (
|
|
(
|
|
os.environ.get("OPENCLAW_ALLOW_REMOTE_ADMIN")
|
|
or os.environ.get("MOLTBOT_ALLOW_REMOTE_ADMIN")
|
|
or ""
|
|
)
|
|
.strip()
|
|
.lower()
|
|
)
|
|
return val in ("1", "true", "yes", "on")
|
|
|
|
|
|
def _deny_remote_admin_if_needed(request: web.Request) -> web.Response | None:
|
|
if _remote_admin_allowed():
|
|
return None
|
|
client_ip = get_client_ip(request)
|
|
if is_loopback(client_ip):
|
|
return None
|
|
return _json_resp(
|
|
{
|
|
"ok": False,
|
|
"error": "Remote admin access denied. Set OPENCLAW_ALLOW_REMOTE_ADMIN=1 (or legacy MOLTBOT_ALLOW_REMOTE_ADMIN=1) to allow.",
|
|
},
|
|
403,
|
|
)
|
|
|
|
|
|
@endpoint_metadata(
|
|
auth=AuthTier.ADMIN,
|
|
risk=RiskTier.LOW,
|
|
summary="List checkpoints",
|
|
description="List available workflow checkpoints.",
|
|
audit="checkpoints.list",
|
|
plane=RoutePlane.ADMIN,
|
|
)
|
|
async def list_checkpoints_handler(request: web.Request) -> web.Response:
|
|
"""GET /openclaw/checkpoints"""
|
|
if web is None:
|
|
raise RuntimeError("aiohttp not available")
|
|
|
|
if not check_rate_limit(request, "admin"):
|
|
return _json_resp(
|
|
build_rate_limit_payload(
|
|
request,
|
|
"admin",
|
|
error="rate_limit_exceeded",
|
|
include_ok=True,
|
|
),
|
|
429,
|
|
)
|
|
|
|
allowed, error = require_admin_token(request)
|
|
if not allowed:
|
|
return _json_resp({"ok": False, "error": error or "unauthorized"}, 403)
|
|
deny_resp = _deny_remote_admin_if_needed(request)
|
|
if deny_resp:
|
|
return deny_resp
|
|
|
|
try:
|
|
checkpoints = list_checkpoints()
|
|
return _json_resp({"ok": True, "checkpoints": checkpoints})
|
|
except Exception as e:
|
|
logger.exception("List checkpoints failed")
|
|
return _json_resp({"ok": False, "error": str(e)}, 500)
|
|
|
|
|
|
@endpoint_metadata(
|
|
auth=AuthTier.ADMIN,
|
|
risk=RiskTier.MEDIUM,
|
|
summary="Create checkpoint",
|
|
description="Create a new workflow checkpoint.",
|
|
audit="checkpoints.create",
|
|
plane=RoutePlane.ADMIN,
|
|
)
|
|
async def create_checkpoint_handler(request: web.Request) -> web.Response:
|
|
"""POST /openclaw/checkpoints"""
|
|
if web is None:
|
|
raise RuntimeError("aiohttp not available")
|
|
|
|
if not check_rate_limit(request, "admin"):
|
|
return _json_resp(
|
|
build_rate_limit_payload(
|
|
request,
|
|
"admin",
|
|
error="rate_limit_exceeded",
|
|
include_ok=True,
|
|
),
|
|
429,
|
|
)
|
|
|
|
# Body Size Check
|
|
if request.content_length and request.content_length > MAX_BODY_SIZE:
|
|
return _json_resp({"ok": False, "error": "payload_too_large"}, 413)
|
|
|
|
try:
|
|
data = await request.json()
|
|
except Exception:
|
|
return _json_resp({"ok": False, "error": "invalid_json"}, 400)
|
|
|
|
# Admin boundary (localhost convenience mode if no token configured)
|
|
allowed, error = require_admin_token(request)
|
|
if not allowed:
|
|
return _json_resp({"ok": False, "error": error or "unauthorized"}, 403)
|
|
deny_resp = _deny_remote_admin_if_needed(request)
|
|
if deny_resp:
|
|
return deny_resp
|
|
|
|
# Extract info
|
|
workflow = data.get("workflow") or data.get("prompt")
|
|
name = data.get("name", "Untitled Snapshot")
|
|
description = data.get("description", "")
|
|
|
|
if not workflow or not isinstance(workflow, dict):
|
|
return _json_resp({"ok": False, "error": "missing_workflow"}, 400)
|
|
|
|
try:
|
|
meta = create_checkpoint(name, workflow, description)
|
|
return _json_resp({"ok": True, "checkpoint": meta}, 201)
|
|
except ValueError as e:
|
|
return _json_resp(
|
|
{"ok": False, "error": "validation_error", "detail": str(e)}, 400
|
|
)
|
|
except Exception as e:
|
|
logger.exception("Create checkpoint failed")
|
|
return _json_resp({"ok": False, "error": str(e)}, 500)
|
|
|
|
|
|
@endpoint_metadata(
|
|
auth=AuthTier.ADMIN,
|
|
risk=RiskTier.LOW,
|
|
summary="Get checkpoint",
|
|
description="Retrieve specific checkpoint details.",
|
|
audit="checkpoints.get",
|
|
plane=RoutePlane.ADMIN,
|
|
)
|
|
async def get_checkpoint_handler(request: web.Request) -> web.Response:
|
|
"""GET /openclaw/checkpoints/{id}"""
|
|
if web is None:
|
|
raise RuntimeError("aiohttp not available")
|
|
|
|
checkpoint_id = request.match_info.get("id")
|
|
if not checkpoint_id:
|
|
return _json_resp({"ok": False, "error": "missing_id"}, 400)
|
|
|
|
allowed, error = require_admin_token(request)
|
|
if not allowed:
|
|
return _json_resp({"ok": False, "error": error or "unauthorized"}, 403)
|
|
deny_resp = _deny_remote_admin_if_needed(request)
|
|
if deny_resp:
|
|
return deny_resp
|
|
|
|
try:
|
|
data = get_checkpoint(checkpoint_id)
|
|
if not data:
|
|
return _json_resp({"ok": False, "error": "not_found"}, 404)
|
|
return _json_resp({"ok": True, "checkpoint": data})
|
|
except Exception as e:
|
|
logger.exception("Get checkpoint failed")
|
|
return _json_resp({"ok": False, "error": str(e)}, 500)
|
|
|
|
|
|
@endpoint_metadata(
|
|
auth=AuthTier.ADMIN,
|
|
risk=RiskTier.MEDIUM,
|
|
summary="Delete checkpoint",
|
|
description="Delete a workflow checkpoint.",
|
|
audit="checkpoints.delete",
|
|
plane=RoutePlane.ADMIN,
|
|
)
|
|
async def delete_checkpoint_handler(request: web.Request) -> web.Response:
|
|
"""DELETE /openclaw/checkpoints/{id}"""
|
|
if web is None:
|
|
raise RuntimeError("aiohttp not available")
|
|
|
|
checkpoint_id = request.match_info.get("id")
|
|
if not checkpoint_id:
|
|
return _json_resp({"ok": False, "error": "missing_id"}, 400)
|
|
|
|
allowed, error = require_admin_token(request)
|
|
if not allowed:
|
|
return _json_resp({"ok": False, "error": error or "unauthorized"}, 403)
|
|
deny_resp = _deny_remote_admin_if_needed(request)
|
|
if deny_resp:
|
|
return deny_resp
|
|
|
|
try:
|
|
deleted = delete_checkpoint(checkpoint_id)
|
|
if not deleted:
|
|
return _json_resp({"ok": False, "error": "not_found"}, 404)
|
|
return _json_resp({"ok": True})
|
|
except Exception as e:
|
|
logger.exception("Delete checkpoint failed")
|
|
return _json_resp({"ok": False, "error": str(e)}, 500)
|