mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
close S64/R116 with explicit route-plane governance and invariant-gated MAE validation
This commit is contained in:
+14
-2
@@ -18,9 +18,19 @@ except ImportError:
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.assist")
|
||||
|
||||
@@ -41,6 +51,7 @@ class AssistHandlers:
|
||||
summary="Run planner",
|
||||
description="Generate prompts from requirements via LLM.",
|
||||
audit="assist.planner",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def planner_handler(self, request):
|
||||
"""
|
||||
@@ -97,6 +108,7 @@ class AssistHandlers:
|
||||
summary="Run refiner",
|
||||
description="Refine prompt/parameters based on feedback.",
|
||||
audit="assist.refiner",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def refiner_handler(self, request):
|
||||
"""
|
||||
|
||||
+19
-2
@@ -57,9 +57,19 @@ except ImportError:
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.bridge")
|
||||
|
||||
@@ -129,6 +139,7 @@ class BridgeHandlers:
|
||||
risk=RiskTier.LOW,
|
||||
summary="Bridge health",
|
||||
description="Returns bridge health status.",
|
||||
plane=RoutePlane.USER,
|
||||
)
|
||||
async def health_handler(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -168,6 +179,7 @@ class BridgeHandlers:
|
||||
risk=RiskTier.LOW,
|
||||
summary="Bridge handshake",
|
||||
description="Negotiate protocol version.",
|
||||
plane=RoutePlane.USER,
|
||||
)
|
||||
async def handshake_handler(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -199,6 +211,7 @@ class BridgeHandlers:
|
||||
summary="Bridge submit",
|
||||
description="Submit a job via sidecar bridge.",
|
||||
audit="bridge.submit",
|
||||
plane=RoutePlane.INTERNAL,
|
||||
)
|
||||
async def submit_handler(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -427,6 +440,7 @@ class BridgeHandlers:
|
||||
summary="Bridge deliver",
|
||||
description="Request outbound delivery via sidecar.",
|
||||
audit="bridge.deliver",
|
||||
plane=RoutePlane.INTERNAL,
|
||||
)
|
||||
async def deliver_handler(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -610,6 +624,7 @@ class BridgeHandlers:
|
||||
risk=RiskTier.LOW,
|
||||
summary="Worker poll",
|
||||
description="Worker polls for pending jobs.",
|
||||
plane=RoutePlane.INTERNAL,
|
||||
)
|
||||
async def worker_poll_handler(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -647,6 +662,7 @@ class BridgeHandlers:
|
||||
summary="Worker result",
|
||||
description="Worker submits completed job result.",
|
||||
audit="bridge.worker.result",
|
||||
plane=RoutePlane.INTERNAL,
|
||||
)
|
||||
async def worker_result_handler(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -741,6 +757,7 @@ class BridgeHandlers:
|
||||
risk=RiskTier.LOW,
|
||||
summary="Worker heartbeat",
|
||||
description="Worker reports its status.",
|
||||
plane=RoutePlane.INTERNAL,
|
||||
)
|
||||
async def worker_heartbeat_handler(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
+15
-4
@@ -12,10 +12,20 @@ else:
|
||||
from services.capabilities import get_capabilities
|
||||
|
||||
|
||||
try:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
except ImportError:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, 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,
|
||||
)
|
||||
|
||||
|
||||
@endpoint_metadata(
|
||||
@@ -24,6 +34,7 @@ except ImportError:
|
||||
summary="Get capabilities",
|
||||
description="Returns API version and feature flags.",
|
||||
audit="capabilities.list",
|
||||
plane=RoutePlane.USER,
|
||||
)
|
||||
async def capabilities_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
@@ -41,9 +41,19 @@ else: # pragma: no cover (test-only import mode)
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.checkpoints")
|
||||
@@ -87,6 +97,7 @@ def _deny_remote_admin_if_needed(request: web.Request) -> web.Response | None:
|
||||
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"""
|
||||
@@ -117,6 +128,7 @@ async def list_checkpoints_handler(request: web.Request) -> web.Response:
|
||||
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"""
|
||||
@@ -169,6 +181,7 @@ async def create_checkpoint_handler(request: web.Request) -> web.Response:
|
||||
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}"""
|
||||
@@ -202,6 +215,7 @@ async def get_checkpoint_handler(request: web.Request) -> web.Response:
|
||||
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}"""
|
||||
|
||||
+17
-2
@@ -149,10 +149,20 @@ def _cache_get(key: tuple):
|
||||
|
||||
# S14/R98: Import Endpoint Metadata
|
||||
try:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
except ImportError:
|
||||
# Test fallback
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
|
||||
# Provider catalog for UI dropdown (R16 dynamic)
|
||||
@@ -196,6 +206,7 @@ except ImportError:
|
||||
summary="Get configuration",
|
||||
description="Returns effective config, sources, and provider catalog.",
|
||||
audit="config.read",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def config_get_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -311,6 +322,7 @@ def _extract_models_from_payload(payload: dict) -> list:
|
||||
summary="List remote models",
|
||||
description="Fetch a remote model list (best-effort) for OpenAI-compatible providers.",
|
||||
audit="llm.list_models",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def llm_models_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -528,6 +540,7 @@ async def llm_models_handler(request: web.Request) -> web.Response:
|
||||
summary="Update configuration",
|
||||
description="Updates non-secret LLM config.",
|
||||
audit="config.update",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def config_put_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -666,6 +679,7 @@ async def config_put_handler(request: web.Request) -> web.Response:
|
||||
summary="Test LLM connection",
|
||||
description="Tests LLM connection using provided or stored credentials.",
|
||||
audit="llm.test_connection",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def llm_test_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -862,6 +876,7 @@ async def llm_test_handler(request: web.Request) -> web.Response:
|
||||
summary="Chat completion",
|
||||
description="Run a simple chat completion using server-side LLM config.",
|
||||
audit="llm.chat_completion",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def llm_chat_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
+14
-2
@@ -42,9 +42,19 @@ SSE_MAX_DURATION_SEC = 300 # 5 minutes
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
|
||||
@endpoint_metadata(
|
||||
@@ -53,6 +63,7 @@ else:
|
||||
summary="Stream job events",
|
||||
description="SSE endpoint for job lifecycle events.",
|
||||
audit="events.stream",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def events_stream_handler(request: web.Request) -> web.StreamResponse:
|
||||
"""
|
||||
@@ -152,6 +163,7 @@ async def events_stream_handler(request: web.Request) -> web.StreamResponse:
|
||||
summary="Poll job events",
|
||||
description="JSON polling fallback for job events.",
|
||||
audit="events.poll",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def events_poll_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
+16
-2
@@ -48,9 +48,19 @@ else:
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
# Strict pattern for pack name/version URL route parameters.
|
||||
_SAFE_SEGMENT_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
|
||||
@@ -93,6 +103,7 @@ class PacksHandlers:
|
||||
summary="List packs",
|
||||
description="List installed packs.",
|
||||
audit="packs.list",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def list_packs_handler(self, request: web.Request) -> web.Response:
|
||||
"""GET /packs - List installed packs."""
|
||||
@@ -125,6 +136,7 @@ class PacksHandlers:
|
||||
summary="Import pack",
|
||||
description="Install pack from zip upload.",
|
||||
audit="packs.import",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def import_pack_handler(self, request: web.Request) -> web.Response:
|
||||
"""POST /packs/import - Install pack from zip upload."""
|
||||
@@ -176,6 +188,7 @@ class PacksHandlers:
|
||||
summary="Delete pack",
|
||||
description="Uninstall pack.",
|
||||
audit="packs.delete",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def delete_pack_handler(self, request: web.Request) -> web.Response:
|
||||
"""DELETE /packs/{name}/{version} - Uninstall pack."""
|
||||
@@ -215,6 +228,7 @@ class PacksHandlers:
|
||||
summary="Export pack",
|
||||
description="Download pack zip.",
|
||||
audit="packs.export",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def export_pack_handler(self, request: web.Request) -> web.Response:
|
||||
"""GET /packs/export/{name}/{version} - Download pack zip."""
|
||||
|
||||
@@ -39,9 +39,19 @@ else: # pragma: no cover (test-only import mode)
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.preflight")
|
||||
|
||||
@@ -80,6 +90,7 @@ def _deny_remote_admin_if_needed(request: web.Request) -> web.Response | None:
|
||||
summary="Run preflight check",
|
||||
description="Analyze workflow JSON for missing nodes and models.",
|
||||
audit="preflight.analyze",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def preflight_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -153,6 +164,7 @@ async def preflight_handler(request: web.Request) -> web.Response:
|
||||
summary="Get inventory",
|
||||
description="Returns a snapshot of available nodes and models.",
|
||||
audit="preflight.inventory",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def inventory_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
@@ -12,10 +12,22 @@ from aiohttp import web
|
||||
|
||||
try:
|
||||
from ..services.access_control import require_admin_token
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
from ..services.presets import Preset, preset_store
|
||||
except ImportError:
|
||||
# Fallback for ComfyUI's non-package loader or ad-hoc imports.
|
||||
from services.access_control import require_admin_token
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
from services.presets import Preset, preset_store
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.presets")
|
||||
@@ -24,6 +36,14 @@ logger = logging.getLogger("ComfyUI-OpenClaw.api.presets")
|
||||
class PresetHandlers:
|
||||
"""Handlers for preset API."""
|
||||
|
||||
@endpoint_metadata(
|
||||
auth=AuthTier.PUBLIC, # Conditionally public
|
||||
risk=RiskTier.LOW,
|
||||
summary="List presets",
|
||||
description="List available presets (dynamic auth).",
|
||||
audit="presets.list",
|
||||
plane=RoutePlane.USER,
|
||||
)
|
||||
async def list_presets(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
GET /moltbot/presets
|
||||
@@ -57,6 +77,14 @@ class PresetHandlers:
|
||||
presets = preset_store.list_presets(category=category, tag=tag)
|
||||
return web.json_response([p.to_dict() for p in presets])
|
||||
|
||||
@endpoint_metadata(
|
||||
auth=AuthTier.PUBLIC, # Conditionally public
|
||||
risk=RiskTier.LOW,
|
||||
summary="Get preset",
|
||||
description="Get preset details (dynamic auth).",
|
||||
audit="presets.get",
|
||||
plane=RoutePlane.USER,
|
||||
)
|
||||
async def get_preset(self, request: web.Request) -> web.Response:
|
||||
"""GET /moltbot/presets/{preset_id}"""
|
||||
# Milestone B: Auth Check
|
||||
@@ -86,6 +114,14 @@ class PresetHandlers:
|
||||
|
||||
return web.json_response(preset.to_dict())
|
||||
|
||||
@endpoint_metadata(
|
||||
auth=AuthTier.ADMIN,
|
||||
risk=RiskTier.MEDIUM,
|
||||
summary="Create preset",
|
||||
description="Create a new preset.",
|
||||
audit="presets.create",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def create_preset(self, request: web.Request) -> web.Response:
|
||||
"""POST /moltbot/presets"""
|
||||
allowed, error = require_admin_token(request)
|
||||
@@ -128,6 +164,14 @@ class PresetHandlers:
|
||||
logger.error(f"Failed to create preset: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@endpoint_metadata(
|
||||
auth=AuthTier.ADMIN,
|
||||
risk=RiskTier.MEDIUM,
|
||||
summary="Update preset",
|
||||
description="Update an existing preset.",
|
||||
audit="presets.update",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def update_preset(self, request: web.Request) -> web.Response:
|
||||
"""PUT /moltbot/presets/{preset_id}"""
|
||||
allowed, error = require_admin_token(request)
|
||||
@@ -170,6 +214,14 @@ class PresetHandlers:
|
||||
|
||||
return web.json_response(preset.to_dict())
|
||||
|
||||
@endpoint_metadata(
|
||||
auth=AuthTier.ADMIN,
|
||||
risk=RiskTier.HIGH,
|
||||
summary="Delete preset",
|
||||
description="Delete a preset.",
|
||||
audit="presets.delete",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def delete_preset(self, request: web.Request) -> web.Response:
|
||||
"""DELETE /moltbot/presets/{preset_id}"""
|
||||
allowed, error = require_admin_token(request)
|
||||
|
||||
+16
-2
@@ -14,9 +14,19 @@ import time
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
try:
|
||||
from aiohttp import web # type: ignore
|
||||
@@ -186,6 +196,7 @@ def _ensure_observability_deps_ready() -> tuple[bool, str | None]:
|
||||
summary="Health check",
|
||||
description="Returns pack status, uptime, dependencies, and stats.",
|
||||
audit="health.check",
|
||||
plane=RoutePlane.USER,
|
||||
)
|
||||
async def health_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -324,6 +335,7 @@ async def health_handler(request: web.Request) -> web.Response:
|
||||
summary="Tail logs",
|
||||
description="Returns the last N lines of the log file.",
|
||||
audit="logs.tail",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def logs_tail_handler(request: web.Request) -> web.Response:
|
||||
"""GET /moltbot/logs/tail - Returns the last N lines of the log file."""
|
||||
@@ -417,6 +429,7 @@ async def logs_tail_handler(request: web.Request) -> web.Response:
|
||||
summary="List jobs",
|
||||
description="Stub endpoint for job listing.",
|
||||
audit="jobs.list",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def jobs_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -441,6 +454,7 @@ async def jobs_handler(request: web.Request) -> web.Response:
|
||||
summary="Get trace",
|
||||
description="Returns redacted timeline for a prompt.",
|
||||
audit="trace.get",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def trace_handler(request: web.Request) -> web.Response:
|
||||
"""GET /moltbot/trace/{prompt_id} - Returns trace_id and redacted timeline."""
|
||||
|
||||
+15
-2
@@ -49,9 +49,19 @@ else: # pragma: no cover (test-only import mode)
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.secrets")
|
||||
|
||||
@@ -123,6 +133,7 @@ def _rate_limit_admin(request: web.Request) -> Optional[web.Response]:
|
||||
summary="Get secret status",
|
||||
description="Returns secret configuration status (NO ACTUAL VALUES).",
|
||||
audit="secrets.status",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def secrets_status_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -165,6 +176,7 @@ async def secrets_status_handler(request: web.Request) -> web.Response:
|
||||
summary="Write secret",
|
||||
description="Save API key to server store.",
|
||||
audit="secrets.write",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def secrets_put_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -304,6 +316,7 @@ async def secrets_put_handler(request: web.Request) -> web.Response:
|
||||
summary="Delete secret",
|
||||
description="Clear provider secret.",
|
||||
audit="secrets.delete",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def secrets_delete_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
+13
-2
@@ -50,9 +50,19 @@ else: # pragma: no cover (test-only)
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.security_doctor")
|
||||
|
||||
@@ -63,6 +73,7 @@ logger = logging.getLogger("ComfyUI-OpenClaw.api.security_doctor")
|
||||
summary="Security Doctor",
|
||||
description="Run security posture diagnostics and remediation.",
|
||||
audit="security.doctor",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def security_doctor_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
+13
-2
@@ -28,9 +28,19 @@ else: # pragma: no cover (test-only import mode)
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.templates")
|
||||
|
||||
@@ -64,6 +74,7 @@ def _ensure_templates_api_deps_ready() -> tuple[bool, str | None]:
|
||||
summary="List templates",
|
||||
description="Returns templates visible to the backend.",
|
||||
audit="templates.list",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def templates_list_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
+14
-2
@@ -20,9 +20,19 @@ except ImportError:
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.tools")
|
||||
|
||||
@@ -33,6 +43,7 @@ logger = logging.getLogger("ComfyUI-OpenClaw.api.tools")
|
||||
summary="List tools",
|
||||
description="List allowed external tools.",
|
||||
audit="tools.list",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def tools_list_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
@@ -63,6 +74,7 @@ async def tools_list_handler(request: web.Request) -> web.Response:
|
||||
summary="Run tool",
|
||||
description="Execute an external tool.",
|
||||
audit="tools.run",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def tools_run_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
@@ -23,11 +23,23 @@ from aiohttp import web
|
||||
# can silently import the WRONG module (another custom node or ComfyUI-adjacent package), causing
|
||||
# template allowlists to appear "missing" even when `data/templates/manifest.json` is correct.
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
from ..services.execution_budgets import BudgetExceededError
|
||||
from ..services.templates import is_template_allowed
|
||||
from ..services.trace import generate_trace_id
|
||||
from ..services.webhook_auth import AuthError
|
||||
else: # pragma: no cover (test-only import mode)
|
||||
from services.endpoint_manifest import ( # type: ignore
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
from services.execution_budgets import BudgetExceededError # type: ignore
|
||||
from services.templates import is_template_allowed # type: ignore
|
||||
from services.trace import generate_trace_id # type: ignore
|
||||
@@ -76,6 +88,14 @@ class TriggerHandlers:
|
||||
if not allowed:
|
||||
raise AuthError(error or "Unauthorized")
|
||||
|
||||
@endpoint_metadata(
|
||||
auth=AuthTier.ADMIN,
|
||||
risk=RiskTier.HIGH,
|
||||
summary="Fire trigger",
|
||||
description="Fire an ad-hoc workflow trigger.",
|
||||
audit="triggers.fire",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def fire_trigger(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
POST /moltbot/triggers/fire
|
||||
|
||||
+13
-2
@@ -35,10 +35,20 @@ except ImportError:
|
||||
|
||||
try:
|
||||
from ..services.diagnostics_flags import diagnostics
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
except ImportError:
|
||||
from services.diagnostics_flags import diagnostics
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
# R46: Scoped logger for safe-by-default redaction
|
||||
logger = diagnostics.get_logger("ComfyUI-OpenClaw.api.webhook", "webhook")
|
||||
@@ -50,6 +60,7 @@ logger = diagnostics.get_logger("ComfyUI-OpenClaw.api.webhook", "webhook")
|
||||
summary="Webhook submit",
|
||||
description="Authenticated endpoint for external job requests (legacy pipeline).",
|
||||
audit="webhook.submit.legacy",
|
||||
plane=RoutePlane.EXTERNAL,
|
||||
)
|
||||
async def webhook_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
+13
-2
@@ -54,9 +54,19 @@ else: # pragma: no cover (test-only import mode)
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.webhook_submit")
|
||||
|
||||
@@ -74,6 +84,7 @@ def safe_error_response(status: int, error: str, detail: str = "") -> web.Respon
|
||||
summary="Webhook submit",
|
||||
description="Authenticated endpoint for external job requests (modern pipeline).",
|
||||
audit="webhook.submit",
|
||||
plane=RoutePlane.EXTERNAL,
|
||||
)
|
||||
async def webhook_submit_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
+13
-2
@@ -62,9 +62,19 @@ else: # pragma: no cover (test-only import mode)
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.api.webhook_validate")
|
||||
|
||||
@@ -84,6 +94,7 @@ def _safe_error_response(status: int, error: str, detail: str = "") -> web.Respo
|
||||
summary="Webhook validate",
|
||||
description="Dry-run validation for webhook requests.",
|
||||
audit="webhook.validate",
|
||||
plane=RoutePlane.EXTERNAL,
|
||||
)
|
||||
async def webhook_validate_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
@@ -52,6 +52,7 @@ class RoutePlane(enum.Enum):
|
||||
USER = "user" # Public user-facing routes
|
||||
ADMIN = "admin" # Admin-only management routes
|
||||
INTERNAL = "internal" # Internal-only (loopback, service mesh)
|
||||
EXTERNAL = "external" # External webhook/callback ingress surfaces
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -65,7 +66,7 @@ class EndpointMetadata:
|
||||
required_scopes: List[str] = field(default_factory=list) # For S46
|
||||
audit_action: Optional[str] = None # For R99
|
||||
# S60: Route plane classification
|
||||
route_plane: RoutePlane = RoutePlane.USER
|
||||
route_plane: Optional[RoutePlane] = None
|
||||
|
||||
|
||||
# Registry to store metadata by handler function
|
||||
@@ -79,7 +80,7 @@ def endpoint_metadata(
|
||||
description: str = "",
|
||||
scopes: Optional[List[str]] = None,
|
||||
audit: Optional[str] = None,
|
||||
plane: RoutePlane = RoutePlane.USER,
|
||||
plane: Optional[RoutePlane] = None, # R116: No implicit default allowed
|
||||
):
|
||||
"""
|
||||
Decorator to attach security metadata to a handler function.
|
||||
@@ -89,12 +90,19 @@ def endpoint_metadata(
|
||||
auth=AuthTier.ADMIN,
|
||||
risk=RiskTier.HIGH,
|
||||
summary="Restart Server",
|
||||
audit="server.restart"
|
||||
audit="server.restart",
|
||||
plane=RoutePlane.ADMIN # R116: Mandatory classification
|
||||
)
|
||||
async def handler(request): ...
|
||||
"""
|
||||
|
||||
def decorator(handler: Callable):
|
||||
# R116: Enforce explicit plane classification
|
||||
# We allow None during decoration to support legacy transition,
|
||||
# but validation gate will fail.
|
||||
# Ideally, we'd fail fast here, but that might crash import time if not all are migrated.
|
||||
# Let's verify during manifest generation instead.
|
||||
|
||||
meta = EndpointMetadata(
|
||||
auth_tier=auth,
|
||||
risk_tier=risk,
|
||||
@@ -102,7 +110,7 @@ def endpoint_metadata(
|
||||
description=description,
|
||||
required_scopes=scopes or [],
|
||||
audit_action=audit,
|
||||
route_plane=plane,
|
||||
route_plane=plane, # May be None if missed
|
||||
)
|
||||
_HANDLER_REGISTRY[handler] = meta
|
||||
# Attach to function for runtime introspection if needed
|
||||
@@ -118,6 +126,10 @@ def get_metadata(handler: Callable) -> Optional[EndpointMetadata]:
|
||||
while isinstance(handler, (functools.partial,)):
|
||||
handler = handler.func
|
||||
|
||||
# Unwrap bound methods (e.g. class instance methods)
|
||||
if inspect.ismethod(handler):
|
||||
handler = handler.__func__
|
||||
|
||||
# Check registry first
|
||||
if handler in _HANDLER_REGISTRY:
|
||||
return _HANDLER_REGISTRY[handler]
|
||||
@@ -159,7 +171,7 @@ def generate_manifest(app) -> List[Dict[str, Any]]:
|
||||
"risk": meta.risk_tier.value,
|
||||
"summary": meta.summary,
|
||||
"audit": meta.audit_action,
|
||||
"plane": meta.route_plane.value,
|
||||
"plane": meta.route_plane.value if meta.route_plane else None,
|
||||
}
|
||||
|
||||
manifest.append(entry)
|
||||
@@ -168,7 +180,7 @@ def generate_manifest(app) -> List[Dict[str, Any]]:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# S60: MAE posture validation
|
||||
# S60: MAE posture validation (Enhanced by S64)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
import logging as _logging
|
||||
@@ -181,42 +193,71 @@ def validate_mae_posture(
|
||||
profile: str = "local",
|
||||
) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
S60: Validate that the endpoint manifest respects route-plane segmentation
|
||||
for the given deployment profile.
|
||||
S60/R116: Validate that the endpoint manifest respects route-plane segmentation
|
||||
and S64 invariants for the given deployment profile.
|
||||
|
||||
In 'public' or 'hardened' profiles:
|
||||
- No ADMIN or INTERNAL plane routes should be exposed on the user plane
|
||||
- All classified routes must have a route_plane assigned
|
||||
Enforces:
|
||||
- S64.INV.005: All routes must have explicit plane classification (R116)
|
||||
- S64.INV.006: All routes must have explicit auth classification
|
||||
- S64.INV.002: No Admin/Internal plane routes exposed on User plane (Public/Hardened)
|
||||
|
||||
Returns:
|
||||
(is_valid, violations)
|
||||
"""
|
||||
# Import here to avoid circular dependency
|
||||
if __package__ and "." in __package__:
|
||||
from .security_invariants import REGISTRY
|
||||
else:
|
||||
from services.security_invariants import REGISTRY
|
||||
|
||||
violations: List[str] = []
|
||||
|
||||
if profile not in ("public", "hardened"):
|
||||
# Local profile: no enforcement
|
||||
return True, []
|
||||
|
||||
for entry in manifest:
|
||||
method = entry.get("method")
|
||||
path = entry.get("path")
|
||||
meta = entry.get("metadata")
|
||||
if not meta:
|
||||
# Unclassified route in public profile is a violation
|
||||
if entry.get("method") != "*": # Skip catch-all routes
|
||||
violations.append(
|
||||
f"Unclassified route in {profile} profile: "
|
||||
f"{entry.get('method')} {entry.get('path')}"
|
||||
)
|
||||
|
||||
# Skip catch-all or unmanaged routes if they are truly outside our scope
|
||||
if method == "*":
|
||||
continue
|
||||
|
||||
plane = meta.get("plane", "user")
|
||||
auth = meta.get("auth", "public")
|
||||
|
||||
# ADMIN and INTERNAL plane routes must not be exposed without admin auth
|
||||
if plane in ("admin", "internal") and auth == "public":
|
||||
# S64.INV.005: Explicit Classification Gate
|
||||
if not meta:
|
||||
inv = REGISTRY["S64.INV.005"]
|
||||
violations.append(
|
||||
f"S60: {plane}-plane route exposed with public auth: "
|
||||
f"{entry.get('method')} {entry.get('path')}"
|
||||
f"[{inv.id}] Unclassified route (missing metadata): "
|
||||
f"{method} {path}. {inv.remediation}"
|
||||
)
|
||||
continue
|
||||
|
||||
# S64.INV.006: Explicit Auth Tier
|
||||
auth = meta.get("auth")
|
||||
if not auth:
|
||||
inv = REGISTRY["S64.INV.006"]
|
||||
violations.append(
|
||||
f"[{inv.id}] Route missing explicit Auth classification: "
|
||||
f"{method} {path}. {inv.remediation}"
|
||||
)
|
||||
|
||||
# S64.INV.005: Plane Check
|
||||
plane = meta.get("plane")
|
||||
if not plane:
|
||||
inv = REGISTRY["S64.INV.005"]
|
||||
violations.append(
|
||||
f"[{inv.id}] Route missing explicit Plane classification: "
|
||||
f"{method} {path}. {inv.remediation}"
|
||||
)
|
||||
continue
|
||||
|
||||
# Posture Checks (Profile Dependent)
|
||||
if profile in ("public", "hardened"):
|
||||
# S64.INV.002: Plane/Auth Compatibility
|
||||
if plane in ("admin", "internal") and auth == "public":
|
||||
inv = REGISTRY["S64.INV.002"]
|
||||
violations.append(
|
||||
f"[{inv.id}] {plane}-plane route exposed with public auth: "
|
||||
f"{method} {path}. {inv.remediation}"
|
||||
)
|
||||
|
||||
if violations:
|
||||
for v in violations:
|
||||
|
||||
@@ -28,9 +28,19 @@ else: # pragma: no cover (test-only import mode)
|
||||
|
||||
# R98: Endpoint Metadata
|
||||
if __package__ and "." in __package__:
|
||||
from ..services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from ..services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
else:
|
||||
from services.endpoint_manifest import AuthTier, RiskTier, endpoint_metadata
|
||||
from services.endpoint_manifest import (
|
||||
AuthTier,
|
||||
RiskTier,
|
||||
RoutePlane,
|
||||
endpoint_metadata,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("ComfyUI-OpenClaw.services.parameter_lab")
|
||||
|
||||
@@ -382,6 +392,7 @@ def _require_admin(request: web.Request) -> Optional[web.Response]:
|
||||
summary="Create comparison",
|
||||
description="Create a bounded multi-model comparison plan.",
|
||||
audit="lab.compare.create",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def create_compare_handler(request: web.Request) -> web.Response:
|
||||
if web is None:
|
||||
@@ -433,6 +444,7 @@ async def create_compare_handler(request: web.Request) -> web.Response:
|
||||
summary="Create sweep",
|
||||
description="Create a bounded parameter sweep plan.",
|
||||
audit="lab.sweep.create",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def create_sweep_handler(request: web.Request) -> web.Response:
|
||||
if web is None:
|
||||
@@ -470,6 +482,7 @@ async def create_sweep_handler(request: web.Request) -> web.Response:
|
||||
summary="List experiments",
|
||||
description="List persistent experiments.",
|
||||
audit="lab.list",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def list_experiments_handler(request: web.Request) -> web.Response:
|
||||
if web is None:
|
||||
@@ -489,6 +502,7 @@ async def list_experiments_handler(request: web.Request) -> web.Response:
|
||||
summary="Get experiment",
|
||||
description="Retrieve experiment details.",
|
||||
audit="lab.get",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def get_experiment_handler(request: web.Request) -> web.Response:
|
||||
if web is None:
|
||||
@@ -514,6 +528,7 @@ async def get_experiment_handler(request: web.Request) -> web.Response:
|
||||
summary="Update experiment",
|
||||
description="Update experiment state (e.g. run results).",
|
||||
audit="lab.update",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def update_experiment_handler(request: web.Request) -> web.Response:
|
||||
if web is None:
|
||||
@@ -550,6 +565,7 @@ async def update_experiment_handler(request: web.Request) -> web.Response:
|
||||
summary="Select winner",
|
||||
description="Select experiment winner and return params.",
|
||||
audit="lab.winner",
|
||||
plane=RoutePlane.ADMIN,
|
||||
)
|
||||
async def select_apply_winner_handler(request: web.Request) -> web.Response:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
S64 Security Invariants Registry.
|
||||
|
||||
This module defines the canonical security invariants that must be true
|
||||
for the application to be considered secure. These invariants are
|
||||
enforced locally by the Security Gate (S41) and in CI by policy checks.
|
||||
|
||||
Artifact Ownership:
|
||||
- Public Posture: unreachable surfaces (admin-plane leaks)
|
||||
- Admin Plane: capabilities restricted to admin token
|
||||
- Fail-Closed: missing security controls block startup
|
||||
"""
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class InvariantScope(enum.Enum):
|
||||
STARTUP = "startup"
|
||||
RUNTIME = "runtime"
|
||||
CI = "ci"
|
||||
|
||||
|
||||
class InvariantSeverity(enum.Enum):
|
||||
CRITICAL = "critical" # Must block startup/CI
|
||||
HIGH = "high" # Should block, overrideable in DEV
|
||||
WARNING = "warning" # Audit only
|
||||
|
||||
|
||||
@dataclass
|
||||
class SecurityInvariant:
|
||||
id: str
|
||||
scope: InvariantScope
|
||||
severity: InvariantSeverity
|
||||
description: str
|
||||
remediation: str
|
||||
|
||||
|
||||
# Canonical Registry of Security Invariants
|
||||
REGISTRY: Dict[str, SecurityInvariant] = {
|
||||
# Public Posture / Network Invariants
|
||||
"S64.INV.001": SecurityInvariant(
|
||||
id="S64.INV.001",
|
||||
scope=InvariantScope.STARTUP,
|
||||
severity=InvariantSeverity.CRITICAL,
|
||||
description="Admin-plane routes must not be exposed on public interfaces without explicit auth override.",
|
||||
remediation="Configure OPENCLAW_ADMIN_TOKEN or bind to localhost only."
|
||||
),
|
||||
"S64.INV.002": SecurityInvariant(
|
||||
id="S64.INV.002",
|
||||
scope=InvariantScope.STARTUP,
|
||||
severity=InvariantSeverity.CRITICAL,
|
||||
description="Public ingress must not bypass MAE route segmentation (no Admin/Internal on User plane).",
|
||||
remediation="Check route configuration and deployment profile (OPENCLAW_DEPLOYMENT_PROFILE)."
|
||||
),
|
||||
|
||||
# Fail-Closed Invariants
|
||||
"S64.INV.003": SecurityInvariant(
|
||||
id="S64.INV.003",
|
||||
scope=InvariantScope.STARTUP,
|
||||
severity=InvariantSeverity.CRITICAL,
|
||||
description="Missing critical security secrets (Tokens/Keys) must block startup in Hardened/Public modes.",
|
||||
remediation="Provide required secrets (OPENCLAW_ADMIN_TOKEN, keys) or switch to Local profile."
|
||||
),
|
||||
"S64.INV.004": SecurityInvariant(
|
||||
id="S64.INV.004",
|
||||
scope=InvariantScope.STARTUP,
|
||||
severity=InvariantSeverity.CRITICAL,
|
||||
description="Failed module adapters must not degrade into 'open' state.",
|
||||
remediation="Check module initialization logs. Ensure fail-closed logic is active."
|
||||
),
|
||||
|
||||
# Metadata / Governance Invariants (R116)
|
||||
"S64.INV.005": SecurityInvariant(
|
||||
id="S64.INV.005",
|
||||
scope=InvariantScope.CI,
|
||||
severity=InvariantSeverity.CRITICAL,
|
||||
description="All managed routes must have explicit Route Plane classification.",
|
||||
remediation="Decorate route handler with @endpoint_metadata(plane=...)."
|
||||
),
|
||||
"S64.INV.006": SecurityInvariant(
|
||||
id="S64.INV.006",
|
||||
scope=InvariantScope.CI,
|
||||
severity=InvariantSeverity.CRITICAL,
|
||||
description="All managed routes must have explicit Auth Tier classification.",
|
||||
remediation="Decorate route handler with @endpoint_metadata(auth=...)."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class InvariantViolation:
|
||||
invariant_id: str
|
||||
context: str
|
||||
evidence: str
|
||||
|
||||
def to_dict(self):
|
||||
inv = REGISTRY.get(self.invariant_id)
|
||||
return {
|
||||
"code": self.invariant_id,
|
||||
"severity": inv.severity.value if inv else "unknown",
|
||||
"scope": inv.scope.value if inv else "unknown",
|
||||
"description": inv.description if inv else "Unknown Invariant",
|
||||
"context": self.context,
|
||||
"evidence": self.evidence,
|
||||
"remediation": inv.remediation if inv else ""
|
||||
}
|
||||
@@ -25,19 +25,20 @@ class TestS60RoutePlane(unittest.TestCase):
|
||||
"""S60: RoutePlane enum and EndpointMetadata classification tests."""
|
||||
|
||||
def test_route_plane_enum_values(self):
|
||||
"""RoutePlane has USER, ADMIN, INTERNAL."""
|
||||
"""RoutePlane has USER, ADMIN, INTERNAL, EXTERNAL."""
|
||||
self.assertEqual(RoutePlane.USER.value, "user")
|
||||
self.assertEqual(RoutePlane.ADMIN.value, "admin")
|
||||
self.assertEqual(RoutePlane.INTERNAL.value, "internal")
|
||||
self.assertEqual(RoutePlane.EXTERNAL.value, "external")
|
||||
|
||||
def test_endpoint_metadata_default_plane_is_user(self):
|
||||
"""EndpointMetadata defaults to USER plane."""
|
||||
def test_endpoint_metadata_default_plane_is_none(self):
|
||||
"""EndpointMetadata defaults to None (explicit classification required)."""
|
||||
meta = EndpointMetadata(
|
||||
auth_tier=AuthTier.PUBLIC,
|
||||
risk_tier=RiskTier.LOW,
|
||||
summary="Test endpoint",
|
||||
)
|
||||
self.assertEqual(meta.route_plane, RoutePlane.USER)
|
||||
self.assertIsNone(meta.route_plane)
|
||||
|
||||
def test_endpoint_metadata_admin_plane(self):
|
||||
"""EndpointMetadata can be set to ADMIN plane."""
|
||||
@@ -79,8 +80,8 @@ class TestS60DecoratorPlane(unittest.TestCase):
|
||||
self.assertIsNotNone(meta)
|
||||
self.assertEqual(meta.route_plane, RoutePlane.ADMIN)
|
||||
|
||||
def test_decorator_default_plane_is_user(self):
|
||||
"""Decorator defaults to USER plane."""
|
||||
def test_decorator_default_plane_is_none(self):
|
||||
"""Decorator defaults to None (explicit classification required)."""
|
||||
|
||||
@endpoint_metadata(
|
||||
auth=AuthTier.PUBLIC,
|
||||
@@ -92,7 +93,7 @@ class TestS60DecoratorPlane(unittest.TestCase):
|
||||
|
||||
meta = getattr(public_handler, "__openclaw_meta__", None)
|
||||
self.assertIsNotNone(meta)
|
||||
self.assertEqual(meta.route_plane, RoutePlane.USER)
|
||||
self.assertIsNone(meta.route_plane)
|
||||
|
||||
|
||||
class TestS60MAEPostureValidation(unittest.TestCase):
|
||||
|
||||
@@ -29,6 +29,11 @@ except ModuleNotFoundError: # pragma: no cover
|
||||
|
||||
_AIOHTTP_AVAILABLE = False
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from api.webhook_validate import webhook_validate_handler
|
||||
from models.schemas import WebhookJobRequest
|
||||
from services.execution_budgets import BudgetExceededError
|
||||
|
||||
Reference in New Issue
Block a user