mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
"""
|
|
R97 Command Firewall.
|
|
|
|
Implements the runtime safety layer for connector chat:
|
|
- Canonical command parsing (assistant output -> internal structure).
|
|
- Allowlist/Denylist validation for flags and values.
|
|
- Normalized safe rendering (internal structure -> user-facing command string).
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
import shlex
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DANGEROUS_PATTERNS = (r";", r"`", r"\$\(", r"\|")
|
|
_VALID_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
|
|
|
|
@dataclass
|
|
class NormalizedCommand:
|
|
command: str
|
|
args: List[str] = field(default_factory=list)
|
|
flags: Dict[str, str] = field(default_factory=dict)
|
|
is_safe: bool = False
|
|
safety_reason: str = "unvalidated"
|
|
code: str = "unvalidated"
|
|
severity: str = "medium"
|
|
action: str = "deny"
|
|
|
|
def to_string(self) -> str:
|
|
"""Render deterministic safe command string."""
|
|
parts = [self.command]
|
|
# Canonical flag order
|
|
for k in sorted(self.flags.keys()):
|
|
v = self.flags[k]
|
|
# Simple quoting heuristic
|
|
if " " in v or not v:
|
|
v = f'"{v}"'
|
|
parts.append(f"{k}={v}")
|
|
|
|
# Positional args
|
|
parts.extend(self.args)
|
|
return " ".join(parts)
|
|
|
|
def to_contract(self) -> Dict[str, str]:
|
|
return {
|
|
"code": self.code,
|
|
"severity": self.severity,
|
|
"action": self.action,
|
|
"reason": self.safety_reason,
|
|
}
|
|
|
|
|
|
class CommandFirewall:
|
|
"""
|
|
Validates and normalizes assistant-generated command suggestions.
|
|
"""
|
|
|
|
def __init__(self):
|
|
# TODO: Load policy from config
|
|
self.allowed_commands = {"/run", "/status", "/help", "/jobs"}
|
|
self.unsafe_pattern_deny = set(_DANGEROUS_PATTERNS)
|
|
|
|
def validate_suggestion(self, raw_suggestion: str) -> NormalizedCommand:
|
|
"""
|
|
Parse and validate a raw command string from LLM output.
|
|
Returns a NormalizedCommand object marked safe or unsafe.
|
|
"""
|
|
clean_text = raw_suggestion.strip()
|
|
|
|
# 0. Pre-parsing unsafe pattern check (Denylist)
|
|
for pattern in self.unsafe_pattern_deny:
|
|
if re.search(pattern, clean_text):
|
|
return NormalizedCommand(
|
|
command="error",
|
|
is_safe=False,
|
|
safety_reason=f"unsafe_pattern_detected: {pattern}",
|
|
code="firewall_unsafe_pattern",
|
|
severity="high",
|
|
action="deny",
|
|
)
|
|
|
|
# 1. Basic Parse
|
|
try:
|
|
lexer = shlex.shlex(clean_text, posix=True)
|
|
lexer.whitespace_split = True
|
|
lexer.quotes = '"' # strict double quotes per router contract
|
|
parts = list(lexer)
|
|
except ValueError as e:
|
|
return NormalizedCommand(
|
|
command="error",
|
|
is_safe=False,
|
|
safety_reason=f"parse_error: {str(e)}",
|
|
code="firewall_parse_error",
|
|
severity="medium",
|
|
action="deny",
|
|
)
|
|
|
|
if not parts:
|
|
return NormalizedCommand(
|
|
command="",
|
|
is_safe=False,
|
|
safety_reason="empty_command",
|
|
code="firewall_empty_command",
|
|
severity="medium",
|
|
action="deny",
|
|
)
|
|
|
|
cmd = parts[0].lower()
|
|
|
|
# 2. Allowlist Check
|
|
if cmd not in self.allowed_commands:
|
|
return NormalizedCommand(
|
|
command=cmd,
|
|
is_safe=False,
|
|
safety_reason=f"command_not_allowed: {cmd}",
|
|
code="firewall_command_not_allowed",
|
|
severity="high",
|
|
action="deny",
|
|
)
|
|
|
|
# 3. Argument Parsing & Normalization
|
|
args = parts[1:]
|
|
clean_args = []
|
|
flags = {}
|
|
|
|
for arg in args:
|
|
if arg.startswith("-"):
|
|
if "=" in arg and not arg.startswith("--"):
|
|
k, v = arg.split("=", 1)
|
|
if not _VALID_KEY_RE.match(k):
|
|
return NormalizedCommand(
|
|
command=cmd,
|
|
is_safe=False,
|
|
safety_reason=f"invalid_key: {k}",
|
|
code="firewall_invalid_key",
|
|
severity="medium",
|
|
action="deny",
|
|
)
|
|
if len(v) > 1000:
|
|
return NormalizedCommand(
|
|
command=cmd,
|
|
is_safe=False,
|
|
safety_reason=f"value_too_long: {k}",
|
|
code="firewall_value_too_long",
|
|
severity="medium",
|
|
action="deny",
|
|
)
|
|
flags[k] = v
|
|
elif arg.startswith("--"):
|
|
clean_args.append(arg)
|
|
else:
|
|
clean_args.append(arg)
|
|
elif "=" in arg:
|
|
k, v = arg.split("=", 1)
|
|
if not _VALID_KEY_RE.match(k):
|
|
return NormalizedCommand(
|
|
command=cmd,
|
|
is_safe=False,
|
|
safety_reason=f"invalid_key: {k}",
|
|
code="firewall_invalid_key",
|
|
severity="medium",
|
|
action="deny",
|
|
)
|
|
if len(v) > 1000:
|
|
return NormalizedCommand(
|
|
command=cmd,
|
|
is_safe=False,
|
|
safety_reason=f"value_too_long: {k}",
|
|
code="firewall_value_too_long",
|
|
severity="medium",
|
|
action="deny",
|
|
)
|
|
flags[k] = v
|
|
else:
|
|
clean_args.append(arg)
|
|
|
|
return NormalizedCommand(
|
|
command=cmd,
|
|
args=clean_args,
|
|
flags=flags,
|
|
is_safe=True,
|
|
safety_reason="valid",
|
|
code="firewall_allow",
|
|
severity="info",
|
|
action="allow",
|
|
)
|