Extract shared core from MCP tools, add CLI browser commands (#4768)
This commit is contained in:
81
skyvern/cli/core/guards.py
Normal file
81
skyvern/cli/core/guards.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user