mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
324 lines
9.8 KiB
Python
324 lines
9.8 KiB
Python
"""
|
|
S66 Runtime Guardrails Contract.
|
|
|
|
Centralized, ENV-driven runtime guardrails used for deterministic safety/
|
|
operability limits and diagnostics. Guardrail values are runtime-only and must
|
|
not be persisted into user config/state payloads.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Tuple
|
|
|
|
logger = logging.getLogger("ComfyUI-OpenClaw.services.runtime_guardrails")
|
|
|
|
# Stable machine codes (S66)
|
|
CODE_OK = "S66_GUARDRAILS_OK"
|
|
CODE_DEGRADED = "S66_GUARDRAILS_DEGRADED"
|
|
CODE_INVALID_INT = "S66_INVALID_INT"
|
|
CODE_INVALID_BOOL = "S66_INVALID_BOOL"
|
|
CODE_CLAMPED = "S66_CLAMPED"
|
|
CODE_RUNTIME_ONLY_STRIPPED = "S66_RUNTIME_ONLY_STRIPPED"
|
|
CODE_RUNTIME_ONLY_PERSIST_FORBIDDEN = "S66_RUNTIME_ONLY_PERSIST_FORBIDDEN"
|
|
|
|
RUNTIME_ONLY_CONFIG_KEYS = ("runtime_guardrails", "guardrails")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _GuardrailSpec:
|
|
path: str
|
|
env: str
|
|
type_name: str # "int" | "bool"
|
|
default: Any
|
|
min_value: int | None = None
|
|
max_value: int | None = None
|
|
|
|
|
|
_SPECS: tuple[_GuardrailSpec, ...] = (
|
|
# Retention
|
|
_GuardrailSpec(
|
|
path="retention.job_event_buffer_size",
|
|
env="OPENCLAW_JOB_EVENT_BUFFER_SIZE",
|
|
type_name="int",
|
|
default=500,
|
|
min_value=50,
|
|
max_value=5000,
|
|
),
|
|
_GuardrailSpec(
|
|
path="retention.job_event_ttl_sec",
|
|
env="OPENCLAW_JOB_EVENT_TTL_SEC",
|
|
type_name="int",
|
|
default=600,
|
|
min_value=30,
|
|
max_value=86400,
|
|
),
|
|
# Timeout / retry budgets
|
|
_GuardrailSpec(
|
|
path="timeout_retry.llm_timeout_cap_sec",
|
|
env="OPENCLAW_GUARDRAIL_LLM_TIMEOUT_CAP_SEC",
|
|
type_name="int",
|
|
default=300,
|
|
min_value=5,
|
|
max_value=600,
|
|
),
|
|
_GuardrailSpec(
|
|
path="timeout_retry.llm_max_retries_cap",
|
|
env="OPENCLAW_GUARDRAIL_LLM_MAX_RETRIES_CAP",
|
|
type_name="int",
|
|
default=10,
|
|
min_value=0,
|
|
max_value=20,
|
|
),
|
|
# Bounded queues (observe and centralize)
|
|
_GuardrailSpec(
|
|
path="bounded_queues.max_inflight_submits_total",
|
|
env="OPENCLAW_MAX_INFLIGHT_SUBMITS_TOTAL",
|
|
type_name="int",
|
|
default=2,
|
|
min_value=1,
|
|
max_value=100,
|
|
),
|
|
_GuardrailSpec(
|
|
path="bounded_queues.max_rendered_workflow_bytes",
|
|
env="OPENCLAW_MAX_RENDERED_WORKFLOW_BYTES",
|
|
type_name="int",
|
|
default=512 * 1024,
|
|
min_value=64 * 1024,
|
|
max_value=16 * 1024 * 1024,
|
|
),
|
|
# Provider safety defaults (runtime default posture, not user-persisted config)
|
|
_GuardrailSpec(
|
|
path="provider_safety.allow_any_public_llm_host_default",
|
|
env="OPENCLAW_GUARDRAIL_ALLOW_ANY_PUBLIC_LLM_HOST_DEFAULT",
|
|
type_name="bool",
|
|
default=False,
|
|
),
|
|
_GuardrailSpec(
|
|
path="provider_safety.allow_insecure_base_url_default",
|
|
env="OPENCLAW_GUARDRAIL_ALLOW_INSECURE_BASE_URL_DEFAULT",
|
|
type_name="bool",
|
|
default=False,
|
|
),
|
|
)
|
|
|
|
_TRUTHY = {"1", "true", "yes", "on"}
|
|
_FALSY = {"0", "false", "no", "off"}
|
|
_AUDIT_FINGERPRINTS_EMITTED: set[str] = set()
|
|
|
|
|
|
def _deployment_profile() -> str:
|
|
return (os.environ.get("OPENCLAW_DEPLOYMENT_PROFILE") or "local").strip().lower()
|
|
|
|
|
|
def _runtime_profile() -> str:
|
|
return (os.environ.get("OPENCLAW_RUNTIME_PROFILE") or "minimal").strip().lower()
|
|
|
|
|
|
def _set_path(root: Dict[str, Any], path: str, value: Any) -> None:
|
|
parts = path.split(".")
|
|
cur = root
|
|
for part in parts[:-1]:
|
|
cur = cur.setdefault(part, {})
|
|
cur[parts[-1]] = value
|
|
|
|
|
|
def _coerce_int(spec: _GuardrailSpec, raw: str) -> tuple[int, str | None]:
|
|
try:
|
|
value = int(str(raw).strip())
|
|
except Exception:
|
|
return int(spec.default), CODE_INVALID_INT
|
|
|
|
if spec.min_value is not None and value < spec.min_value:
|
|
return int(spec.min_value), CODE_CLAMPED
|
|
if spec.max_value is not None and value > spec.max_value:
|
|
return int(spec.max_value), CODE_CLAMPED
|
|
return value, None
|
|
|
|
|
|
def _coerce_bool(spec: _GuardrailSpec, raw: str) -> tuple[bool, str | None]:
|
|
val = str(raw).strip().lower()
|
|
if val in _TRUTHY:
|
|
return True, None
|
|
if val in _FALSY:
|
|
return False, None
|
|
return bool(spec.default), CODE_INVALID_BOOL
|
|
|
|
|
|
def _build_violation(
|
|
*,
|
|
code: str,
|
|
path: str,
|
|
env_var: str,
|
|
raw_value: Any,
|
|
applied_value: Any,
|
|
source: str,
|
|
) -> dict:
|
|
return {
|
|
"code": code,
|
|
"path": path,
|
|
"env_var": env_var,
|
|
"raw_value": raw_value,
|
|
"applied_value": applied_value,
|
|
"source": source,
|
|
}
|
|
|
|
|
|
def get_runtime_guardrails_snapshot(*, emit_audit: bool = False) -> Dict[str, Any]:
|
|
"""
|
|
Return centralized runtime guardrails snapshot with diagnostics.
|
|
|
|
Contract fields:
|
|
- status / code
|
|
- deployment_profile / runtime_profile
|
|
- values (grouped)
|
|
- sources (flattened path -> source)
|
|
- violations (machine-readable)
|
|
"""
|
|
values: Dict[str, Any] = {}
|
|
sources: Dict[str, str] = {}
|
|
violations: List[dict] = []
|
|
|
|
for spec in _SPECS:
|
|
raw = os.environ.get(spec.env)
|
|
applied = spec.default
|
|
source = "default"
|
|
violation_code = None
|
|
if raw is not None:
|
|
source = "env"
|
|
if spec.type_name == "int":
|
|
applied, violation_code = _coerce_int(spec, raw)
|
|
elif spec.type_name == "bool":
|
|
applied, violation_code = _coerce_bool(spec, raw)
|
|
else: # pragma: no cover (spec invariant)
|
|
applied = spec.default
|
|
if violation_code:
|
|
violations.append(
|
|
_build_violation(
|
|
code=violation_code,
|
|
path=spec.path,
|
|
env_var=spec.env,
|
|
raw_value=raw,
|
|
applied_value=applied,
|
|
source=source,
|
|
)
|
|
)
|
|
|
|
_set_path(values, spec.path, applied)
|
|
sources[spec.path] = source
|
|
|
|
status = "degraded" if violations else "ok"
|
|
snapshot = {
|
|
"status": status,
|
|
"code": CODE_DEGRADED if violations else CODE_OK,
|
|
"deployment_profile": _deployment_profile(),
|
|
"runtime_profile": _runtime_profile(),
|
|
"runtime_only": True,
|
|
"values": values,
|
|
"sources": sources,
|
|
"violations": violations,
|
|
}
|
|
|
|
if emit_audit and violations:
|
|
_emit_degraded_guardrails_audit(snapshot)
|
|
|
|
return snapshot
|
|
|
|
|
|
def _emit_degraded_guardrails_audit(snapshot: Dict[str, Any]) -> None:
|
|
"""Emit a one-shot machine-readable audit event for degraded guardrails."""
|
|
fp = "|".join(
|
|
f"{v.get('path')}:{v.get('code')}:{v.get('applied_value')}"
|
|
for v in snapshot.get("violations", [])
|
|
)
|
|
if fp in _AUDIT_FINGERPRINTS_EMITTED:
|
|
return
|
|
_AUDIT_FINGERPRINTS_EMITTED.add(fp)
|
|
try:
|
|
from .audit_events import build_audit_event, emit_audit_event
|
|
except ImportError:
|
|
try:
|
|
from services.audit_events import ( # type: ignore
|
|
build_audit_event,
|
|
emit_audit_event,
|
|
)
|
|
except ImportError:
|
|
return
|
|
|
|
try:
|
|
event = build_audit_event(
|
|
"runtime.guardrails.degraded",
|
|
payload={
|
|
"code": snapshot.get("code"),
|
|
"deployment_profile": snapshot.get("deployment_profile"),
|
|
"runtime_profile": snapshot.get("runtime_profile"),
|
|
"violations": snapshot.get("violations", []),
|
|
},
|
|
meta={"component": "RuntimeGuardrails"},
|
|
)
|
|
emit_audit_event(event)
|
|
except Exception as exc: # pragma: no cover (non-fatal telemetry path)
|
|
logger.warning("S66: Failed to emit degraded guardrails audit event: %s", exc)
|
|
|
|
|
|
def reset_runtime_guardrails_audit_cache() -> None:
|
|
"""Test helper for one-shot degraded-audit emission."""
|
|
_AUDIT_FINGERPRINTS_EMITTED.clear()
|
|
|
|
|
|
def strip_runtime_only_config_fields(
|
|
config_blob: Dict[str, Any]
|
|
) -> Tuple[Dict[str, Any], List[dict]]:
|
|
"""
|
|
Remove runtime-only guardrail fields from config blobs before persistence/use.
|
|
|
|
Returns a deep-copied sanitized blob plus compatibility notices.
|
|
"""
|
|
sanitized = copy.deepcopy(config_blob) if isinstance(config_blob, dict) else {}
|
|
notices: List[dict] = []
|
|
|
|
for key in RUNTIME_ONLY_CONFIG_KEYS:
|
|
if key in sanitized:
|
|
removed = sanitized.pop(key, None)
|
|
notices.append(
|
|
{
|
|
"code": CODE_RUNTIME_ONLY_STRIPPED,
|
|
"path": key,
|
|
"reason": "runtime_only_guardrails_not_persisted",
|
|
"removed_type": (
|
|
type(removed).__name__ if removed is not None else "none"
|
|
),
|
|
}
|
|
)
|
|
|
|
llm = sanitized.get("llm")
|
|
if isinstance(llm, dict):
|
|
for key in RUNTIME_ONLY_CONFIG_KEYS:
|
|
if key in llm:
|
|
removed = llm.pop(key, None)
|
|
notices.append(
|
|
{
|
|
"code": CODE_RUNTIME_ONLY_STRIPPED,
|
|
"path": f"llm.{key}",
|
|
"reason": "runtime_only_guardrails_not_persisted",
|
|
"removed_type": (
|
|
type(removed).__name__ if removed is not None else "none"
|
|
),
|
|
}
|
|
)
|
|
|
|
return sanitized, notices
|
|
|
|
|
|
def payload_contains_runtime_guardrails(payload: Dict[str, Any]) -> bool:
|
|
"""Return True if request payload attempts to persist runtime guardrails."""
|
|
if not isinstance(payload, dict):
|
|
return False
|
|
if any(key in payload for key in RUNTIME_ONLY_CONFIG_KEYS):
|
|
return True
|
|
llm = payload.get("llm")
|
|
return isinstance(llm, dict) and any(key in llm for key in RUNTIME_ONLY_CONFIG_KEYS)
|