82 lines
2.6 KiB
Python
82 lines
2.6 KiB
Python
"""Shared input validation guards for both MCP and CLI surfaces."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
PASSWORD_PATTERN = re.compile(
|
|
r"\bpass(?:word|phrase|code)s?\b|\bsecret\b|\bcredential\b|\bpin\s*(?:code)?\b|\bpwd\b|\bpasswd\b",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
JS_PASSWORD_PATTERN = re.compile(
|
|
r"""(?:type\s*=\s*['"]?password|\.type\s*===?\s*['"]password|input\[type=password\]).*?\.value\s*=""",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
CREDENTIAL_HINT = (
|
|
"Use skyvern_login with a stored credential to authenticate. "
|
|
"Create credentials via CLI: skyvern credentials add. "
|
|
"Never pass passwords through tool calls."
|
|
)
|
|
|
|
VALID_WAIT_UNTIL = ("load", "domcontentloaded", "networkidle", "commit")
|
|
VALID_BUTTONS = ("left", "right", "middle")
|
|
VALID_ELEMENT_STATES = ("visible", "hidden", "attached", "detached")
|
|
|
|
|
|
class GuardError(Exception):
|
|
"""Raised when an input guard blocks an operation."""
|
|
|
|
def __init__(self, message: str, hint: str = "") -> None:
|
|
super().__init__(message)
|
|
self.hint = hint
|
|
|
|
|
|
def check_password_prompt(text: str) -> None:
|
|
"""Block prompts containing password/credential terms."""
|
|
if PASSWORD_PATTERN.search(text):
|
|
raise GuardError(
|
|
"Cannot perform password/credential actions — credentials must not be passed through tool calls",
|
|
CREDENTIAL_HINT,
|
|
)
|
|
|
|
|
|
def check_js_password(expression: str) -> None:
|
|
"""Block JS expressions that set password field values."""
|
|
if JS_PASSWORD_PATTERN.search(expression):
|
|
raise GuardError(
|
|
"Cannot set password field values via JavaScript — credentials must not be passed through tool calls",
|
|
CREDENTIAL_HINT,
|
|
)
|
|
|
|
|
|
def validate_wait_until(value: str | None) -> None:
|
|
if value is not None and value not in VALID_WAIT_UNTIL:
|
|
raise GuardError(
|
|
f"Invalid wait_until: {value}",
|
|
"Use load, domcontentloaded, networkidle, or commit",
|
|
)
|
|
|
|
|
|
def validate_button(value: str | None) -> None:
|
|
if value is not None and value not in VALID_BUTTONS:
|
|
raise GuardError(f"Invalid button: {value}", "Use left, right, or middle")
|
|
|
|
|
|
def resolve_ai_mode(
|
|
selector: str | None,
|
|
intent: str | None,
|
|
) -> tuple[str | None, str | None]:
|
|
"""Determine AI mode from selector/intent combination.
|
|
|
|
Returns (ai_mode, error_code) -- if error_code is set, the call should fail.
|
|
"""
|
|
if intent and not selector:
|
|
return "proactive", None
|
|
if intent and selector:
|
|
return "fallback", None
|
|
if selector and not intent:
|
|
return None, None
|
|
return None, "INVALID_INPUT"
|