Files
ComfyUI-OpenClaw/api/tools.py
T

229 lines
6.4 KiB
Python

"""
S12: API Handlers for External Tools.
Protected by Admin Token and Feature Flag.
"""
from __future__ import annotations
import json
import logging
if __package__ and "." in __package__:
from ..services.import_fallback import import_attrs_dual
else:
from services.import_fallback import import_attrs_dual # type: ignore
(require_admin_token, resolve_token_info) = import_attrs_dual(
__package__,
"..services.access_control",
"services.access_control",
("require_admin_token", "resolve_token_info"),
)
(import_aiohttp_web,) = import_attrs_dual(
__package__,
"..services.aiohttp_compat",
"services.aiohttp_compat",
("import_aiohttp_web",),
)
(emit_audit_event,) = import_attrs_dual(
__package__,
"..services.audit",
"services.audit",
("emit_audit_event",),
)
(get_tool_runner, is_tools_enabled) = import_attrs_dual(
__package__,
"..services.tool_runner",
"services.tool_runner",
("get_tool_runner", "is_tools_enabled"),
)
(AuthTier, RiskTier, RoutePlane, endpoint_metadata) = import_attrs_dual(
__package__,
"..services.endpoint_manifest",
"services.endpoint_manifest",
("AuthTier", "RiskTier", "RoutePlane", "endpoint_metadata"),
)
(check_surface,) = import_attrs_dual(
__package__,
"..services.surface_guard",
"services.surface_guard",
("check_surface",),
)
logger = logging.getLogger("ComfyUI-OpenClaw.api.tools")
web = import_aiohttp_web()
@endpoint_metadata(
auth=AuthTier.ADMIN,
risk=RiskTier.MEDIUM,
summary="List tools",
description="List allowed external tools.",
audit="tools.list",
plane=RoutePlane.ADMIN,
)
async def tools_list_handler(request: web.Request) -> web.Response:
"""
GET /openclaw/tools
List allowed external tools.
Requires: Admin Token.
"""
if not is_tools_enabled():
return web.json_response(
{"ok": False, "error": "External tooling is disabled (feature flag off)."},
status=404, # Not Found or Forbidden? 404 implies feature doesn't exist.
)
# Admin check
allowed, error = require_admin_token(request)
if not allowed:
return web.json_response({"ok": False, "error": error}, status=403)
runner = get_tool_runner()
tools = runner.list_tools()
return web.json_response({"ok": True, "tools": tools})
@endpoint_metadata(
auth=AuthTier.ADMIN,
risk=RiskTier.HIGH,
summary="Run tool",
description="Execute an external tool.",
audit="tools.run",
plane=RoutePlane.ADMIN,
)
async def tools_run_handler(request: web.Request) -> web.Response:
"""
POST /openclaw/tools/{name}/run
Execute an external tool.
Body: {"args": {"arg1": "val1", ...}}
Requires: Admin Token.
"""
# S62: Block tool execution in public+split mode
blocked = check_surface("tool_execution", request)
if blocked:
return blocked
if not is_tools_enabled():
emit_audit_event(
action="tools.run",
target="tools",
outcome="deny",
status_code=404,
details={"reason": "tools_disabled"},
request=request,
)
return web.json_response(
{"ok": False, "error": "External tooling is disabled."}, status=404
)
token_info = resolve_token_info(request)
# Admin check
allowed, error = require_admin_token(request)
if not allowed:
emit_audit_event(
action="tools.run",
target="tools",
outcome="deny",
token_info=token_info,
status_code=403,
details={"reason": error or "unauthorized"},
request=request,
)
return web.json_response({"ok": False, "error": error}, status=403)
tool_name = request.match_info.get("name")
if not tool_name:
emit_audit_event(
action="tools.run",
target="unknown",
outcome="deny",
token_info=token_info,
status_code=400,
details={"reason": "missing_tool_name"},
request=request,
)
return web.json_response(
{"ok": False, "error": "Tool name required"}, status=400
)
try:
body = await request.json()
except json.JSONDecodeError:
emit_audit_event(
action="tools.run",
target=tool_name,
outcome="deny",
token_info=token_info,
status_code=400,
details={"reason": "invalid_json"},
request=request,
)
return web.json_response(
{"ok": False, "error": "Invalid JSON body"}, status=400
)
args = body.get("args", {})
if not isinstance(args, dict):
emit_audit_event(
action="tools.run",
target=tool_name,
outcome="deny",
token_info=token_info,
status_code=400,
details={"reason": "invalid_args_shape"},
request=request,
)
return web.json_response(
{"ok": False, "error": "'args' must be a dictionary"}, status=400
)
runner = get_tool_runner()
result = runner.execute_tool(tool_name, args)
if not result.success:
emit_audit_event(
action="tools.run",
target=tool_name,
outcome="error",
token_info=token_info,
status_code=500 if result.error else 400,
details={
"exit_code": result.exit_code,
"error": result.error,
"duration_ms": result.duration_ms,
},
request=request,
)
return web.json_response(
{
"ok": False,
"tool": tool_name,
"error": result.error,
"output": result.output, # Redacted output might contain useful error info
"exit_code": result.exit_code,
"duration_ms": result.duration_ms,
},
status=500 if result.error else 400,
) # 500 for runtime error, 400 for validation?
emit_audit_event(
action="tools.run",
target=tool_name,
outcome="allow",
token_info=token_info,
status_code=200,
details={"duration_ms": result.duration_ms},
request=request,
)
return web.json_response(
{
"ok": True,
"tool": tool_name,
"output": result.output,
"duration_ms": result.duration_ms,
}
)