Skyvern/skyvern/schemas/proxy_pinning.py
Suchintan d0094d3568
feat(SKY-8207): consistent egress IP for Save & Reuse Session runs (#7169)
Co-authored-by: Suchintan Singh <suchintan@skyvern.com>
Co-authored-by: AronPerez <aperez0295@gmail.com>
2026-07-07 17:45:11 -04:00

103 lines
3.5 KiB
Python

from __future__ import annotations
import json
import secrets
from hashlib import sha256
from typing import Any
from skyvern.schemas.proxy_location import GeoTarget, ProxyLocation, ProxyLocationInput
_LOWER_HEX = set("0123456789abcdef")
def is_proxy_session_id(value: str) -> bool:
return len(value) == 10 and all(char in _LOWER_HEX for char in value)
def normalize_proxy_session_id(value: str | None) -> str | None:
if value is None:
return None
raw_value = value.strip()
if not raw_value:
return None
return raw_value
def generate_proxy_session_id(source_id: str) -> str:
if normalize_proxy_session_id(source_id) is None:
raise ValueError("Cannot generate proxy session id from an empty entity id")
proxy_session_id = secrets.token_hex(5)
if not is_proxy_session_id(proxy_session_id):
raise RuntimeError("Generated proxy session id does not match the expected format")
return proxy_session_id
def derive_proxy_session_id(*parts: str) -> str:
if not parts or any(not part or not part.strip() for part in parts):
raise ValueError("Cannot derive proxy session id from empty parts")
return sha256(":".join(parts).encode("utf-8")).hexdigest()[:10]
def redact_proxy_session_id(value: str | None) -> str | None:
if not value:
return None
if len(value) <= 5:
return "***"
return f"{value[:3]}...{value[-2:]}"
def should_generate_proxy_session_id(proxy_location: object | None) -> bool:
return proxy_location == ProxyLocation.RESIDENTIAL_ISP or proxy_location == ProxyLocation.RESIDENTIAL_ISP.value
def apply_proxy_pin_update(
update_kwargs: dict[str, Any],
*,
proxy_location_was_set: bool,
proxy_location: ProxyLocationInput,
proxy_session_id_was_set: bool,
proxy_session_id: str | None,
rotate_proxy_session_id: bool = False,
) -> None:
if rotate_proxy_session_id:
update_kwargs["rotate_proxy_session_id"] = True
if proxy_location_was_set:
update_kwargs["proxy_location"] = proxy_location
if proxy_location is None or not should_generate_proxy_session_id(proxy_location):
update_kwargs["proxy_session_id"] = None
elif proxy_session_id_was_set and proxy_session_id is not None:
update_kwargs["proxy_session_id"] = proxy_session_id
return
if proxy_session_id_was_set:
if proxy_session_id:
update_kwargs["proxy_location"] = ProxyLocation.RESIDENTIAL_ISP
update_kwargs["proxy_session_id"] = proxy_session_id
else:
update_kwargs["proxy_location"] = None
update_kwargs["proxy_session_id"] = None
def validate_proxy_session_id(value: str | None) -> str | None:
return normalize_proxy_session_id(value)
def parse_proxy_location_input(value: object) -> object:
if value is None or isinstance(value, (ProxyLocation, GeoTarget)):
return value
if isinstance(value, dict):
if "url" in value and "country" not in value:
raise ValueError("Custom proxy URLs are not supported for pinned proxy identities")
return GeoTarget.model_validate(value)
if isinstance(value, str):
raw = value.strip()
if not raw:
return None
if raw.startswith("{"):
data = json.loads(raw)
if not isinstance(data, dict):
raise ValueError("proxy_location JSON must be an object")
return parse_proxy_location_input(data)
return ProxyLocation(raw)
return value