From 60dcd6bcb1fae6db3dd39f47e67c9f67079f355f Mon Sep 17 00:00:00 2001 From: Shuchang Zheng Date: Wed, 25 Jun 2025 05:45:07 +0800 Subject: [PATCH] in which pylon replaces intercom (#2783) --- skyvern-frontend/src/api/AxiosClient.ts | 37 +++++++++++++++++++++++-- skyvern-frontend/src/api/types.ts | 4 +++ skyvern-frontend/src/global.d.ts | 7 +++++ skyvern/config.py | 7 ++++- skyvern/forge/sdk/routes/__init__.py | 1 + skyvern/forge/sdk/routes/pylon.py | 34 +++++++++++++++++++++++ skyvern/forge/sdk/schemas/pylon.py | 5 ++++ 7 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 skyvern-frontend/src/global.d.ts create mode 100644 skyvern/forge/sdk/routes/pylon.py create mode 100644 skyvern/forge/sdk/schemas/pylon.py diff --git a/skyvern-frontend/src/api/AxiosClient.ts b/skyvern-frontend/src/api/AxiosClient.ts index 4c62c89e..96207d1d 100644 --- a/skyvern-frontend/src/api/AxiosClient.ts +++ b/skyvern-frontend/src/api/AxiosClient.ts @@ -1,8 +1,13 @@ import { apiBaseUrl, artifactApiBaseUrl, envCredential } from "@/util/env"; import axios from "axios"; +type ApiVersion = "sans-api-v1" | "v1" | "v2"; + const apiV1BaseUrl = apiBaseUrl; const apiV2BaseUrl = apiBaseUrl.replace("v1", "v2"); +const url = new URL(apiBaseUrl); +const pathname = url.pathname.replace("/api", ""); +const apiSansApiV1BaseUrl = `${url.origin}${pathname}`; const client = axios.create({ baseURL: apiV1BaseUrl, @@ -20,6 +25,14 @@ const v2Client = axios.create({ }, }); +const clientSansApiV1 = axios.create({ + baseURL: apiSansApiV1BaseUrl, + headers: { + "Content-Type": "application/json", + "x-api-key": envCredential, + }, +}); + const artifactApiClient = axios.create({ baseURL: artifactApiBaseUrl, }); @@ -52,18 +65,36 @@ export function removeApiKeyHeader() { async function getClient( credentialGetter: CredentialGetter | null, - version: string = "v1", + version: ApiVersion = "v1", ) { + const get = () => { + switch (version) { + case "sans-api-v1": + return clientSansApiV1; + case "v1": + return client; + case "v2": + return v2Client; + default: { + throw new Error(`Unknown version: ${version}`); + } + } + }; + if (credentialGetter) { removeApiKeyHeader(); + const credential = await credentialGetter(); + if (!credential) { console.warn("No credential found"); - return version === "v1" ? client : v2Client; + return get(); } + setAuthorizationHeader(credential); } - return version === "v1" ? client : v2Client; + + return get(); } export type CredentialGetter = () => Promise; diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index 078dd851..fcf6e69d 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -411,3 +411,7 @@ export const RunEngine = { } as const; export type RunEngine = (typeof RunEngine)[keyof typeof RunEngine]; + +export type PylonEmailHash = { + hash: string; +}; diff --git a/skyvern-frontend/src/global.d.ts b/skyvern-frontend/src/global.d.ts new file mode 100644 index 00000000..aad3f948 --- /dev/null +++ b/skyvern-frontend/src/global.d.ts @@ -0,0 +1,7 @@ +interface Window { + pylon: { + chat_settings: { [k: string]: string }; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Pylon: (method: string, ...args: any[]) => void; +} diff --git a/skyvern/config.py b/skyvern/config.py index 22acabd8..dd6af35f 100644 --- a/skyvern/config.py +++ b/skyvern/config.py @@ -275,10 +275,15 @@ class Settings(BaseSettings): SKYVERN_BROWSER_VNC_PORT: int = 6080 """ - The websockified port on which the VNC server of a persistent browser is + The websockified port on which the VNC server of a persistent browser is listening. """ + PYLON_IDENTITY_VERIFICATION_SECRET: str | None = None + """ + The secret used to sign the email/identity of the user. + """ + def get_model_name_to_llm_key(self) -> dict[str, dict[str, str]]: """ Keys are model names available to blocks in the frontend. These map to key names diff --git a/skyvern/forge/sdk/routes/__init__.py b/skyvern/forge/sdk/routes/__init__.py index 40527991..68afb33b 100644 --- a/skyvern/forge/sdk/routes/__init__.py +++ b/skyvern/forge/sdk/routes/__init__.py @@ -1,6 +1,7 @@ from skyvern.forge.sdk.routes import agent_protocol # noqa: F401 from skyvern.forge.sdk.routes import browser_sessions # noqa: F401 from skyvern.forge.sdk.routes import credentials # noqa: F401 +from skyvern.forge.sdk.routes import pylon # noqa: F401 from skyvern.forge.sdk.routes import streaming # noqa: F401 from skyvern.forge.sdk.routes import streaming_commands # noqa: F401 from skyvern.forge.sdk.routes import streaming_vnc # noqa: F401 diff --git a/skyvern/forge/sdk/routes/pylon.py b/skyvern/forge/sdk/routes/pylon.py new file mode 100644 index 00000000..9214b41e --- /dev/null +++ b/skyvern/forge/sdk/routes/pylon.py @@ -0,0 +1,34 @@ +import hashlib +import hmac + +import structlog +from fastapi import Query + +from skyvern.config import settings +from skyvern.forge.sdk.routes.routers import base_router +from skyvern.forge.sdk.schemas.pylon import PylonHash + +LOG = structlog.get_logger() + + +@base_router.get( + "/pylon/email_hash", + include_in_schema=False, + response_model=PylonHash, +) +def get_pylon_email_hash(email: str = Query(...)) -> PylonHash: + no_hash = "???-no-hash-???" + secret = settings.PYLON_IDENTITY_VERIFICATION_SECRET + + if not secret: + LOG.error("No Pylon identity verification secret", email=email) + return PylonHash(hash=no_hash) + + try: + secret_bytes = bytes.fromhex(secret) + signature = hmac.new(secret_bytes, email.encode(), hashlib.sha256).hexdigest() + + return PylonHash(hash=signature) + except Exception: + LOG.exception("Failed to generate Pylon email hash", email=email) + return PylonHash(hash=no_hash) diff --git a/skyvern/forge/sdk/schemas/pylon.py b/skyvern/forge/sdk/schemas/pylon.py new file mode 100644 index 00000000..1156f8cf --- /dev/null +++ b/skyvern/forge/sdk/schemas/pylon.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class PylonHash(BaseModel): + hash: str