close S64/R116 with explicit route-plane governance and invariant-gated MAE validation

This commit is contained in:
rookiestar28
2026-02-19 03:03:03 +08:00
parent b948852eaa
commit 242bd98db4
23 changed files with 516 additions and 72 deletions
+14 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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:
"""
+16 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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."""
+14 -2
View File
@@ -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:
"""
+52
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
"""
+20
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
"""
+70 -29
View File
@@ -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:
+18 -2
View File
@@ -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:
"""
+108
View File
@@ -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 ""
}
+8 -7
View File
@@ -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):
+5
View File
@@ -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