Files
ComfyUI-OpenClaw/api/routes.py
T

746 lines
26 KiB
Python

"""
API routes for observability endpoints.
Registers /openclaw/* endpoints (and legacy /moltbot/*) against ComfyUI PromptServer.
"""
# IMPORTANT: __future__ imports MUST be the first non-docstring line in the file.
# Do not move this import or insert code above it, or ComfyUI route registration will fail.
from __future__ import annotations
import os
from typing import cast
if __package__ and "." in __package__:
from ..services.import_fallback import import_attrs_dual
else:
from services.import_fallback import import_attrs_dual # type: ignore
# IMPORTANT: route-family helpers must stay one-way imported from here.
# Do not make api.route_registrars import api.routes back, or bootstrap can
# regress into circular import failures before PromptServer registration.
(
build_assist_route_specs,
build_connector_installation_route_specs,
build_core_route_specs,
build_pack_route_specs,
register_route_family,
) = import_attrs_dual(
__package__,
"..api.route_registrars",
"api.route_registrars",
(
"build_assist_route_specs",
"build_connector_installation_route_specs",
"build_core_route_specs",
"build_pack_route_specs",
"register_route_family",
),
)
(
RouteHandlerDependencies,
emit_jobs_list_audit,
health_response,
jobs_response,
logs_tail_response,
owned_ensure_observability_deps_ready,
trace_response,
) = import_attrs_dual(
__package__,
"..api.route_handlers",
"api.route_handlers",
(
"RouteHandlerDependencies",
"emit_jobs_list_audit",
"health_response",
"jobs_response",
"logs_tail_response",
"ensure_observability_deps_ready",
"trace_response",
),
)
(
RouteRegistrationDependencies,
orchestrate_dual_route,
register_route_families,
run_mae_startup_gate,
) = import_attrs_dual(
__package__,
"..api.route_orchestration",
"api.route_orchestration",
(
"RouteRegistrationDependencies",
"register_dual_route",
"register_route_families",
"run_mae_startup_gate",
),
)
# R98 / R64: Endpoint Metadata import via shared helper
(
AuthTier,
RiskTier,
RoutePlane,
endpoint_metadata,
) = import_attrs_dual(
__package__,
"..services.endpoint_manifest",
"services.endpoint_manifest",
("AuthTier", "RiskTier", "RoutePlane", "endpoint_metadata"),
)
(build_legacy_route_deprecation_headers,) = import_attrs_dual(
__package__,
"..services.legacy_compat",
"services.legacy_compat",
("build_legacy_route_deprecation_headers",),
)
try:
from aiohttp import web # type: ignore
except ModuleNotFoundError: # pragma: no cover (optional for unit tests)
web = None # type: ignore
PACK_NAME = PACK_VERSION = PACK_START_TIME = LOG_FILE = get_api_key = None # type: ignore
metrics = tail_log = require_observability_access = check_rate_limit = trace_store = None # type: ignore
require_admin_token = resolve_token_info = emit_audit_event = None # type: ignore
jobs_request_tenant_scope = normalize_jobs_query = build_jobs_audit_details = None # type: ignore
SAFE_JOB_AUDIT_OUTCOMES = None # type: ignore
JobsSecurityError = TenantBoundaryError = None # type: ignore
JobsHostContractUnsupported = JobsBackendUnavailable = read_jobs = None # type: ignore
get_executor_diagnostics = None # type: ignore
webhook_handler = webhook_submit_handler = webhook_validate_handler = capabilities_handler = preflight_handler = None # type: ignore
pnginfo_handler = None # type: ignore # R168
config_get_handler = config_put_handler = llm_test_handler = llm_models_handler = llm_chat_handler = None # type: ignore
remote_admin_page_handler = None # type: ignore # F61
security_doctor_handler = None # type: ignore # S30
connector_installations_list_handler = connector_installation_get_handler = None # type: ignore
connector_installation_resolve_handler = connector_installation_audit_handler = None # type: ignore
connector_extraction_contract_handler = None # type: ignore
templates_list_handler = None # type: ignore
rewrite_recipes_list_handler = rewrite_recipe_get_handler = None # type: ignore
rewrite_recipe_create_handler = rewrite_recipe_update_handler = None # type: ignore
rewrite_recipe_delete_handler = rewrite_recipe_dry_run_handler = None # type: ignore
rewrite_recipe_apply_handler = None # type: ignore
model_search_handler = model_download_create_handler = None # type: ignore
model_download_list_handler = model_download_get_handler = None # type: ignore
model_download_cancel_handler = model_import_handler = None # type: ignore
model_installations_list_handler = None # type: ignore
secrets_status_handler = secrets_put_handler = secrets_delete_handler = None # type: ignore
list_checkpoints_handler = create_checkpoint_handler = get_checkpoint_handler = delete_checkpoint_handler = None # type: ignore
events_stream_handler = events_poll_handler = None # type: ignore # R71
redact_text = None # type: ignore
if web is not None:
# Import discipline:
# - ComfyUI runtime: package-relative imports only (prevents collisions with other custom nodes).
# - Unit tests: allow top-level imports.
(capabilities_handler,) = import_attrs_dual(
__package__,
"..api.capabilities",
"api.capabilities",
("capabilities_handler",),
)
(
connector_installations_list_handler,
connector_installation_get_handler,
connector_installation_resolve_handler,
connector_installation_audit_handler,
connector_extraction_contract_handler,
) = import_attrs_dual(
__package__,
"..api.connector_contracts",
"api.connector_contracts",
(
"connector_installations_list_handler",
"connector_installation_get_handler",
"connector_installation_resolve_handler",
"connector_installation_audit_handler",
"connector_extraction_contract_handler",
),
)
(
create_checkpoint_handler,
delete_checkpoint_handler,
get_checkpoint_handler,
list_checkpoints_handler,
) = import_attrs_dual(
__package__,
"..api.checkpoints_handler",
"api.checkpoints_handler",
(
"create_checkpoint_handler",
"delete_checkpoint_handler",
"get_checkpoint_handler",
"list_checkpoints_handler",
),
)
(
config_get_handler,
config_put_handler,
llm_chat_handler,
llm_models_handler,
llm_test_handler,
) = import_attrs_dual(
__package__,
"..api.config",
"api.config",
(
"config_get_handler",
"config_put_handler",
"llm_chat_handler",
"llm_models_handler",
"llm_test_handler",
),
)
(events_poll_handler, events_stream_handler) = import_attrs_dual( # R71
__package__,
"..api.events",
"api.events",
("events_poll_handler", "events_stream_handler"),
)
(remote_admin_page_handler,) = import_attrs_dual( # F61
__package__,
"..api.remote_admin",
"api.remote_admin",
("remote_admin_page_handler",),
)
(inventory_handler, preflight_handler) = import_attrs_dual(
__package__,
"..api.preflight_handler",
"api.preflight_handler",
("inventory_handler", "preflight_handler"),
)
(pnginfo_handler,) = import_attrs_dual(
__package__,
"..api.pnginfo",
"api.pnginfo",
("pnginfo_handler",),
)
(secrets_delete_handler, secrets_put_handler, secrets_status_handler) = (
import_attrs_dual(
__package__,
"..api.secrets",
"api.secrets",
("secrets_delete_handler", "secrets_put_handler", "secrets_status_handler"),
)
)
(security_doctor_handler,) = import_attrs_dual( # S30
__package__,
"..api.security_doctor",
"api.security_doctor",
("security_doctor_handler",),
)
(templates_list_handler,) = import_attrs_dual(
__package__,
"..api.templates",
"api.templates",
("templates_list_handler",),
)
(
rewrite_recipe_apply_handler,
rewrite_recipe_create_handler,
rewrite_recipe_delete_handler,
rewrite_recipe_dry_run_handler,
rewrite_recipe_get_handler,
rewrite_recipe_update_handler,
rewrite_recipes_list_handler,
) = import_attrs_dual(
__package__,
"..api.rewrite_recipes",
"api.rewrite_recipes",
(
"rewrite_recipe_apply_handler",
"rewrite_recipe_create_handler",
"rewrite_recipe_delete_handler",
"rewrite_recipe_dry_run_handler",
"rewrite_recipe_get_handler",
"rewrite_recipe_update_handler",
"rewrite_recipes_list_handler",
),
)
(
model_download_cancel_handler,
model_download_create_handler,
model_download_get_handler,
model_download_list_handler,
model_import_handler,
model_installations_list_handler,
model_search_handler,
) = import_attrs_dual(
__package__,
"..api.model_manager",
"api.model_manager",
(
"model_download_cancel_handler",
"model_download_create_handler",
"model_download_get_handler",
"model_download_list_handler",
"model_import_handler",
"model_installations_list_handler",
"model_search_handler",
),
)
(tools_list_handler, tools_run_handler) = import_attrs_dual( # S12
__package__,
"..api.tools",
"api.tools",
("tools_list_handler", "tools_run_handler"),
)
(webhook_handler,) = import_attrs_dual(
__package__,
"..api.webhook",
"api.webhook",
("webhook_handler",),
)
(webhook_submit_handler,) = import_attrs_dual(
__package__,
"..api.webhook_submit",
"api.webhook_submit",
("webhook_submit_handler",),
)
(webhook_validate_handler,) = import_attrs_dual(
__package__,
"..api.webhook_validate",
"api.webhook_validate",
("webhook_validate_handler",),
)
# IMPORTANT: use PACK_VERSION / PACK_START_TIME from config.
# Do NOT import VERSION or config_path (they do not exist) or route registration will fail.
(LOG_FILE, PACK_NAME, PACK_START_TIME, PACK_VERSION) = import_attrs_dual(
__package__,
"..config",
"config",
("LOG_FILE", "PACK_NAME", "PACK_START_TIME", "PACK_VERSION"),
)
# CRITICAL: These imports MUST remain present.
# If edited out, module-level placeholders stay as None and handlers raise at runtime
# (e.g., TypeError: 'NoneType' object is not callable), producing noisy aiohttp tracebacks.
(require_admin_token, require_observability_access, resolve_token_info) = (
import_attrs_dual(
__package__,
"..services.access_control",
"services.access_control",
(
"require_admin_token",
"require_observability_access",
"resolve_token_info",
),
)
)
(emit_audit_event,) = import_attrs_dual(
__package__,
"..services.audit",
"services.audit",
("emit_audit_event",),
)
(
JobsSecurityError,
SAFE_JOB_AUDIT_OUTCOMES,
TenantBoundaryError,
build_jobs_audit_details,
jobs_request_tenant_scope,
normalize_jobs_query,
) = import_attrs_dual(
__package__,
"..services.jobs_security",
"services.jobs_security",
(
"JobsSecurityError",
"SAFE_JOB_AUDIT_OUTCOMES",
"TenantBoundaryError",
"build_jobs_audit_details",
"jobs_request_tenant_scope",
"normalize_jobs_query",
),
)
(
JobsBackendUnavailable,
JobsHostContractUnsupported,
read_jobs,
) = import_attrs_dual(
__package__,
"..services.jobs_read_model",
"services.jobs_read_model",
(
"JobsBackendUnavailable",
"JobsHostContractUnsupported",
"read_jobs",
),
)
(tail_log,) = import_attrs_dual(
__package__,
"..services.log_tail",
"services.log_tail",
("tail_log",),
)
(metrics,) = import_attrs_dual(
__package__,
"..services.metrics",
"services.metrics",
("metrics",),
)
(get_executor_diagnostics,) = import_attrs_dual(
__package__,
"..services.async_utils",
"services.async_utils",
("get_executor_diagnostics",),
)
(
create_compare_handler,
create_sweep_handler,
get_experiment_handler,
list_experiments_handler,
select_apply_winner_handler,
update_experiment_handler,
) = import_attrs_dual(
__package__,
"..services.parameter_lab",
"services.parameter_lab",
(
"create_compare_handler",
"create_sweep_handler",
"get_experiment_handler",
"list_experiments_handler",
"select_apply_winner_handler",
"update_experiment_handler",
),
)
(check_rate_limit, build_rate_limit_response) = import_attrs_dual(
__package__,
"..services.rate_limit",
"services.rate_limit",
("check_rate_limit", "build_rate_limit_response"),
)
(redact_text,) = import_attrs_dual(
__package__,
"..services.redaction",
"services.redaction",
("redact_text",),
)
# IMPORTANT: services.trace does NOT expose a `trace` symbol.
# Do not import `trace` here or route registration will fail.
(trace_store,) = import_attrs_dual(
__package__,
"..services.trace_store",
"services.trace_store",
("trace_store",),
)
def check_dependency(module_name: str) -> bool:
"""Check if a module is importable."""
try:
__import__(module_name)
return True
except ImportError:
return False
def _handler_dependencies():
"""Capture facade patch seams for the owned route implementations."""
return RouteHandlerDependencies(
web=web,
pack_name=PACK_NAME,
pack_version=PACK_VERSION,
pack_start_time=PACK_START_TIME,
log_file=LOG_FILE,
metrics=metrics,
tail_log=tail_log,
require_observability_access=require_observability_access,
require_admin_token=require_admin_token,
check_rate_limit=check_rate_limit,
build_rate_limit_response=build_rate_limit_response,
trace_store=trace_store,
get_executor_diagnostics=get_executor_diagnostics,
redact_text=redact_text,
check_dependency=check_dependency,
resolve_token_info=resolve_token_info,
emit_audit_event=emit_audit_event,
jobs_request_tenant_scope=jobs_request_tenant_scope,
normalize_jobs_query=normalize_jobs_query,
build_jobs_audit_details=build_jobs_audit_details,
safe_job_audit_outcomes=SAFE_JOB_AUDIT_OUTCOMES,
jobs_security_error=JobsSecurityError,
tenant_boundary_error=TenantBoundaryError,
jobs_host_contract_unsupported=JobsHostContractUnsupported,
jobs_backend_unavailable=JobsBackendUnavailable,
read_jobs=read_jobs,
ensure_observability_deps_ready=_ensure_observability_deps_ready,
)
def _ensure_observability_deps_ready() -> tuple[bool, str | None]:
"""Preserve the established initialization-check patch seam."""
return cast(
tuple[bool, str | None],
owned_ensure_observability_deps_ready(_handler_dependencies()),
)
@endpoint_metadata(
auth=AuthTier.PUBLIC,
risk=RiskTier.LOW,
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:
"""
GET /openclaw/health (legacy: /moltbot/health)
Returns pack status, uptime, dependencies, config presence, and stats.
"""
return await health_response(request, _handler_dependencies())
@endpoint_metadata(
auth=AuthTier.ADMIN,
risk=RiskTier.MEDIUM,
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."""
# CRITICAL: logs_tail_response performs require_admin_token( before log access.
return await logs_tail_response(request, _handler_dependencies())
@endpoint_metadata(
auth=AuthTier.ADMIN,
risk=RiskTier.LOW,
summary="List jobs",
description="Admin-authorized versioned bounded in-process jobs read model.",
audit="jobs.list",
plane=RoutePlane.ADMIN,
)
async def jobs_handler(request: web.Request) -> web.Response:
"""
GET /openclaw/jobs (legacy: /moltbot/jobs).
This handler preserves the authorization and tenant boundary around the read model.
"""
# CRITICAL: jobs_response performs require_admin_token( before queue/history access.
return await jobs_response(request, _handler_dependencies())
def _emit_jobs_list_audit(
*,
request,
token_info,
outcome: str,
status_code: int,
reason: str,
**counts,
) -> None:
"""Preserve the established facade seam with content-free dependency capture."""
emit_jobs_list_audit(
_handler_dependencies(),
request=request,
token_info=token_info,
outcome=outcome,
status_code=status_code,
reason=reason,
**counts,
)
@endpoint_metadata(
auth=AuthTier.ADMIN,
risk=RiskTier.MEDIUM,
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."""
# CRITICAL: trace_response performs require_admin_token( before trace access.
return await trace_response(request, _handler_dependencies())
assist = None
if web is not None:
# Initialize Assist Handlers
try:
from ..api.assist import AssistHandlers
except ImportError:
from api.assist import AssistHandlers
assist = AssistHandlers()
def register_dual_route(server, method: str, path: str, handler) -> None:
"""
Registers a route to both the standard PromptServer table
and directly to the aiohttp router with and without /api prefix
to ensure robustness against loading order (R26/F24).
"""
orchestrate_dual_route(
server,
method,
path,
handler,
metrics=metrics,
legacy_headers_builder=build_legacy_route_deprecation_headers,
)
def _resolve_mae_profile() -> str:
try:
if __package__ and "." in __package__:
from ..services.effective_security_posture import (
get_effective_security_posture,
)
else:
from services.effective_security_posture import (
get_effective_security_posture,
)
posture = get_effective_security_posture(required=False)
if posture is not None:
return str(posture.mae_profile)
except ImportError:
# IMPORTANT: dependency-light import mode retains the accepted resolver below.
pass
profile = os.environ.get("OPENCLAW_DEPLOYMENT_PROFILE", "local").strip().lower()
if profile in {"public", "hardened"}:
return profile
try:
if __package__ and "." in __package__:
from ..services.runtime_profile import get_runtime_profile
else:
from services.runtime_profile import get_runtime_profile
runtime_profile = get_runtime_profile().value
if runtime_profile == "hardened":
return "hardened"
except ImportError:
# IMPORTANT: optional standalone import absence may fall back to the deployment
# profile, but unexpected resolver failures must propagate instead of downgrading
# hardened posture silently.
pass
return profile or "local"
def _run_mae_startup_gate(server) -> None:
run_mae_startup_gate(server, _resolve_mae_profile)
def register_routes(server) -> None:
"""
Register API routes with the ComfyUI server.
Called from __init__.py during pack initialization.
"""
# S56: Startup deployment profile gate (fail-closed pre-route validation).
# Must run BEFORE any route or worker registration.
try:
try:
from ..services.effective_security_posture import (
get_effective_security_posture,
resolve_effective_security_posture,
)
from ..services.startup_profile_gate import enforce_startup_gate
except (ImportError, ValueError):
from services.effective_security_posture import (
get_effective_security_posture,
resolve_effective_security_posture,
)
from services.startup_profile_gate import enforce_startup_gate
posture = get_effective_security_posture(required=False)
if posture is None:
# Compatibility/direct-test invocation is not the process owner.
posture = resolve_effective_security_posture()
enforce_startup_gate(posture=posture)
except RuntimeError:
# CRITICAL: fail-closed. Never continue route registration after S56
# startup gate failure.
raise
print("[OpenClaw] Registering routes (Shim Alignment R26)...")
core_handlers = {
"remote_admin_page_handler": remote_admin_page_handler,
"health_handler": health_handler,
"logs_tail_handler": logs_tail_handler,
"jobs_handler": jobs_handler,
"trace_handler": trace_handler,
"webhook_handler": webhook_handler,
"webhook_submit_handler": webhook_submit_handler,
"webhook_validate_handler": webhook_validate_handler,
"capabilities_handler": capabilities_handler,
"config_get_handler": config_get_handler,
"config_put_handler": config_put_handler,
"llm_test_handler": llm_test_handler,
"llm_chat_handler": llm_chat_handler,
"llm_models_handler": llm_models_handler,
"templates_list_handler": templates_list_handler,
"preflight_handler": preflight_handler,
"inventory_handler": inventory_handler,
"pnginfo_handler": pnginfo_handler,
"list_checkpoints_handler": list_checkpoints_handler,
"create_checkpoint_handler": create_checkpoint_handler,
"get_checkpoint_handler": get_checkpoint_handler,
"delete_checkpoint_handler": delete_checkpoint_handler,
"rewrite_recipes_list_handler": rewrite_recipes_list_handler,
"rewrite_recipe_create_handler": rewrite_recipe_create_handler,
"rewrite_recipe_get_handler": rewrite_recipe_get_handler,
"rewrite_recipe_update_handler": rewrite_recipe_update_handler,
"rewrite_recipe_delete_handler": rewrite_recipe_delete_handler,
"rewrite_recipe_dry_run_handler": rewrite_recipe_dry_run_handler,
"rewrite_recipe_apply_handler": rewrite_recipe_apply_handler,
"model_search_handler": model_search_handler,
"model_download_create_handler": model_download_create_handler,
"model_download_list_handler": model_download_list_handler,
"model_download_get_handler": model_download_get_handler,
"model_download_cancel_handler": model_download_cancel_handler,
"model_import_handler": model_import_handler,
"model_installations_list_handler": model_installations_list_handler,
"secrets_status_handler": secrets_status_handler,
"secrets_put_handler": secrets_put_handler,
"events_stream_handler": events_stream_handler,
"events_poll_handler": events_poll_handler,
"secrets_delete_handler": secrets_delete_handler,
"security_doctor_handler": security_doctor_handler,
"tools_list_handler": tools_list_handler,
"tools_run_handler": tools_run_handler,
"create_sweep_handler": create_sweep_handler,
"create_compare_handler": create_compare_handler,
"list_experiments_handler": list_experiments_handler,
"get_experiment_handler": get_experiment_handler,
"update_experiment_handler": update_experiment_handler,
"select_apply_winner_handler": select_apply_winner_handler,
}
connector_handlers = None
if connector_installations_list_handler:
connector_handlers = {
"connector_installations_list_handler": connector_installations_list_handler,
"connector_extraction_contract_handler": connector_extraction_contract_handler,
"connector_installation_resolve_handler": connector_installation_resolve_handler,
"connector_installation_audit_handler": connector_installation_audit_handler,
"connector_installation_get_handler": connector_installation_get_handler,
}
register_route_families(
server,
RouteRegistrationDependencies(
build_core_route_specs=build_core_route_specs,
build_assist_route_specs=build_assist_route_specs,
build_connector_installation_route_specs=build_connector_installation_route_specs,
build_pack_route_specs=build_pack_route_specs,
register_route_family=register_route_family,
register_dual_route=register_dual_route,
core_handlers=core_handlers,
assist=assist,
connector_installation_handlers=connector_handlers,
run_mae_startup_gate=_run_mae_startup_gate,
),
)