mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
252 lines
7.7 KiB
Python
252 lines
7.7 KiB
Python
"""
|
|
S72 reasoning-content redaction and privileged reveal helpers.
|
|
|
|
Default posture is fail-closed: reasoning-like payload fields are stripped from
|
|
operator-facing payloads unless an explicit local-debug reveal gate is allowed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import os
|
|
from collections.abc import Mapping
|
|
from typing import Any, Dict, Iterable
|
|
|
|
from .access_control import is_loopback
|
|
from .audit import emit_audit_event
|
|
from .internal_content import sanitize_internal_content
|
|
from .request_ip import get_client_ip
|
|
from .runtime_profile import get_runtime_profile, is_hardened_mode
|
|
|
|
REASONING_REVEAL_ENV = "OPENCLAW_DEBUG_REASONING_REVEAL"
|
|
LEGACY_REASONING_REVEAL_ENV = "MOLTBOT_DEBUG_REASONING_REVEAL"
|
|
REASONING_REVEAL_HEADER = "X-OpenClaw-Debug-Reveal-Reasoning"
|
|
LEGACY_REASONING_REVEAL_HEADER = "X-Moltbot-Debug-Reveal-Reasoning"
|
|
REASONING_REVEAL_QUERY = "debug_reasoning"
|
|
|
|
REASONING_KEYS = {
|
|
"analysis",
|
|
"analysis_text",
|
|
"chain_of_thought",
|
|
"cot",
|
|
"internal_reasoning",
|
|
"reasoning",
|
|
"reasoning_content",
|
|
"reasoning_text",
|
|
"thinking",
|
|
"thinking_text",
|
|
"thought",
|
|
"thoughts",
|
|
}
|
|
REASONING_BLOCK_TYPES = {
|
|
"analysis",
|
|
"reasoning",
|
|
"reasoning_content",
|
|
"thinking",
|
|
"thought",
|
|
}
|
|
|
|
_DROP = object()
|
|
|
|
|
|
def _is_truthy_flag(value: Any) -> bool:
|
|
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _normalize_key(key: Any) -> str:
|
|
return str(key or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
|
|
|
|
def _is_reasoning_key(key: Any) -> bool:
|
|
normalized = _normalize_key(key)
|
|
if normalized in REASONING_KEYS:
|
|
return True
|
|
return normalized.endswith("_reasoning") or normalized.endswith("_thinking")
|
|
|
|
|
|
def _is_reasoning_block(value: Dict[str, Any]) -> bool:
|
|
block_type = _normalize_key(value.get("type"))
|
|
return block_type in REASONING_BLOCK_TYPES
|
|
|
|
|
|
def _strip_reasoning(value: Any, *, include_reasoning: bool) -> Any:
|
|
if isinstance(value, dict):
|
|
if _is_reasoning_block(value):
|
|
return copy.deepcopy(value) if include_reasoning else _DROP
|
|
|
|
result: Dict[str, Any] = {}
|
|
for key, raw_child in value.items():
|
|
if _is_reasoning_key(key):
|
|
if include_reasoning:
|
|
result[key] = copy.deepcopy(raw_child)
|
|
continue
|
|
|
|
child = _strip_reasoning(raw_child, include_reasoning=include_reasoning)
|
|
if child is _DROP:
|
|
continue
|
|
result[key] = child
|
|
return result
|
|
|
|
if isinstance(value, list):
|
|
result_list = []
|
|
for item in value:
|
|
child = _strip_reasoning(item, include_reasoning=include_reasoning)
|
|
if child is _DROP:
|
|
continue
|
|
result_list.append(child)
|
|
return result_list
|
|
|
|
return copy.deepcopy(value)
|
|
|
|
|
|
def sanitize_operator_payload(value: Any, *, include_reasoning: bool = False) -> Any:
|
|
"""Strip internal and reasoning-like fields from visible payloads by default."""
|
|
public_value = sanitize_internal_content(value)
|
|
cleaned = _strip_reasoning(public_value, include_reasoning=include_reasoning)
|
|
if cleaned is _DROP:
|
|
return {}
|
|
return cleaned
|
|
|
|
|
|
def extract_reasoning_payload(value: Any) -> Any:
|
|
"""Return a payload containing only reasoning-like fields/blocks, or None."""
|
|
reasoning_only = _strip_reasoning(value, include_reasoning=True)
|
|
sanitized = sanitize_operator_payload(reasoning_only, include_reasoning=True)
|
|
without_non_reasoning = _extract_only_reasoning(sanitized)
|
|
if without_non_reasoning in (None, {}, []):
|
|
return None
|
|
return without_non_reasoning
|
|
|
|
|
|
def _extract_only_reasoning(value: Any) -> Any:
|
|
if isinstance(value, dict):
|
|
if _is_reasoning_block(value):
|
|
return copy.deepcopy(value)
|
|
|
|
result: Dict[str, Any] = {}
|
|
for key, raw_child in value.items():
|
|
if _is_reasoning_key(key):
|
|
result[key] = copy.deepcopy(raw_child)
|
|
continue
|
|
|
|
child = _extract_only_reasoning(raw_child)
|
|
if child not in (None, {}, []):
|
|
result[key] = child
|
|
return result
|
|
|
|
if isinstance(value, list):
|
|
items = []
|
|
for item in value:
|
|
child = _extract_only_reasoning(item)
|
|
if child not in (None, {}, []):
|
|
items.append(child)
|
|
return items
|
|
|
|
return None
|
|
|
|
|
|
def resolve_reasoning_reveal(request: Any, *, admin_authorized: bool) -> Dict[str, Any]:
|
|
"""Evaluate the privileged reasoning reveal gate for a request."""
|
|
headers = getattr(request, "headers", {}) or {}
|
|
if not isinstance(headers, Mapping):
|
|
headers = {}
|
|
query = getattr(request, "query", {}) or {}
|
|
if not isinstance(query, Mapping):
|
|
query = {}
|
|
requested = (
|
|
_is_truthy_flag(headers.get(REASONING_REVEAL_HEADER))
|
|
or _is_truthy_flag(headers.get(LEGACY_REASONING_REVEAL_HEADER))
|
|
or _is_truthy_flag(query.get(REASONING_REVEAL_QUERY))
|
|
)
|
|
|
|
deployment_profile = (
|
|
os.environ.get("OPENCLAW_DEPLOYMENT_PROFILE", "").strip().lower() or "local"
|
|
)
|
|
runtime_profile = get_runtime_profile().value
|
|
remote_addr = get_client_ip(request)
|
|
env_enabled = _is_truthy_flag(
|
|
os.environ.get(REASONING_REVEAL_ENV)
|
|
or os.environ.get(LEGACY_REASONING_REVEAL_ENV)
|
|
)
|
|
|
|
if not requested:
|
|
return {
|
|
"requested": False,
|
|
"allowed": False,
|
|
"reason": "not_requested",
|
|
"remote_addr": remote_addr,
|
|
"runtime_profile": runtime_profile,
|
|
"deployment_profile": deployment_profile,
|
|
}
|
|
|
|
if not env_enabled:
|
|
reason = "debug_reveal_disabled"
|
|
elif not admin_authorized:
|
|
reason = "admin_required"
|
|
elif is_hardened_mode():
|
|
reason = "runtime_profile_hardened"
|
|
elif deployment_profile not in {"local", "lan"}:
|
|
reason = f"deployment_profile_{deployment_profile}"
|
|
elif not is_loopback(remote_addr):
|
|
reason = "loopback_required"
|
|
else:
|
|
reason = "allowed"
|
|
|
|
return {
|
|
"requested": True,
|
|
"allowed": reason == "allowed",
|
|
"reason": reason,
|
|
"remote_addr": remote_addr,
|
|
"runtime_profile": runtime_profile,
|
|
"deployment_profile": deployment_profile,
|
|
}
|
|
|
|
|
|
def audit_reasoning_reveal(
|
|
request: Any,
|
|
*,
|
|
target: str,
|
|
decision: Dict[str, Any],
|
|
extra_details: Dict[str, Any] | None = None,
|
|
) -> None:
|
|
"""Emit an audit event for explicit reasoning reveal attempts."""
|
|
if not decision.get("requested"):
|
|
return
|
|
details = {
|
|
"requested": bool(decision.get("requested")),
|
|
"allowed": bool(decision.get("allowed")),
|
|
"reason": decision.get("reason"),
|
|
"remote_addr": decision.get("remote_addr"),
|
|
"runtime_profile": decision.get("runtime_profile"),
|
|
"deployment_profile": decision.get("deployment_profile"),
|
|
}
|
|
if extra_details:
|
|
details.update(extra_details)
|
|
emit_audit_event(
|
|
action="reasoning.debug_reveal",
|
|
target=target,
|
|
outcome="allow" if decision.get("allowed") else "error",
|
|
status_code=200 if decision.get("allowed") else 403,
|
|
details=details,
|
|
request=request,
|
|
)
|
|
|
|
|
|
def get_redacted_reasoning_debug(value: Any) -> Any:
|
|
"""
|
|
Extract a reasoning-only payload and redact sensitive tokens/secrets inside it.
|
|
"""
|
|
reasoning = extract_reasoning_payload(value)
|
|
if reasoning in (None, {}, []):
|
|
return None
|
|
try:
|
|
from .redaction import redact_json
|
|
except Exception:
|
|
return reasoning
|
|
return redact_json(reasoning)
|
|
|
|
|
|
def strip_reasoning_keys(keys: Iterable[str]) -> list[str]:
|
|
"""Helper for tests/debug output."""
|
|
return [key for key in keys if not _is_reasoning_key(key)]
|