From 705c563a94f7b124236efb39bcb8636ea804c390 Mon Sep 17 00:00:00 2001 From: Suchintan Date: Fri, 26 Jun 2026 15:41:23 -0400 Subject: [PATCH] feat: add residential ISP proxy pinning (#6848) Co-authored-by: Suchintan Singh --- ..._1926-a1cad008154a_add_proxy_pin_fields.py | 36 ++ docs/api-reference/openapi.json | 165 +++++ .../proxy-pin-browser-profile-detail.png | Bin 0 -> 112271 bytes .../proxy-pin-create-profile-dialog.png | Bin 0 -> 15313 bytes .../proxy-pin-credential-modal-rotate.png | Bin 0 -> 56046 bytes .../proxy-pin-credential-modal.png | Bin 0 -> 52708 bytes skyvern-frontend/src/api/types.ts | 10 + .../BrowserProfileDetailPage.tsx | 139 ++++- .../CreateBrowserProfileButton.tsx | 113 +++- .../hooks/useBrowserProfileMutations.test.tsx | 103 ++++ .../hooks/useBrowserProfileMutations.ts | 20 +- .../useCreateBrowserSessionMutation.test.tsx | 87 +++ .../hooks/useCreateBrowserSessionMutation.ts | 3 + .../routes/credentials/CredentialsModal.tsx | 198 +++++- skyvern/client/client.py | 82 ++- skyvern/client/raw_client.py | 120 ++++ skyvern/client/types/browser_profile.py | 3 + skyvern/client/types/credential_response.py | 11 + skyvern/forge/agent_functions.py | 20 + skyvern/forge/sdk/db/models.py | 5 + .../sdk/db/repositories/browser_sessions.py | 43 +- .../forge/sdk/db/repositories/credentials.py | 46 +- .../sdk/db/repositories/proxy_pin_update.py | 56 ++ .../sdk/db/repositories/workflow_runs.py | 5 + skyvern/forge/sdk/routes/browser_profiles.py | 74 ++- skyvern/forge/sdk/routes/browser_sessions.py | 22 +- skyvern/forge/sdk/routes/credentials.py | 131 ++-- skyvern/forge/sdk/schemas/browser_profiles.py | 61 +- skyvern/forge/sdk/schemas/credentials.py | 105 +++- .../schemas/persistent_browser_sessions.py | 7 + .../credential/credential_vault_service.py | 18 + skyvern/forge/sdk/workflow/service.py | 146 ++++- skyvern/schemas/browser_sessions.py | 11 + skyvern/schemas/proxy_pinning.py | 96 +++ .../default_persistent_sessions_manager.py | 19 +- skyvern/webeye/persistent_sessions_manager.py | 1 + skyvern/webeye/real_browser_manager.py | 39 +- ...eate_browser_profile_from_session_route.py | 70 +++ .../sdk/test_update_browser_profile_route.py | 132 ++++ tests/unit/test_client_proxy_pin_types.py | 104 ++++ .../unit/test_credential_test_trigger_type.py | 93 +++ tests/unit/test_credential_update_params.py | 572 +++++++++++++++++- .../test_login_block_credential_proxy_pin.py | 200 ++++++ tests/unit/test_proxy_url_custom.py | 9 + tests/unit/test_real_browser_manager.py | 72 ++- 45 files changed, 3110 insertions(+), 137 deletions(-) create mode 100644 alembic/versions/2026_06_26_1926-a1cad008154a_add_proxy_pin_fields.py create mode 100644 docs/pr-screenshots/proxy-pin-browser-profile-detail.png create mode 100644 docs/pr-screenshots/proxy-pin-create-profile-dialog.png create mode 100644 docs/pr-screenshots/proxy-pin-credential-modal-rotate.png create mode 100644 docs/pr-screenshots/proxy-pin-credential-modal.png create mode 100644 skyvern-frontend/src/routes/browserProfiles/hooks/useBrowserProfileMutations.test.tsx create mode 100644 skyvern-frontend/src/routes/browserSessions/hooks/useCreateBrowserSessionMutation.test.tsx create mode 100644 skyvern/forge/sdk/db/repositories/proxy_pin_update.py create mode 100644 skyvern/schemas/proxy_pinning.py create mode 100644 tests/unit/test_client_proxy_pin_types.py create mode 100644 tests/unit/test_login_block_credential_proxy_pin.py diff --git a/alembic/versions/2026_06_26_1926-a1cad008154a_add_proxy_pin_fields.py b/alembic/versions/2026_06_26_1926-a1cad008154a_add_proxy_pin_fields.py new file mode 100644 index 000000000..8a2a443bb --- /dev/null +++ b/alembic/versions/2026_06_26_1926-a1cad008154a_add_proxy_pin_fields.py @@ -0,0 +1,36 @@ +"""add proxy pin fields + +Revision ID: a1cad008154a +Revises: 45128d67bc1a +Create Date: 2026-06-26T19:26:11.232122+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "a1cad008154a" +down_revision: Union[str, None] = "45128d67bc1a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("SET LOCAL lock_timeout = '5s'") + op.add_column("credentials", sa.Column("proxy_location", sa.String(), nullable=True)) + op.add_column("credentials", sa.Column("proxy_session_id", sa.String(), nullable=True)) + op.add_column("browser_profiles", sa.Column("proxy_location", sa.String(), nullable=True)) + op.add_column("browser_profiles", sa.Column("proxy_session_id", sa.String(), nullable=True)) + op.add_column("persistent_browser_sessions", sa.Column("proxy_session_id", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("persistent_browser_sessions", "proxy_session_id") + op.drop_column("browser_profiles", "proxy_session_id") + op.drop_column("browser_profiles", "proxy_location") + op.drop_column("credentials", "proxy_session_id") + op.drop_column("credentials", "proxy_location") diff --git a/docs/api-reference/openapi.json b/docs/api-reference/openapi.json index 18d998fe9..183ba19e2 100644 --- a/docs/api-reference/openapi.json +++ b/docs/api-reference/openapi.json @@ -8640,6 +8640,35 @@ ], "title": "Source Browser Type" }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location" + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id" + }, "created_at": { "type": "string", "format": "date-time", @@ -9753,6 +9782,37 @@ ], "title": "Workflow Run Id", "description": "Workflow run whose persisted session should be captured. Omit for a blank profile." + }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location", + "description": "Optional proxy location for this profile's pinned proxy identity." + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Explicit sticky-session id for this profile's pinned proxy identity." } }, "type": "object", @@ -9797,6 +9857,18 @@ "title": "Proxy Location", "description": "\nGeographic Proxy location to route the browser traffic through. This is only available in Skyvern Cloud.\n\nAvailable geotargeting options:\n- RESIDENTIAL: the default value. Skyvern Cloud uses a random US residential proxy.\n- RESIDENTIAL_ES: Spain\n- RESIDENTIAL_IE: Ireland\n- RESIDENTIAL_GB: United Kingdom\n- RESIDENTIAL_IN: India\n- RESIDENTIAL_JP: Japan\n- RESIDENTIAL_FR: France\n- RESIDENTIAL_DE: Germany\n- RESIDENTIAL_NZ: New Zealand\n- RESIDENTIAL_PH: Philippines\n- RESIDENTIAL_KR: South Korea\n- RESIDENTIAL_SA: Saudi Arabia\n- RESIDENTIAL_ZA: South Africa\n- RESIDENTIAL_AR: Argentina\n- RESIDENTIAL_AU: Australia\n- RESIDENTIAL_BR: Brazil\n- RESIDENTIAL_TR: Turkey\n- RESIDENTIAL_CA: Canada\n- RESIDENTIAL_MX: Mexico\n- RESIDENTIAL_IT: Italy\n- RESIDENTIAL_NL: Netherlands\n- RESIDENTIAL_ISP: ISP proxy\n- US-CA: California (deprecated, routes through RESIDENTIAL_ISP)\n- US-NY: New York (deprecated, routes through RESIDENTIAL_ISP)\n- US-TX: Texas (deprecated, routes through RESIDENTIAL_ISP)\n- US-FL: Florida (deprecated, routes through RESIDENTIAL_ISP)\n- US-WA: Washington (deprecated, routes through RESIDENTIAL_ISP)\n- NONE: No proxy\n\nFor self-hosted deployments, you can pass a custom proxy URL as a dict: {\"url\": \"http://user:password@proxy.example.com:8080\"}. This routes the browser through your own proxy server and takes precedence over any globally configured proxy pool.\n Can also be a GeoTarget object for granular city/state targeting: {\"country\": \"US\", \"subdivision\": \"CA\", \"city\": \"San Francisco\"}, or a custom proxy URL dict for self-hosted deployments: {\"url\": \"http://user:password@proxy.example.com:8080\"}" }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Opaque Skyvern-managed proxy sticky-session id for pinned Residential ISP sessions." + }, "extensions": { "anyOf": [ { @@ -9896,6 +9968,37 @@ "azure_vault", "bitwarden" ] + }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location", + "description": "Optional proxy location for this credential's pinned proxy identity." + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Optional advanced reuse key for this credential's pinned proxy identity." } }, "type": "object", @@ -10240,6 +10343,37 @@ "examples": [ "cfld_1234567890" ] + }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location", + "description": "Optional proxy location used for the credential's pinned proxy identity." + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Opaque Skyvern-managed proxy sticky-session id." } }, "type": "object", @@ -21416,6 +21550,37 @@ ], "title": "Description", "description": "New description for the browser profile" + }, + "proxy_location": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProxyLocation" + }, + { + "$ref": "#/components/schemas/GeoTarget" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Proxy Location", + "description": "Optional proxy location for this profile's pinned proxy identity." + }, + "proxy_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Session Id", + "description": "Opaque Skyvern-managed proxy sticky-session id." } }, "type": "object", diff --git a/docs/pr-screenshots/proxy-pin-browser-profile-detail.png b/docs/pr-screenshots/proxy-pin-browser-profile-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..694c7e05868349a011eddef05bad7b1881fa35d5 GIT binary patch literal 112271 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 q0?JeV?>cW3- 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�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�&l# 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�#K5IdjIZBSk4Qr@j<82 z-)z28RF*d!U#UWFtS}fk9v@^9I%7*XR9{#qKr-W%i+PK8fEq}EO~|eTm4bDW1n9_m zWB&Q^(094S#1kxDfbXJ$f`p(B_PGF2T(}{Lf3q|AVlKs~$lbaAKxlg^B+^PuL6I9C zzRY3GRKX-S%2+ls!br|Y?{}O+<&7^VAIZQ_Rs)}8v9ZiV!AZtUqo$-JtsfPR+p(1yqD=QhT_@UnW0V}T8Y??hC`vHm>)puvhV4=Y}tEHb^1y5!VqYkwY7E6UN07;rjnDAN_6`8KHGT2Jx?^R z#9E?Jqr;yH!ePtFfR`~}W~Ob1wa{p5`I@GZ`GcILnBQjSWubp$R}AV?YSgK=pJyCzO26TUL}S4JSz!7jU(4 z-xq%~u+x^_oUvEj@)N9vMlft2bq? z-iL52DJeOGgor|ZVqZCDn`#Cqs7+;q{ZCFnMih;`4&{QWf*sL)ivyW>zPj2O%u zjq8-DN z_lVYcCBqBUjrH6>}dMPoJ;OQ-y_wLJCgqYXgH$x02G~qxxD=Bf3%kyy0oxs^Da2-cTDK zN`se3;TnQSdj=LzyEaSqNyf5RJKei*7i%=P%`3OYGddawtZ0T3Ha;yMM)}--c8mrs zlKn+dTc&k8owa-H1`p30;CpjFRSg7B6DD?zD}VGT7f2g`M}>U%fzSZzw=H~SGE!1= zvSE$t{0UR$it;RDHiOmg{uhz>bq`S0g|&f$o2SDad#yb?du^6Rt^ER)5*&nP7mqc~ zt@XdWo{~wEM|$`|=lONDsXBw9gOODv7+7b80Y@JtcdHI@=X|3&~K)IXbE~O(J4+AWz6vXW|UDQ0P8*38+Crn3jSC#uu=~B*T8~2+2Tr zRENNhm`*Z9H3#QB!>{z!t-CK!`a=kjN4e~+)PQo{u-ZN4v1iRCn9B01x;NS{>q4W3NjoHTs7$9CPd~ zb7~u%xSdu%m}EyscYtQL%2Z7eMtvfWg5o0S{=y_UJ`cH_nw;JN`qICFTBE0Su)Xb= zJ8BhH{9ks`G(u(|x&HHy7+~hDt7`%}t9E~s3SU4BgZv&7pzQhE5H=o2$++EzH>8IL z-QK4d9ZfCp{%>Sl=_Ro8jjq#4h>Ej2MlzyjMh1!*bbCsZ_X)YfQ_c8*igxSV(5v(dSnVo{A&*0@X-eI@2#XJJiFUeSE1Bi#a;IvFbL_-;{K+fbdodtnK@_LdyV2YFJtJ(O@{(uP$d##{zLNA|o+vcey@A1QT-dbDH1!>_zg} z?DjRVk3vX*zz2{31RQL`cbY=Wke>-oF#!Gsj{qPQ#N6g^xuJfLhaWfsx%GjDjDq|g z^Y@qMD^Zp?`*Ss#dPXCKXgP-Gz&5yyLz1zFoy_f0bME#|E2lr*3E3l&ttKXEZEEEg za9rVY*euU>uTU!Y7_v?PcO!3uvu8q^I-K?8uCTB^#jkZ5z!DF80PFlf@M{rwG;!mk zj$oRTpg@Wgp7Cm0dnxjLd08yg&uhnbb8|Zr$`$&COn#ObE_zZ!wHQJa=68Rj+3gXO zUp9tEptte!0|yZ91e~y+v1z+qj4IBecA- z6kIL!JRe*}oZY!{`}?Lsv2NcNZ9-x{k8=iNn+-R7+5_0&e>S+R{FrE|+YPc4&>%8z zPna6dCAq%9LJl@#L_m1LYC6U2I3NQA^Yh&GBLNh+$Lah+13}8p*EXyy4AfY2-$h`S zY3DY^DkA~?_Q&H9=~&P#4Y1IRx12PI$KI{VZKii(79uWCZfEB(Ol`R##_f9cOk3yY zPZ_Q(sJxfu22BR};I3=-M-uql^<47OQ3_;!I()-`3?Uo5glT#)Z%6mPteu6y18dg@ zQt4zCzxPzwYwNBK%oKLEAf6XdD}M~QwF8~iS#T5xlzZ3KVcCmD$Z-cme=5~xKFn#p zeJ$=8lfqF(5&M-od(kUG=c-K(<+E5%9x8a&#f5-+agxVSH%-#t7${ula75zt?W@84!2i+=T;nNkuG>NHQ`?a9i| zC&0o=N=WFGBch-Xc63y+wJpxfqzg$pI=07pgFfEGZ@SLO&c@_)4ujwK6rd!drk<@a z_dGa&$PC)7Wu>P_rf9SnSrBJTjU?Cy~@r?>Nx-Z+dt-84*^mGN-E<1+i!AmnD{X*fBzErlqqXyXl6`? zniI{EIIvD2+!bbw{wF&Glmy+&HuyIHPY0fyeNHQ%+i$9>egFAIqMU3*wkJ=XR8-1& zets{ywzH+@<4rP*XRqawnS3q+L7CD!?Z0z?_LwkKC#pRD7{KUpep2i%x)V_NS& z#{trnSHSx!KZkB!|EI2WbMm_cem@_SK8grHNhGQ%i;VpFy5F%Ulu$)PO-UKg?Hm{z zt2d>oShRq4gK2RM!{Fol4JcIUcpVCIaw$dmv|QA)@oE^4%gP* zK(YbVv>OnZeWgRXyqvB}Hw)}vHP?$u4uU>W0%{K|{=>^9K>6cSAwj|Mt>?B^R<`Ib z7eNLI6Z3dR2!P|{<806GB5>otY%mo$Z@CC9qUjApIh}7jr)cyQ8_2>&AYpA-U0pAW zLqPC$atieIElE#zvJ&G;=q4p^35)NkMgPD~64gH;r3ggs^ED2H*U};)GJJf&{}Nbk z3+Xm57(|v{ARC-)Psfi$LV7Cksh?ZQ5;aR2xe@a=rve{R`iH*ruKs?&eJS4H6$0KW z>%8XT1Hj(rH35wV0ysu>iogFo7;uwT=L2J@nY7djaL1nHIaql@nVH|PSA>9w5ul~t zCnNyc(%Z)#vHPn_YjXp+abSZ~Vxky`SHeJ#Xl)e@XL)m zPR5k~e&A&^SFzotl?}3B+y?#r8=$d{jI0QnLDkXGpYgJffKMf5J$HaOSEcsQ_ z?vqeco=pDFpSd_Wb0C}eA-3)sORR4!{*^2IbL5MTua%}9>S{kWj}W;aAX0I26W`|4 zH)57UdWgTeE+WgJH&*-y3wZVK$+{4()Oqo*a}CbT|Nq9Gb&Cb^U7(+U@9S?tg!|O5 z5I|V&28Y<<3NNYk&v}6e)(AYrAV0kRmlAjFT@3*VvHvB1n+b~+|Gw-0OJ@5?oWXla zN=;z$RS^|e5X=KaGcye_8&gkPFMxag6UYX45tX-rj4G=&_xqN^&)*k-q;-vF4wviB z3O2!+UYj7n`_zi*Z*>Pp;HHs__xO#%X%PN&d`gp@(3dZp^TgHZ>C3>yQ;((rid15= zMYFxE3I=f!r9xIlp|+tG-~I_Y7MAuR8VNZ$Aa`5gLY5i+{r@0kDUAXG;IoDR+z(BiL~p7y^K%zf;ibuV%%e zUHG{;h>nx8dN)uFYA{YQH#If&&pRHYgUZk`@(o`Ed;W8!!AoW%OOe>Lw6}8Bje$ovQG4R8Rz9}7C+T3NumjC;Xi&WaLEG-$}~Mqoakg6Hwm~)S$gqfWVg8-C1oDo$c}(^j{Hx z)v;BTrKx>1_|XQCe2KVPJ+O2of-ErgHL|vdsOXgP1S>tuw+1KAoQY08cn3BPrZ?P6 zDfAfk!xLk$iFzvdv86`a@2XBRj%0}I#Y~3lUrxIoID(pvAn#P zx-UMCzn<71t{13RlJl^b^=+ns{Dx{tTX^`njx!r9EQp~&neogtsrYUj(dTAGrfc6N z&mIYfi$!GPPb^if*|l`Q!q)bSosl;)NcOouPKo)X%+s^|u|^f4_2!R1Zo0N)JFT}z zT<<)vmPpcmpyT23=)%g98-EojRn};v!2XtpPeCWLT4qu)Z^>AzAvG5ngJcK@Q-i;k z^ta-SRNHp6;F93v6lK%PPq1|al|XBN%k!3j(ucs)a|8%mXIP8BH5)7qNy|>Ofwn|aB?gv}j+uKJ1lo|;cRXZPO`Lg4uPVV^02vnME(qVva1Ct>?UY*v#O0XV@xiS57e>7m9!++EJFqZWts zK>dHzIMdrJGBXvgpO@d&^R>HM#84l?Qk73fLqcC7iKs=KG|2a=&$g$#`_N%u%Rpw} zbmqrW*tS95Y={8>o#F(m6-^7Nx2ISuT$av!Jn&A%f(Y=YWJuz`O0%&Ex=>9Lab=sC zrt6{9N8Cx*=RnMoeKQX)$XNyBE552KtUD!Cw=oB#&wZcDaE<-M);%?SzHYqb4QpQD88W(y`m@ z?8OO@jVyr%iir4!5UL0HANU0z9h^JwtwI}&*04|eCwsO8DsyvlOD+cnoEx}Oh68`6 zEF`g8hX!F?do$s521kUCm}19f@g@L(&$3v&`J9r*)AFu$?GXtf?lg`|N&BL=?huqf3V?Ha)Wx8HA2tT*6yJ zjwI8GQ?l?k3o=c=t}&uqL&U7ERv#Il?RkLB#z5#-61S@`U9Zbn5BdlZ=rj-;$OzjI z^JSf;I z4^a~;503UJl1M5_nmOB?o}WABpVE|6os9=hENcT=Vq$Kc%yOXUKMqfjQg3iFG|F5k z@Vh;ZF)atOE9cu^sN2`eB)aINO zH9IU-bZ7L^np{QF8!GhWR7q=roL)=IMSHpRg!nkXP>9%uJI^-QzbF3l3zIA}pkq&H-@k87(AP39 zb(-QbC=BQg0O8=%$qB55nKwXUH5kjo2dH*z8Y<$^DsfR5HF(sep$H(w|I{+hEi7zb zFIUM0D{~@&>;W8Ft?jucv19!~$uocEA;Biw>vS(KI|2N{**P^oY??Q~C)}2u8{X)M zw$OCDI9_-SV&zO`H+%c}Iwcbghtnzk&dgHBH@jMzqoa%(^{%_4b6ZD2ofNrSeDA`5 zQUHn=;3q1lAoKOB*w#!&KM-yg6eQ+Bv(t(Mi?RFhWI0GlVPp7%@;g;gG&a{TVr%B8 zDMnIq${yeZx??mI`1JkdT4yKOQ-ohg$jCztCx_lMGf!Ef2j}J)NNmdzc^vKUb}6(3 zQPbDw8t+efPU|CIv)aS9wJnK2E0t3~abRwB8(bDp?{s>Wr)`B~9+O&0+HJ|`Q~Y9sDXz`Fx2BZx^BOJob+ zA>^(5g$&KjUpIly%$?d5pPE!~!Bjv`i zp;(-l$YDMEX@l-nA1cAML*xC|v|pf;R+hXFQhWz{$@uys@@mit@86bEbZ8h+}(GavK~XkEXH zBzuP?#7s2n)`M1|P-PPFiO&#kQ(*#Ypyb!>sjM;LmGc2 zkCSbfqw0EZclH;-WH^C%#K6cx?QtmQO1JC=Xu~fLeq$kjDw)6;QuM3Vx%R0#aru5v zf3}kPPHW05xkv)EmIAD?;cOW*0TTdMXEq)8O1j#;jCt9OZwr9K*x1+eO}B5Us1oXi zfbtPYpa|KW@}LLUK!-1p$eFyX#NhHX=+*tgU;31MBumZ6$`wxVNXHlJ&U$biSDPE~=_6A~ zxx?K{V$P+8K&vt{fBFZSQ8`_7d5c#V_Fm#>Xdq45ckR9C12>UbeUd?no6-NXhhT@yL(xPq| z7CHv|t5#->V#4$%N%}-X`-my>(o#}gjXWOhHlF2WL?1ZcEk|PpSC^dj1u-eI_ z+pP&5eR;PtF_CWV`1ok*2?8G;dqu6&8maT#F5b8O>B^;x(#8fXmvXpQ_sYEWf+x$f zbw@|c)lW^jiRxdtYrYI2s#kAz(H+TjeG^}m!^67|rO)`4%+^$`RNnE}<8ei9oIgwZ zgmum$MdJq6swLU5#A;!vBSlc=2i>12{^2E5r(2dUz4dq-&L2cu_4QPr4Z-?#hzi3A zLrhxOZ=WDeaGSOod#0uF&|nV#-Ovkh&s#!9RRGJ%GjfWz+QFKR?cvJX)TIdI0RQPq zeV3b6Z{3jMkGzy0m@0~Lg=bi3_UBi)l6?j$I9ZIp_^DLGfJ%INW+s`-+6BZklrfAH z4Z8n)+tIgK#z4<>zrJ?MsmPL|{9s1Lk7#u9sz)JypH5!$J+-)V9{UTY00{n^jVI!9 z;;slR?b{cbARo$6nv$JVLF{JX(k#SLmW_y<59B)us!B-4Ewth`yZ3=8$IBC)Nv z+MDI!q%s7d5Jg4B$`yLcN2?u`JGRr+rfLff^WZ5d7*urd>MNL-pxJYOE*nnrh3+-rI)P|5D`VOaN3T zxBd2Xk(wb4sPmU*8^aTqf!=*GY>tTMD)0@xuMKUXoQ{IRl=1twEgGP>WUkepy{U6K zJpuV9E6qF+^^*H0KwnLm45C)R?C;`SUv077@t^Bq8GsBYH$3s1_!3~J_Qy1RJM*9K!^&2O0@Cn2R~w7Zm!c6<}5UfF_g@D@jk9RGd1-) zfSIVGEeZ2Dw6|3$g>z28T^1h^!3nIl;Qn3TzzM|Vq#-%IuZBonZ4QruXPYc(i1BE4 zyO8NZS??|8YbK}FOkgVFX{@u}nRZ8SyndlIQ*L@M{o_a4Qd?qS;kK-m=N@7Cx!=uw zbWr#c?fK^W)md*UaI;Yu)*~yQ!#`VY}e4vIht3KxeTHy*P+Y)<_Wq}KpujxfW^M= zRGIa8p~^XkVbjAH8y-(5HP_vm)76ubdbU?^ck8gezAE_TOX+1+HF{fx!CaIFPK9Nz zg}>5JRaT35?M!C+oSJa2rzbzi@HRUgL875SVbSd<@ZL=Y5siN~uPR5uCP3Xz`)zhNh%wfN+xcm{GR-Q^WKa7!e|C z6S*0}%rSUH=?@}e%<z978U@_3+7Lyn(ymz zj4@|KTpb+@V6BB<2;OWd{|p$$<{Ruo{vPs$%YX9Ayx#D31mLnLqju^j+#2oWqhPm%c~H$_yseH|UQJlpW$de4X7W0kgNr z2`z#XGB*#)^FIPZBv3r%rl+f#bWcv!{6b49Qmv#1544aFOMcvcGtZ>(Z++kppW-51 zm`b&Hv|`KV4CZV8_?M8O1pm~s-vL6W)&KnVp=m{z3`=uJ9dtiNcMuWlkSrd`LmhzN$1L7A9NgT(ACU3OnX zR#UIW^2{eJCvF{GPh8q@a_U?z^XFM$=&licwv)MeX-&-&S>h>nrpw*^js4rO%ktTJ^F0u9 zD`veo_hn|H@T$oElI>#Zivh^fq$Pc4vcI?qEw%h<2Wbvrg?DY-=Y|;645bb>Zwl>8 z-j)J=*9Gm^n?+zg+Z@h{3Vh&qDV?DcV@wPX%fsomlM^wJTqis2aOM0IB_b+9R6>NS z)YVkX>VvIJ8)+~w9%X(Y+rh`(_^GYdXtv(?i*pTZ0~kwFqI$&VzS-r=&snXlt*yWl zkl4_Af)K#lyLhq zYK<>zQ*HDYH6#H>%~L2O2SK{HrFW(Oaz&_DjSq;vha++p>KqPFEhjpH`&FzrKTuL) zW1|CQ__&9Y#K)|`#UDDSg+S)dHQ&(S0DH04r&Kk)aP8?W4*%}Qk8Qg*v$cgK2ltPr zfb)uilA@Q2JRZuZQEo@L_VugCUHMOhLLC{+lY)GQ)$pkSwz#cris)JSIg zRY+A*$=FwvvSeF~benXR|S-z_R73`8|&?f3X7b$IZ5=4|-qb`#T9 z`K%G!&aP!;!*zI_r%@|SXChMx5C8fL1d13NX@+Gz@_NQoYMLDnSDE0ui5-X+98+9x zVI?d~;@@7o&DsfPAf@2b~Sbtr7nAkqJ128eIWFI!qk;KaV zq@qeQX3kRL5>U(oe#q+EoA)Y>#@#oCtmZ@1{QPn>h-JV30Bcr|b zs8oFe0|UT!?!ep$c!GiGwuy)dr@P8vH(OQ5bv!Mwi~;BEXq~@^8q6 zXVi8^6&Hj9lcRW#r0`iRXmR`d`Wm38!yw`Ww}70%DeM^i03>R@nW+luvl?s6fEUn_ zCC@fp9h}Kz)!RN(N-pi-ypoEG4oAQ1AMGI zx1|;#IwPaPIt7cf6^qTu=XK=9rp5kLi7Gf-r$4|TRF{GABz)T!yz>bP4E%Ut`T8{> zOhdDb!I~G1O-WeUq4k_9;LuL&Y^Srk_YJS(qkUQy6$ud)Vd1E(;kyLrW31SiXcc8e z^S#l%?iwZc%6!1Tvz;pZ&2E~u_xQ%U5K#a<8uUy-?Q=P7o7-4JPOmos_-~7l07b+? zOUrRSzJ|_n1zH6>3ytf@Z2TX@Uc6Sd{Ld&`-jdUYtvI#0BZ6w^3 z_B(!GP|OEZXquV?wk8wvP+)}tE(F- z)!nUNw7co;PB-I}sw2Pm&kOv!yD%y>6`t*7{@-4}J;&rGHdqSeBl@$bx-{XoRx^q$ zc-ElobBw3<#GEmW+-t-NDzn~emBl-UqCG^Mc9MYHWL@h&_?fGmQn{5|YARv(O@jR6 z)7~MWa>(WrtJTTg{gua;byjAYv5@AtarjDzeK+rsm~`gT6TM0P4Ri~?l zB=7*P^bgFo2?p)H0;^Us&W@L;)O5_UfcT5ZD`&H`@&pV5c7#Wd@`+A{fPjHE&#wRX z>z3{&4Q=Bl%%~}^4a$WuU9ItaacSw#wskq(Hq=)c6PAmgr&S+2TlEd}FgOd+Ix)mW z&HhL?HIvrzX8$6;1ykP(9|)@SPW=9$z$F!AYwGHcZu+%(T(!jp zzN-H0%ZA!nE!cSNhv8^{?x@e{o@km~NxUJG7WdDPe&*q@(OuIP@M*PgrriDZx{0!4j$QpD{j@9;%NIaI(GBED-xEW)^nv@d(e)!{n0`hbRo34-8xJK zO@Xg^(#laNuW-fS=cly}R%Na;e2;Rab!9=?vF}b=i-4o$crBfKU>Ma?Zh*H|{4~() zf9Yo$N<>Xhoz9$6_KLyuE@O?FntHm)kU2@{YIVQLYDHD4?DC|EO37X}jLWw&QRA`wUPLRz|Q1ctMx3{O8b^rpMfY9YcQ*6nJ(QP zy7%Ow6s!DE+M0@inNg!rQ|R@x3&RrbSw>%2D`&IWR#Cu8Qv}`*zZNX7pBg&xc0ag060FG$qppm&`_|QJi5OA4r}Nxms!*@f4W5u#eslgZSEw(L=sUXJ;4j zj}5@(v01*`;hfA8aM@7Kx@=&Gq}4&>W(jU8!u%^d`lb5=gZyi2_=r9_d{F812r30Z zFq+wgwAMmgHVX}|ecY#JU7H4%bwB?mnaFn}3={=UlVOQ-H10}I%!7<%U1uAKLZdTc@@D+jQOlo1$Y**TPNDZhNaifL(iaM{S> z@C8UWOuD(#yLsL6CR<87V2GNBed1aHo{!Ch=yaOkg*6xG$&oY0mRb9>4d>?+Q7}lnI z;F!vRzALZrq9pj_q(O^Kxty^H0obqWs^eO!blKxEb*UgntAxxPHlkZ>KG}&{dCVma zOjh=#Wi_VVLocfCn?I#RlZ@jd4rLV_!{wJ#^m$lSo0*<3AAL_OtdK}F z>~*z$-#E?0zM_NSPR_4wRk|l({zqqrrk)wKS)!pb1f};wo8^89iOTAyYPPje)HEIM z&Lh^^EV0bRKxzW!-mK`IO{qB{x4RRq1V#aubeBrstU)dTK|N$`eZ3TAC;#OG9jlCr zP7)W8ye9)Ns8ZPc3;nbXtev#GWSSHoj4zp4AIctP8X2WarGNOq#Pj|3Dm#=$31L5T zaXxR&70F|@Bh00l1Cue$-b~S)uK_jICNB$z|BadEM6j$ay=Gas)>p1}rqWV}?pWS! zscNWqr#}O~jKb0qI1xox?%n0ZM7@@ST1hG^)8$$9Y$o0nmqeDIo=Ew~90Sj;u%Muz z!d|R{z93&)PWK!+yjCC4)fJt8)fP$dD1<~1QMB{{OAeVd^_`*peQXY9)NkK5d#>h9fBs09I6dU9txao9 zxg~fgn`>)5l99rkR7poYb?V`9y>sv_d`ZtKr3#PYHa(}66WO~*j>{_hEytgMMkJh7 zi5VgpHAfI_l|rJ6P$Ud=djAl8k}!4S>S*|sN-KZo#c6~4PqnF)5FL8LwN+YrRuP#p z%zaw$h5*S?hm1i{_`A`NY}CYn4i{=i=;=!BX`vq1K$P<;P3)TwvlztKxbQ55f(>_% zb@VJ!E=txv=&Drk<;(8=EDPAc){k3EtxwH3k)BQxP#f zhQK2*Mvws%#Ni?od#r2|QsKptRYHh-*?rb&CF!pF&B+M{xo0v|C=OE>&rcu-aLOz^ zzy3iTRQ%IU?C*?OsM_8?`;m1nb2ET>weY@6Nr*YTUgNfuQj)}bg?p&7yD;;szoBrh z+WD-7^=t*GF}BPQT5%KbdmYo!#~Fspz?TW*nsnS|=HcVByN*k^VKV?#u4(cy{Q6k5E{`()1j6>ACVB#=y}?bmXS65GCCR;*_&u)Xq*g#iJcZL zl*4o=Cz?HZFVFVU-!dR~35zXuD}N4tc|^TP^7{RI+EKVrS|0PwRoKzBiCOIujs);s4n)hc9m&EIc<1YP#8g zJ#O<%_~zygd$Dd`;T9%!t-`tz;k9JOFy)H@HIcE>oEAm8Y{;~u!zl<~!y!0{|1K88 zh7o7`XM&J7>zGa1oFD=*Uc&(sLyLuq?5bEiXGMF1@Ac^~VXE?%5ue>@VQLIPLAQ4w zpUq;Zs3K&9$q2dG81R3r;X6(PP-d61ptf}Hx{4G{!n9@c-(D$UvHgji{oR^1K9CoD z)~|d=9xLhZ567y>_ude^HXv*7fT&6;#AAH#?4bdi%IG@Q?sa)&t}LBECT$W`G)OhDjgNVSB7#FeLu-jId;(0)xs z1QRUgXH+^{R3Nf6_xzLr#lfH^ZIp?q;VdAyxk%*~wKA!>>Yr`N-TBY^V*MNw-&}$Y z12@p}3k3L-frMSkR5l8C4XAsYG`vAaGPH7hyDGUp9w=?>wswpqDXFMW_D5K0dF;n$ z*AR%!6;Z6cu?EsM)^&PqYg5ab&-BRm=#GwKGTSj9$g-vHOaWxrVL?}CQW6Je)yY~% z<}f^b#iY?~x5W52ljh(ASKYVMV1LT}vgW7{!Nt4=>rRmxQ65m$6j<)xIOqL}F%&YDA;xPXJ``*@Z!kg>)u5 zP)MbFelqZw1+9Y(mQ<9xoHZ}6MN_fA_`q#HGaj9x3cT`iuiJ+o_Ci*FoTfgCYZdQX! zSF%H%-iY0h{edFLKhR-o2!_DCesDdd5kCq~OQv@Jl$@?PYQ4S76IJOOZa#;P9!oG$ z(7}yf_p0GBW>&?=T&*x&K9c(Ge>ROjGV})R1P0It$ z2e5H_io~L;*kLL1_9acny1f(wJ5i{dCVgKIeeA0(&^HD6++Tvk?@j85xYbBI90U3d+xxp0|0RBTYmq zAu~0T2l{LIOsB`o$riWic8wUh=5}`et@_6YqL&VzPwZ}w(6sg%X;2fv9F_Lz`~pCF zcun+2cFn1=$v2mjZtya>;b5^Zdqdzjs}kolb>ws*Usy;dP{2Nx7bE3iq77=fm*?c*ihK<$6jPz|Yzv-{s`uckIz5U*;czLVu&sfZ@0N}a= zR7IC~tLKQIxw=Z*y~&MEyZ$v?+u4SGz1m+(sXI*+tUSDFbh)QfQd;FxDL=`@{R9VM4XhD19P#{K6`VT2Kr^y)uwkH ze2=}Wzkhs)S-aW5u-$ROp0@*xEYM{PM>t4{=&jZLW>Bko!P3caIR8;j(#E9MGan8Av1Wv6=lUjXDM+)oyVf;-pyy<%TneoQo6@t=K~4|gHQdBiwSV_i$tJe4 znuD42<|N#E6j^TbSx%}3dTMalB&P6h&jJT(Hd7O{9qBdqm%^D#2Cz3eBu$=~bB+O|_o4{HGmpt;Y(L8Ptn` zYO%*WA0%~kSlCmDC_-BMlou83a+9oeSJDZ zk0S`8<;u$#m6TMX{>F=p4i5~KsCHanC0&`pO8~B!CWo3vQ0xS$*+eI~neAsS4j!!K zaj10yIbEY$yDN@{~O?eek;u3}T6V>VtY9!6;Q>ke`ZbJ48T+F--SRTZ0~{N&`J)Fk{S zIo}#FAIj%MShDXayfqyBn*7`E#CE6>FDWeHqmz0{auTKF^5Gt7n*qM8voZ6!<_XU5II&b&~D)gXt zC)3d9?T{O!y!@2y6L0Z}!b+sA4I~%FF@^N@^yL{DD&E(uP@LJVtHe^Pp=q1CdhmVr z1qQWPUE>aQRNm36N}GkQmdg8AsSAqTH(CBcQU`qN7j-cz0rp|nxkZKuRgBkcOgt2_gO+m}2Gn1T82%t(loLkkk8 zop(!CQ_N>c@=C7?)Ahi>G3V)jIis7VMT@|;wL8~G#l^PNo@9FB6kZ5GJbR2c|zt@%{v3x9lZcRBv1*VPM!tkF`CD>Fw{A5Eq{=xjKO7-97UG zK&qTv$`W=9rbMYvjlp5K`1nMGg!`iX*o3~;NeAGdu4^`oOs+Md}4-_@A3B=>{^`yVuol zN~yrT&21li6y&5-GqVc|p{;6mwk9dGSKERuywFuyIE}87mk}D-U-7dyrA5IMB^?#^yq`OJjr!#yO4?gvE7jspb_f@_7J`oW%w}-fwRFGRn$AH^u zJ7m4Qq%13p+jA?u0c(Q>_7%e~gA(aUk9?iPK9(N(uTa5z!%=+;msmGN>Re zotWjDfCyO19(Xs~8=>?d5u5Y`3uDYmp~*)1q{-Lcaw1ElX*`=19Gxtt zMlIYX0rs91t;~R4P1rqojKia;5k&etjx&=pjW6eVZk$=~`V3?`%8H&$PT z#N~e);E&1>t~bO7vu+W9cN~LFySkK5k(K5C^nrd!CB_ zEl(f(>tDa1JoDIhMh@Mk(=2Du4j())vu#Q}+48sfIXQWri<5CrxQ1xjK7(+z&aG*! zU68UYV&jpL{V_#4;lJXAdreaVk2pXD2C@^|{_~@I*Usnv1EST${QnI!_x}aoEBEvM zSAI+ql9aUYC7@jM@p;%QhaLO#_2TT^`Or!YXN|PADjkiY0$D+#riiHU#lMmKA7JaG^UCJ-cCf|ap5I=tbrIRm?))Eo?Mjk4YIL*npfQYC$Kj*XyDL-*QRqa9DPLjDTofJPUw~z&aVR9VF@?d#D zax(F80WKol0`a4*?!}{1B92klCuBK@(a%}!8?y!IZ9PZF$JK{faGKb*_Erv7OID-B z&#odqLo1@p3m_1p*vi2hL`w%fLH}2R!@sK5Mg$_SxGOBTcJWU3yuHx2-7u{Cbmnr? zK&*E(ID)Y;vvXJPHv_WMX2$l2=@n{kf3O6N+wmqQW zGC?1YkFPoHxNNw>RycF8eoDy2++;oBv^DYy53jDtE``@HCZTqENU6i!c{@5Gg2{4^ zU)DPDufHBzCJ!cTK1*qx>+=i3u0ko1A+x65D4XDrm#0WiUak@ApElPU82)) z@?5!f8H68MJ*hswTW&ppNgV@=bU!)|i0qIp?s2=@OG<(Yy?d9on3{T+(SCbC##2s4 zAsg?qeOW->2eW5Jy9QhtUOfO>ASMtk$>wq_k0(*tYoJ!6w;O+e%Kh`0Hlr?8)V`RR z=d`zyfn-k*%XzZcMT5p$klPDSE2IWC(9zd!wbSlS`M`JcIaPD$LQ6s6#iLwUjSpwT zvFQE&6i#-lL3znu9gczN_RGavH14Xsk1Q;pGK6RVz2J}U>7N{=duyXDgng>&%9hf*Q@yshE8k|#4BgvCF+*&$j zD0q3H^ZXFXRYuExT1%~MULfb>_Q*Gfh&@Y{_W=M0Rkj$|ge`E+%8riD!tKPi3y3~G;OzX z<9EKCt?bjPp;a9Vuy@#Z+Kb(Lyjjf6TOmQj#Ag z!AG;V`}Q@_on2i63GA>3LOrh-`ue(J!ox3yi%F)RD3XC{Kj+W=wOyzdc}L9)g`N#Z z?=`rYn3%=}+5mbx4K)qDUTN<{hiU6ZcsqIfPU)KjwzSuM{RK1IW|l?TRlXMr4wI`$ zJgfQQaCXSj?8E&9S4J}LuapQ%ey>}i72l}g2KNr2WOmqI(%xbPx>1`su7B|v3oB|5 zK^%yYF&sWAm9S=%$;?!i%QUgKYw3%*r6h5mRH$K1>Nfo6-=?1NPS%>z1pJ9IN+hCS zV4FfRkbp2bvLEK?PEK}7gOp|bTs62tJtK9&`Mn)jB-|C70ykR#CYRaT z3N8=0pa6@(!NFDA;Bs;P%a99sw>grnTA)?JJYQ?Ky|>E^8@KK4>$6@!2q9{^b@243 zzZGZxw-=!Q;;KDFGz7oFYV3%NCf3N)Y;eHoY;7(PmZMqB)pq1ff~kx9YQf3|?pL+Ao`6Wjx&) zGxYSY3}t&LS!p%dtHY{W-d4FgFG!e(Nxd%i^ZVkRb)`N7J0>rOQ_7oOkVd0#nq_jao)C{{adKUE?EBau2Mi#pet+umFeX-@$i zB~nnzR_${MZ~k`xSo?ns01+u&0<8KZOKAwW%2^f`pkM-nnGNHM@J6fEP`5TK6BqTk zulJDTLA=~fW6VYDUWY9;=*XtylSE!9TU7*mk#3XAZTemFeYC{KKoQ)V#K_IW6yZ#T z^UBU_crvYv?2NSPsPllKjft75tu+@X73T$R+AF8ssSvk`ThII8*3hba9x5dZ?IWr= z6yx*s#Gvcj=*(e|3J&IRIyV!E9>Iz&3JX)>&So#tlaiE7=J6b!Y>hfHWr*vc%Ye7l z?B$Q*V%I;%z$hy#o2uid{1l!X{!1ApdOqA=X-)Z;(3m2(6fR}wfSink{s?gPanzwx6uYdYT;SHG>**Y6=gsoqw${~?ND9TA~Ki5#8k+1c@a ze6}@T(NG>Wx@2fdyETFS408rJkF=0IYJ1*~PBiQ6kaJgZAghkwNZuCL7{2(k*b(N` zaD0@^=IStDLnJIBVx!C1XmA=cT7GhxhlfeT%F4$VuA}<t~4w;MA1b8b!xCB}rXks7Vo%H0{6H_Uv@%t}gSm@o~oI^bN=t;t@K(A1p% zdF!2NY;<|HJu^^q=RZVhyHMMfRQvgi=iR#{w%r-&L?-jDZsVPguRMBVmnfK-nD!PL znxMz3SgfomF=|v#Ob9~$4~4FbzT^F)8hON4869;3sX&EaDHg9DSyHlcJa=!yyD@* zha@$8`ut8i!$Xs!E+@s|QMx5gxIU5XU_OwT*!s_7Gy0t0{aA!o>4wlbnb+ld3#j8< zFD@){B|8S@jWP&7ykl%TZdgExf+}7qwQ~G)dvqwS%0&hw7Non5zEvSe1@kCSF;&xkj{oVWP?U zip}J-kBVi`R^V>%XK^64Mx$F>Kv_1W-OY3bu+gTc-z|I^E)YX)JKJt;ZqGMC*y&gc za{QH;9qZx7ncyTZ6|+210uf_|FdO4W8=A`I&s8_3!w>pT(I#o$8W;lxE`SM?|Imd I6JPbtZ0>(!0UnP3m!x@-n@RdD(WAwB+NKnX-V_; zZKt*A#NHgS+CkJTh87zQHI-}}lLX+At#x6um5JoVl)nN87hbnBuwWJB+@Gz;$lBwr z*V7V%u|u|6yGAHv4E3foriyeM4OV0_+JkL&2G188Z@zyA?wD*R z|9Wc+JkLPn#!G35f!uWL7JhPodzfq2iSiE)+?;GwK_d(@9nkafffVwiu%1gwN~(g4 z@Xv;vX6Km$g6~dhK`Ru{C%}t8SPRbx+U`N~-_Umr3guU1ycM8adhq#{l-rmWH)LR|0kTh?g{ zOzfdI4*;6tbZ0(%nJNmvC)Sb3*q9iPyI;=DKc7Bt-Y@-pR?qtw6`J(XX14Y4gz+3Q zI6IX%L%%+d?7TSn%et;oiHq@_-Fnm}+$7WXBa8!jGQ|ibcBySDE34v$Z1rF(nn)*C8Y9P*@cKU{GII0j@N~HT5Cf;v^!Mp&mG8KyFE`Ri zz5Ot1zMQ@+0H5P!Mf!!}!sA53UoNiUvkj5;?u|Q2Av6~0uEjkA4o}eq6hIp+A#ymo zhJJ$XaW%fn4id62^wZJ^tE|({r7SGA-Xz0l`=9YoLTXP?m7fjnLR z1y+vzd9UTkR?m)@=)Reg^m~teUOL(6fDolV-LG^sG}t(Ara`Nnxx?8VdZ5u@!>xrx zWN?Gkrbx3cq)HY547e;WtFuCO0hXz?jdLQVYJd3+J}&RSy>|q0cS(7Um`)2@(j_7O z#*jU%aw|C@LG)LOqQs?+nv4AYJ&USH%QXi5){V&&?jHJEP6}(qp4)i zIwlC$kV|Qpt%vGaS@EFVsWv62H72XF&ZmEZC0$1L78(7g6W^W@dFQx2c5{6JQC)9p zbbFuvo&}+7Qe$Lf46!eTK;G6Mt(@ex4`F3g*F zLbAH_RE!8^a{eX(5wwIOAlvR5=vk=@B%+FMd5jyK{oq;PGn@#@=(|Y5Z%n|^Jh?{9 z;U=#xZ1ja68E65*JLIIKRto1DI#8(HdFRt36`&C~dllg$^$MiNM<;Bbm#o-IJ$S#P zxY7LC@_e0IErTXj7L)oko*z3(qQzKSuE9mC*?}JYDtm!Ye$D#hK+MRRm%^L^)l=J5IQ@-cZ z_Wt!(4zm&^14CYRp3MEwQkCaKqjf%pEBYDPKE5I>#U+^>B4j=2H26gFIx;{3mXn82 z{Ot3cfXlHP$aia}fcW4YEv=QIF`rjI&NChkc%WxRiu*pFQ13 zi{o*A*4iH@{QNoCOT|o$NSpok@lOlnPoT=h!wc!n7`($mBIm2?WYGL>P=oJyE3dAt znZ5jH)jZ(!WNQ{gUV2=A-)QTTkdSHmvi8lp!I&5O2Cai0Kvq~w%Zkfbi(>AHIXusP z`AmAY5ykO=g@@yV6;M0mjaz4YY3Cp55EJRJnXlYm-IM~{r@1Q2IUpPY&nJLctf`WL z{PTDG>wsV5{i*^`9H-_H@&wV5yVk!omY$g?MVGI8+hH#YVy zE<~fj16PSu#lEO}3KIM8JHCHdc0V+*mEr>hJQE<{l>P!=zq?i34}tkD<$KXwwb&Af zC!cd9bPk2_IG>i*VA&+$;^2539_eU{D)D7P2^+b*?W!4>BElvM4ZZQ&^o{6_4)Z48oFFO#6;p*%?SOjspRV zfnHa43YR3dXkNF%s=v%H7AXDep${l`TJB+S@$~D~`fWTCSd$bDf&TKyF2#CUSn`J} z%kYn*ES2ke=k*)h7ueWDj5#yj6bzuSB~+h~5|?&8Yw<>q!);z<8!IqTQ25=LSsU9J z>pPvaUV!bA6U4Nz%NEQPAGfT*!2KWtn3{MMCl}>2%-GU3P{C0(E3_roR9CBm&8$lQ zAW#242)eHch*!bC6vB6Q5(%BP-|WyCXOf}u`89eRCnNaag`KI!hn~S9A%JuRDIzBN z-I77uyX|>$)^xRKE~291fF>pKE_gps)yVi}^!z`WYWL(tngAr~Q%m|r+qF};0^{7} z^QuY~HAUsg8f0;m+&vqge~yfxxnFkEa<9XSlA9Y@Geh_vOWnO&%!n~JHpium?bm;wkH;_$+hqH=Y|Jc^{@2@-hdTbVbcx0%& z^yAWs;XIxE>EVBS0dD6%euyvvjPTQ-z$5gxwTG4p*ZyI0v<%!q|FrhM5@Zl?eFEJI zYt3mf;dVt~?{vas-A3S&9*@bA=t6Kg0wAOE9-jk0Gv8F+!Ul?J8X|GL*!l5g{{7reIToKwn606rX+`pJj~UFfl*a7Tm$Sm z1Y5bia%S739=qKa&zl#1=*jzkTX?AT&%7X26yY1$U~Y~NbQ>s=hSk24%hv)7lI1o- zAE~G+0#!6di|SnGfTZIvIG9#M#t2u z`F(M2LW(Q?`E@O#EY2VEa$S%Ogd8yHs`>Hqo za;Re=O{6Mj*A0t=xxqcwsV`w#O>w>?017>>UGvQR*s;2a{^!$^%LroX+3e+u6B1> zLn22ys`hlz{A5qCo)~0@k}#ia`TTY+Oi5&SFa6ww{T_37&d4lvrbH(XrWOjS`>4yw zBIgOPua_Px%A)9ZQTy`;=X*XP$JsPgRK*vdG_Lj7Tl_tP1S6|Id>~M{#znXrtq;T> zEsjr#IrlT@9bu`o39v5h7!ZY&UPMVm{)cS%m4vLBg0kzHGSG&_+-Q^V?;X6?n)32y z*7`58VT(&^ctnu`Ll|TtH92u0PY=hJs)ZrZXiFi5;(+xjv6S~Vk07G>8T~f zr9%S(K&=lD%)Fa(86$yXT6?qZJjHs>Kph2?aD1Nk!A7vpe+k5^%v|LCGZQJO45-jH zVEIrCXs1v8(T}DUl|cr=tB7?$O%D*O=^GM$PIOe0cFUt}h7<$rj>Wb^`F83qs{j$> zi?KJ-DAQ9CxjtKeoGLbf2}l#f`+Ohei9pm-t+PdZQe@=MxR$2)|%H^$~{)h)uCj{pp^P+;BF)>!<4JSR;J>i=d*q= z7nhVhJj6(i6G4Q*E%@ATsUI@f%(t}x8^1S$oC&7DyI|V6E}yQC2I=nY>G>xl9;_jrqjC>9-lrSr%*}6FSgI!w z4s~GhrEJA-X=%kxYL2lo4#UI1EeFY={E*{d|KJ-F9{{A}M!ne^9IMfZ{YD|Hw>43> zowJ12+A0imycBY;?NFCgZUyR0pdqNPayo;mKL`Z8Q|ywGr_0NUzr@DIl;39Rv($U@0n%h{CB*QB?DAsLs@3z2` z7u&sV80&L-C%G{-RW~!gq_S{kgAxQp6s|Ljm&a!ajgZ5caHFH$PB;6mDeayHgd2(m z`m4^c3eSqN6c=+1V$?kKZU~?G`O!|DpZu66%Y%%l{4?r zQ7~nH9q7QpQVKl&-T#NUp(<_*`+%ymyG2?+AhozyURPIDPVFiqjQ_VQrSv8Lv8w#@ zw)Sq1+e9k#b?JB(4DV=Fis>|*(T%YRRyfr+OLkA@z!%a4+)-DI6zr(O5xj3HLe$g? zx1W1&x^HuKO{t;z1; z;O{U7J@+FMVA7wNQU_lBFG4XYzh*wE;EDk2QS}dD=K-t*?TVBthc%|H9ITBo26A%W zi#B@Qy4B6S8iUQFHA_`a7V}<{_4Rcy6tn~w(R#8D>DO9udI#6>G`3kjA~$xC?Rp%} z$rr>uYDFB?cb~=Oi(R42fvnX{cBqqsH@r4hU*rdG>QHeh&DN`_`a&SfRyISZhxu^TUUCTXdb zctd}J(`CK}qn^0<&r5!Vp^_#yX}>1yhpe(mO)?C^?6)*SC3Ywh-eVV(pu9kFk`hGl z*R#j2tW2ZKY@c(FZbG2H#{1JA51^i@bx zGoltzpvCfLwkZ-`Q}WjD9@&QB_Nz0Tx2X?+ya;ePW>;%Q*Q%krt$C*GY78mvTRYJ~ zY8-0T`MOne9qb3ALrGElqq&=BI*+Hahm$#-RRjbU$H%Y%fZUj0wV!|Sc=1If?}97f z#4~7=FapSA!ST`9%(Np5z=T~dcd5M4U+SAVW4z~;7`3XbcPCe9W81*zqEw__g_(V4 zYir8t^y4_QL`dxUW}e1NN{77}oln}qjq=!{STAs2ghz)HZ_UDEUWkPt^F>ty&cLCueAv4u&JL zbP3Y0dE>UMmL}u01hl0c9w{Tjx9#Q=kBrI zGWEzwPgQ@i^9B3TrN{os)9;2C&=w zs6M}U_jOGwX3Yr6VmbBQdFzsnP&HqtEvi-6X>aDHd2MK}1EtATvHr?zGH0pHGByVT zuTYnXnLRJ3f(5T(K#Rp|Wn9K$%fgv>E;2kC&ICvu;DOL>bPP$5#8@w!PWHYUx6vNZ zkxaRTnvYGMAka-!ZPk0Cg1-xwZES8s2rp!W{ID`qs!^-=Z7&>QZ1TLLEo42hF-bm2 zk`RT+$r+2n+1f^9q-ytUiL+XbyP#BE|CAJx%CEZ@%=qk0`jaK{Bxt09#z%*#oztQQ z`W;Wt9C0g#f1yrc?D-2VuG6tUGC%Z9mdPwufIua{8KbJYgGpB2U9JeX z`I#2xz{6t?{nVD|_r<^oNa{i7GP6&-sKF04(ha!(3xx ze?Bv*|IhMr%{2D+qRUx}lB6OJ6ZPI!AH}7c=Co#R?KFW~ZGxH&PiSx;oEQ@q0MX|8 zZgt!+t>?4UCUk%A5OFJg`9?&~!q4y3|2-O$u9nSwsz7UhX`lbRbWN}6bZ5*fssgHY zc8NY8c7>I+H8>Ver@Bx%wp*muv_I|s!y@%ZMYJo}@7>Qxv?`pmv^@+OYDK?&OB5Ua zmztLRYBbXr^^P?AOULHDbC9`a@77at_|*53%ZV)p(B1dvuYlLtd3RJ)SiKW~B|Saw zi+o&&;c2Q7nt2sLHh>!#7&ry-L^J}nj65}) z?%%wB|2~w6?>#A<^=C7Ai`@UGQFsUeJ28vHV8-JxN+l5$$KT^JfX0i59Q=X(-h6FT zQ(oM^CwxVSh_6q=}#J7&{Z>?YzTdfwKIAS%FCw74v1510}yZay|w2AH>|5&cA^@V)Mf!o-Z@30{JkQ}rotD)h?D3Z$kL0v7etJWdx@`Uoz}$B(j3l{jAclp#Zc zL)=i?N<+B{D+N}xph+q`RG!uM2JvxUk+Od*Uxb-$F4u8e?pq4n*eIqzGaSJ6^RNkC z@<94&pDjEYs`?}zN7v5qR=_=y$z@xk+C`sHiN|xa8VFG^i5P<)2%$Z~U}9s;YG`ur zphrXVe)rqAhQ~8EyQstWM=feg);%Tk+pfEFXJ==jX65qkHoIYa7~L2Tc=x_qoRk@U z`ADXwJKK1dDC1s#Tv5paKb8gx|B+$lk!x@0jd%QhR8&-=vhIF52qmYZYnZ*3)?YlS#3dNxX01X z8km&CO;(>-Yi?LrUOe)!nC-00DLxpRab}sQ31ZKA?r4Y*;{rP}s|s!rR-;506(~Qg zzM07EY~Jjl;bmn?Ss7%?Np%)1ihkfCovTh`eaLij>Oi@x_fnrWh&s(DnF9h~jz1sJ z$kK7@Kyy_!C7BK&{$UFgZWuc=1%-&TI|^A05NhYV2Wjj9o7>P>fe^-uby{HQdo62( z3i?wFc1EUD3rEK-c|)FgsF5e8ih=IJ^<=mq;}GLX&^nLHV1!SQRCF52Kn*h|;{!*T zzT5sAsv)?T8nuTRKl05vJP7X!W6`LV*J~=n8B6jti~yh+?`%K)w-+G7YP0<1wpZg> z!fbDfiYA1BSyNPXIxU{GVs$z?Z|zlCc?n>7cTEANN*@;*Enfs-5^+l>P1O*1`uLIMLzG*mRZ_|Wevn5=Mn>*=-8|(hN5XGC7X!f- zyOP~C=bx{4@~(48pkZgP(g<2_6A>vNFSWPNwa7gSAr$i!`?#>OIFD9UxRCiHIz;98 zRbLn}*Ye`V#@X`P%HuGqkrI(IpnM?W_K4DI3>PA`7>mbFq%iXN@fQuH2j_=E-5RI$ z56HrtF~aefIdtrf;B4ouF|XT`8XZ3;)$wsPdY!DXOtvQIO-$KbH|@!FJOl?f-THh7 z(F4w@wD|=t#4PQ0SM9s^y%VWs7bj=K&+;4jkiJwxq~2m!BxMhidF}M8)URZTCgsIN z@|?EY3+k%&2YV1Ygw)c47<-DcDc~?W;Ur=VuTRu|Mp2;KIGlz+Yr6n5&e)U`nE?q=GYCuYWKw<+_1Qeu~s3^U+&_b2oq$Tvw zBQ^98(%u!H=Q-aQ-x=eKcZ~0Q&v<`4{xC|&o%>#EuC?Zz*EO$sK1f*Y3vUmSZt!2z zW`A)B3m-rpSju&H;=pX1Ym3~JB*-6RJqTKXaK~E8atWT&Ap*1t!QfB^q6NTN{hoh{ zQnV6{8X+Vpl9qeA^^Ns8*duc@&zEDl2N%0#F7HjPn(E(wap?TuRiLDyyy9Z)EYhog zeGvDBCrMaXxLfr4_1gCt(QzoTOAm%n6`;5Wq!A19i^^j~r6fReb+S_4n4*6CXKi%; zs&aW;bR3Ktz?#D2Lmav5_wZb`vAF1;($a{HeG&JBs3c2wc0u`vckaA;d~Z<2swU8B zHeDyvWr;|z(L7wWJ2yTB#8}X_JIbuepDaQS86Tu0Tt{15%6+30o$+BrMNftmU%LFW;m6=!1BTK?+FPfq zCLQBoP0v?psywFN@kP@vRMbXZ7(H);HP^aQ&*)6IvpbVJ{7VM698=9R3j1}{cSh0H z)^^w79)jK#D&K0GvNDVTn%elV?wU z9t{i%1bj-2Kh500U~_IGx~}NhqbUBB72 zX6hen+4`M}EDdcB^mOluQZv%{zG)Csx9MvF1YVXUrGXJw3l)pWHSbDl5bf;Qvs&!= z=g$b-q4g(tIW#vnV*)oo=K!l}A9nVvmCB17o*O~%KuSWvVCVdt< z%EZTgm%p2U465+`_mV1F{=6t$1ztC z{eFNn{GBZ}{o5so2bpmOxmSC1zeC!{Jn@Y801js-+gy9Qe2`LabqJ8r_ zW}gYNe#W3u652haGk7(orI_jTDW05=Yl;Ux$3F9c8$Nva6zZsBRjP-&lSvrQGvvDy zlv19TWVn5?;v1Vz#v3s1ONr+;V;*VU<6ar3GcY#q-akM9*?!M!eR{Vp0;M!iah{Wv zwY@szPyKaq`y*ND;+Ip04pCLB%N1nhoCi8KmT#AAZf?5eE-f!RB@M|6LIOOJun}BC z^<%du11F8sB0-tSCoV2Uy$1oL7i6UszcU&fg4Si;A{dAJ38{%u{pdN}&<<7jer`mB zFIsRyR)yIi%s(UlO7eB)*}~k<-{X+=*)P9_ zE7KAx6N=w;6v>MDX~>JH31L!370t}e7WvNct9rV2l!|}9A_I@n>b)yN>lf3lA#cpE_(@XgL0H~e zW01hjnJLJG9n@)X)f^d=bd(M2bKSI%hvWVpSGdh9X%On z_Uqf1fDq^d10z2gHhLd2FI`npk*yQ7y2izICg)53XmRD8XYFKvY;z7elU-o6oEiBi z?r)FeY?_o*7_s?hZ07R@1zK#bJoNp>xj^t*iZl z-FMs+(@6@X^{}f{AEm&E_FGw<-5!kxlW!@ETh~Zx;vXYMUske#xT<`0wAKsn?p_B( zksS!-0y_Kx`ID8`9H_%U2}b?iz1T7HkO2tH=jrGZ%34Yd5FoF(RB!>g(0_h+Ho-9Y zg_K&2jF{`EPw{szKF`d2IvPVq(1FUL+xX#{b%pPI_MPY!g|*3V z*2<)NN!kRNKCGhm%2oA;9ag`_&Agt>J<&^n$T}^}y?OJjg}V|?k;c^d(-J44)`S!2 zjm91S%A9tD{S_NIlPt>fzd`tOgiQX_YrS-uhp#$ltNbWCpJfdq%c^y^HIkpSv(eqv z<_GvCVOS}ry3W`?GPH)6OkKcCy*yRs(f&0f67>_SR_Iulo4gWZflI%Sit5;!*!D(> zUB2{V4R+mb^Tv6z1i%;hDIXc}uGTMN3-kH;r=ZyuCgk;3*$lG-*8%!5K&hSQxMLBX z%d2#>2HrVn^fk&jVCk;No+2(MtVDQm{qz*UF_iH6^_w>yCce5^hXCZca`n8c$XOOm zz23}cCuhep5w{M$NFJ{-AV2>4Rb4%pE8NappUvOXR7X!E$B=@pMZ5s#-gV>X=nd@% z1?B4eZ@Kp|4;IiL0C%B%!Sa;h6&rmsaq){quF%S}&Qg%N11Y64F*1^pKd?^bxKD>J z4|duU2lu!xT@n$$8)f`V;SbRLF#bcK2d`dhQrxlNjX#3>>mr5?Ohz!wpjKESOY3galD*4$hRd0g-^tHq58)XAYfe$UHDV%w4cEk%(We<&i<>Njvp z*ztWB~A# zI{q87ttSFBwI24@^TP)tgwk^}FG&7*mP^3GXj>rT)2XRFQ2|vePtR+pNVOAGjk&qG z9ngxdZmXj+$9ay~#caxSkNsf867n7Efxq%VElpd!%!8$aBI^_pF)>U~%5Qcc^~lw> z%iwpJfBxSvZape(&w{h&x%2D1-%_g&(x{CGLQ8M7sQj^)_wMIB{Xamzvz849*9n)q zdhMoQ4cOmkai%_VUjm^3;8~vqS_)j;a|2k1-{14O1w7DHqybQIJj)}yF2Mzo5Cguy zKlMEEF0&5kZ%+GPPzo@2Yxu zH~t2ZrV3t`RR8`0uq!TuDfs8V%dEp|)zyuR?&{wvk2(Z;BPuDKTjKo3n^RCzufzP{ zcnyM=0*p%LvLy2I!Hc(Uflk+F9HAx#ZxgeNL03m}^P}I-L>qu(XE+daet1EvT7v|N zf)Uh+b6amOa--kw5kB9*;Pke>hJ{yS z{4ELGsw7GD>}1xmX*UM3^(E+zgpQ9frwkD&JQbD1<7C&yzr=)8vD&w^h@RtHbQ=$N z{?9vo&bYmL%!7IrbS1RAVbU1MiOvA0#q)YrHioqyK71~Yx~*=CT-W`6H-ke8 z@RF~Hiq4D|SZHZ2#VnJ61hH6zz{*Asr7>m5rcaiyd`%q=lx2M4`YjVe2dWFuJ&Gtp}SPKP;gShBpaHaSW#igVKP`bPd z2@IJ$eCU;rPw61&NA=H(?nMrcA7E+b*ppzlK8fg26i|;j`vw4Tty7;%3k%l=-!`>G zKC>=E@NB6X7z~WuH89A2kt#0&WJv&Fx*{whOz-aVKHIOp|H=jY2CTsvgctz>0}QaN z#q+d)@|?MAMVjJHF*k32{{+eqdT-gfSt!^d%^v;|9Lqe77Z$Xvo1>jveEMLkQ4St59De**=a3RNTt+G z4Yp7LM9MPFj~_4Ukh%1)NwLYk$D$Zz*bmazd}C&3x#Bp4j;*tS?xCJg!K^n?Uk_n& zrIxQH^W8V72|65!s_mkd0`^H+(yn}QiGiVEE#oJU?nn0GfF+1u>QiwVeOw zx$=)$s3%?h7key`el`Z6-HfXr^gqtc8J)2U%81ng-E`%KjKxQv{TlZFk1-N1o4r;f z#~y5jbse^Ql9}`M8_1;cpFN!#9o_lQ%SN8&<~now6c=a}c=GHcU_TEZJd8)hp^~EG zqPqv>fuUABub+Cpbgua9?@vrYTUuo{#^fdDCwvOv%tp60XKCq5D1-|s`DcjErr%rQ!!88pD{);73 zkGz*@;`4$Rxl*N z;)dWiFTcKlWA#m4GRkJ1Rs+me4fFD_&N)8VzN%*Sm&IpqrMFE={B_~4zb{<6I18H^ zrCvUC^mt4fk+fX9wha0jj(g>6QfCp0uVn5AzJ#aqSAX3Z^8|+kFkSycOis^#_tF=g z{OQK8A9T>7mNP=N%o%%5dF~r1Nclr!rDqIEBiJF2XC6SmJ#yf{1&|5Pq0bVLpoqZG zwFvasO{EC=PfcgM?AWD2t>6Rfrjrz$1`)n4ROCt4R@vHWtyLknqUr8+Xi&a5^EQ;2 zWmxS%cE(dDd(#n$l#_5JuV3O~DL)zcaKX08Vkt{keEE)?sr8Mevc4i1yXwxjPoL)2 zAxaRWY5m3H{NPFMw%ZVErE>WU(|ZEfzT=#Sa>A$di4*jiednEQeThft_FMfH8vj_9p>C&tQLOeH-|@cQZxWs5J}Q@<Q5-%bRhe?NkM9@IJ}cVUrY7aScQp7nU()} z|01XfGOKo{9fXmnhREe|5`>VYLA0t2=>A4Zw+d=jpPu>7lj_ybHf=>o-Q7+jVe_p@ z@a@7q`~e*)X#9T1G8FZ!zkamLIoBMs8%a_-yY{CgK>PMN@i2`r?ryD4EtKDWFk7#R zgcq&lBaaQwy@b%U>NF@yP}4YjZeewALTC+69_nj|GkucZ&{udXg=5t`ByxYpnQ5AV z#(8FO=hY(Nvo^xL%Bs{Q!Im%~`gS?2rA`QeLQ;vyY>SNDZ8M?ZBVU{b?%4_qS4lY% zdZ;}Dqs^>s#}89pvp*)kGoIcHMymbHFeUjU@Tn!DKl|h?$lRnoa&{hKMT7{b_{PS> z7;91^UmXR_`%W}n*Mw*d}-0iY5D?4*ij=f>*LcZu$8Xy4@1T3C1Xh1>?7hq(dqQj zQm0CvvGREd6C3!D$dGKm9ieTUo+Fgv7l>ci*ci2#%{I}goJB*VAg~4P#fO~yG>SKd zp1q|jFj!zi%m-@uDe|+k9-@{tYaL!qw~i>BUMS*NwSX-SN~@|$?G@x0=d{Gcq2fMn z99aP>{ZpNUF$Kn-_mAuF%2jObS`fO=k$U(T%3x_t2bK+HBWaW~xXJCb0xJql7GPI! zck7kwC@4l%E%yM=dw#(P8O&7jI{8a0W9XY_)2^F^jCrx7u6!MI`vZO__qwLUl$BQ-ghKhR%bIV011?dSdmvUW6E zJ$haO2fWTu*#tg$wEM`Deu`{C&310q0EFHpm8|6bv#mrgEJZpdK3;LDtSHSIuCgb? z%5c^bxZ$y)LY!Y%10oRz6t5>NE9~<7wX~J!0&xOaI9S=GnrZMgs!yP0;6s*4L4`#N ze8m#dy;59GPL4_!vS2i5q}EbryfjV-Z4S!H>=1#TfBs7nB;Ip=p69iU&r_U3<1a$v ztyOb4hugWI1%$A)$(uj&>$bXG8Z}%kA3ofkut1dIS?yYMmaaV--Z8ZaSP~cWeu`73 z`pM9iiLZKV{J_oyXiHp`XHm1!Kbx>zGKo^A)v^fUuU zgfiNy@aeo`Ee44fkp(tB`no;4$D=dA5pduxHWP>E5c2f& zL{`nlHHC82Qv1Dlq|E61VUfX%8UaK&zEr8Yy^h$=E1i%3FjQ$rap>mF$S@AGLjOt4 z__gj&TAT6c_9#?UC~yd0V&g`%$O$!NUJF{#5E1!4;Lyb}_M<#`6=e;07V9#cHwD!i zt!nnEX*#?LHwn34-o8DKvV*bg9|$hGwXrr))Ub<+pq^o^)_ z7incuX|B$Qgo(Z2V>ZVW&!s_5!h1m#@p8ot4Cg?9_E6@>Qx>k+(mt|qDCnOa=(Z4I zY9oXOMTQ=md^oFEjJ-vLd4XU`T;9X2eG8(bP0_+h3Ft`TnR4`+l?rv+6nSh`yFwRl zJ<%9h<=VW|ni_afOiTd`>*uJ~-yiLM>a!J9^x6A9{TKk&nYeqQFVZS1_GY$vS(jVz zLP7ia5Qb`?(=H42y9sS>jGl>W8TW3Aflcm~cu?z`^g?^1MfE6jbUwRF^N<4lf@N;% z$H{_T&#m25sb|NgTbiDE7MkiQ`A-EQsjC)tBukj^FkCa@d&N10`zA!7MOD0Fwoss_pUqgB?mnQ?~#fPlj>a$$qJ3bo(6J?-m#7neQ&{qeTy{oiq(aMm4uVFq|1)!Q=lIj2wF)KL8S7DiL-$vE3~-n{(hZnfj`;RIlC>1RJ=WWy}@h#)eAa9?-)2 zW>A~vkbz+n^%NXur>92hUarHIz~fdc{kOO!SsfRoER#!AVUHNrNRapr4vVw)T=btN zRTCL|xMhVWM^LyQ?S`*ZZOrv@EUI_66TC*t+*G~WG^e|?3u?I5mLF9osX+Vc=@=v> zOh}EoC#$Er)nAGc`;zQA`q`@5w|TAQ*;5x>o)Bd5m7y;0Ri8$QqOp!o3{%EpgiWx` zI~#wy6>wsejlo4b2a%}h&F|ilHcz~J^Fr(t_;vq&qo=F;u3I#8;xW-?)iK)?ZSs&P z?+6W-0=L;_Q46!$+gsBy4Go*9@&;vW$M4+3rmtL^6(y_=ds>ooi{yBut<@0ygZ^x> z@A()Au-$<5!dbNWQoYj};d8&@+3=j`{nXuP6#g1=x<4idD`i=e>(lALj7?&e`g7_q z&xR6#=-dRb(KxG{*Qp_UZID!d;I*NUi#zLK4=0q%YT*MoK5RjqI9!`-(U@QKY-rSD zy=RZqA*Nv+IFa5xa_1;&Jq2oQ2%5dW&9L$F>e>r}l|p5P3k3KdRP@B)f{?8})PuBu z5R~n(lUOKYFKD0}9pW^5vKqeq))5Wubr~!~#6N0mwwEvoKA!G8^r=%uO`&{bShrJ+ ze4wE=`nx?`v~B|kzxk66m#8I@rr$?O?~m%(+oJ-zh~t}`Zc1al=Mkd}8c}Zvmve^x zW-{rzD%ehaBc~qLcpJx`&{g|_la+~RpNuL|f{#@?a&o|1t9Fvg-Nyv)g+@mZ$@D>` zP(^k7PHghJQto4vt4I60o#m1$Z+CpYPMAlLXWCKQE96-QO>);c0Qmk$yo?xiAdS`wcWg{rVFJz7yrSh@xxXVG!8h87G(je88Gi&TIB0Uy+}^m zh2%&$$IUeBlu^|xn04V(;PkMgDyy#|x0M!i*YTU$TH23g3047ik00-G(%{l#*F4tS zhUZFTYPr=q@H-_o`15LwxAwgJ7KBJ{qowmv5_MeC*1-9h#tjb#%QvF#*F|f5%P`Kd zs>U8<9Iks3vFPF3^?gh^-*@|<`HNK68rw(shwmu8H|vypc`p|uBE^R15X z$-6J%(XE4=B>va3K?~#Sm3E=>%L=ZnEJ+H`kt#XD0&3jkqn<%SOdVn8nplRFr4XR@`J={wW#2)-BT*Q3!#yp8$rA*LF;6suSgYXCzu4ng4NY2(jhid zVXvY-A8JJ1(R5nW6x+7Ax;1h2)A7dlJs!I1_Xb67T-{A*boz)y`mJEUQtn@IeWB~ z^B1##zh2*Wtr>?0HY?Nw@=0OCaWs!Eh%B4XHV(Su zgkAkYlJ6CZa(9T+qu$D`xWI)m$DULYt`6eUEJ8^3+;5+r=}93+LddsQ9z=jskfgdk zXU`K^@rycb8|4R^VUcwR3M^bT@|fqbMl2`cZ0PkEbYAE9RoLWLK`Rwz^zQLVmAY5g zsIfl8&4V(4C@NpS-tXnL^-`-JIQ?f>wokB}!J`t&oI0m#F}f+{p0^$#8v25ab7HGp zOmD5cyoSp9QzCSE=({(ytTbzzf(pT)h^kW^Y7a)=DCM5h{}?mGOmptZgx#My7IcGH(yPhb zn`&Q1?1a4s`Q3F96LW(v@Zj~}H(02~6nJZj&9DXzt1r4@UMKJZBz&IB+_$CoJhwA0 zMX3A^q;2A7zBYb+`SQ7)o_<6AI{M7rP-a@Po<|%4g_}8<)fasOP!j|OKPj$X1=_$J zIAEh@4=VAKldH{%**Dg-685md^6jwim+1>)zx!@e}cnb6vU<1Do%nv%K zPc`T+Y7)N!{{Pq9@3{AW=EdWjFAsOKm?(2HjUL{995bP=-k>k)QEv;fiP~9-%${Hm zY{j#($SSZuG!l?uAx(v{FpZdRbKs5VtlIarCKHnQ`0XR6o;MXbH6>PsC;j76m=>K9 zY{Bt}>n{#-+yTzbl6@Fo=~Jd9KRC#4QEFcK#MWc`IKPMAvvSwTDy5t+EnrmF&3Mb7 zVnifdDA<*$U)pc9@3T#5Uj%8w!u3p>cm*TS9UavFXB*<8qGn$I_yOFA;{JZ-jbKRk zHFdNLVy!VwV;l)CCJR*4rF)m9)xIMR*sz{IzX-~U8<5(TmX?@%I{p1Qrg^a@BI4rU zlNXNp@^Gz=R@!M;yPGWN*R8eG{=rdulivB;80fGrW|o9@Rhi1!SsGjkvUFsgwFeXfu3YrE zBr2L~fhtp?)W5I;F8G?&7)vN%_GbYpF?RKKVwdTAG@RNzgh| zZaCTFgut`LBZO4)W{%xGjC^_ECaI3*Nv;6NsFdS|sql?(kHAT#Ci5FERAxGY$Q|Il zyxqMl!yvU@HY@R^#d}7p#mj>@0l1m(!4v#A%e9)S=0eX9d({M^KA!}bz+nn$RKeqk zTL;~bKI52*#F&?5AExZo?7b|(LfZnM9q;eB5-1S5o2{*WbPK!g&Dvgen>ugs{O3#5 zm@I(jGAZC8JmJ9I&Fk!9jd&Qgpu;;@Kmj*I%|QjBeR3Q+O>^INhqeoMiF`>;o`6Oq zcu=2Pr>K;xcu%$U;<=7Sh-jOJL^@|>fo$JRZpB17H*b2It(&hvi!`78X z4}zaint$CAzEog^o=)Jgwkdkno3Bz1_1ZYn)D&5&1YZtIyVsjgeS!J75!Yfol7@>h zr%#^-lR93fhuhSs@GG>);cu*`tHw!NlsWHI0Vn;$3V!+d^6Mm>91|%dF8_05dM`B- zfl(gH{*KN3xY0zk0^N1jTj4?p`_suT`8A*Q3f!kzL5iYcW3;mou@MRv>=;d^MGK9` zM@1#a$M4agHd}qIYz#si3~|*B%N3Uzot*p|^=pmqP;UjRwUd)wa7d@S0yZI(uC~yu%h&ZRbQX1Wu0$PVZBabWM&(N6?L%LMncBJgqnNoA$HS}USdKKl2+wQ`4Gt2>WkVHdvSHDr+lMJdd`9V z3#5hkx3gk8?1T&*lp!!KZJC=C5LJVni2{Q1m^id>;A))n@6ru^t>|`owKApx%^kFlQoPXA7yuF!6Y=H2mTsSWj!?33YfNTx@<$ zMTZ_UkNv5GEY-rUVCVehY53Bvw?P+9_MSMlYkBhS#P4r1 z72$6<*m_KZ8SezzH#1Wy2TuKux&CqIjH14MyCRh+mu*}U0F0}nqodq)A{AplmMzBz zc=^nO;(1!uvqKt`aw%PvP@5q=d zEKfy@vyB#i=G$9O_H>Vo9jZO+jBU$IjIw<2;2b|ZDmt2tEsRP3stt(P=H=JnQ&3#o zv!2FHMDj>VNJ&kQzJo{;WKcqE6ho8y3*1u@x$q94`FiMXUbVM#Tq>P0s|`#a>Al4+ z=lo;^{S4H-Hyzyp*WFEVeNg8PFv&YBkJsA|gUq2#{q*u!jyB3V1>#k<9v7dOXz)=J ze6`{;4exha=XgL1NeI8|c&YvPURHEQmzJjPXlk`^a+bbokm)VkhYHR^ZBP3v=pSy; z=sS(S-nnjdeR&1)Ugj0^szTD&JO^FuG%&lnlR?cQVj|58l%2)CZ=SDsG*(9*1q77g z0oV;=%=amI@qB{L?b_fsyS54SwjRF%S*=IC7rA^ITU&Bh?m!FwdjtxkoBYNENX;fL zeKne-e1XR=UNIsD2R{vFiwlgFSr$I^-&0u~DA}6%p0f$#0(z$_XouN*I{@^ew?!Ac zMxNk7!2(FqZW-P2u=@5*bm>!UCbU4hw|2X{ikZeCc-?)GW;PH~)R0^m#!=q0QW@4L zgKkKbDPo7ndL;X_WEnz|Wdl~e4bS`_somGk;Z~6ndC!XnA|YtJKVvZ?Rq@vk1gLwX zuUDh!vspYi2Y1CEM|+iA{vHqwUjz7qhh>>_2NdDC)f5S-_G^DPoI{AbY4`JO5ZZlA z&0+kTxS`__{=b2(OJg1lmnYY-n~kWz7J6+;A45T7Yao^v0_6NM0Z#7)4Tdyr?lir= z=H6IpRlWNkIJgJ4Cr zwoJpDq>MDFbC60nE`q3+;*Wo&x4SsBy6+-Al8o_LEH*xfio%Exh7`m&l#58YN^I!) zxfan3T}FjT_mU&Z(_pV3Vf&{gB=8S7V$XOCOwz&_QONcB@Rr!8fa$gYlf03_fYlK2 z1P9a#fCYh^{0W1oXh=}@?fXNY=Ja+y?VYDEVU6JnjXzIX)XemM+*uwv&QcBI!BcRp zou-|@3j{bLAX#J&Ex*L@ygG&gKrN`@V90pxCs0`3I!EX~24sR?HEWxAuML+wMoJ%Q zZFs$7JGeXxij?W!ag)c90u7Tw6sL@BPBFVUOLD14uvafHwXa6RMOH!Zak1v?J^+o& zTqiF}N%^!!P=DWS9JpDK9d0?AXCbdNOg;iAVUg?|1s$GOEy~x(JfKC8bmLkfBFH4J zXLYR1=~Q4WMJUi2ANF2t6OBhMSX{?BuBy0$t)jjm_+r{q(i-qwHtY2t8hIC1ojRI+ z^+|4bSE4d)_KseN&unEJUTwB?w)f`hYl23&lmyfP+=GW=#4q`x@U(G&ML2=Hyc()jSB&o27J69+`xaKto`5wB7RsPd%hTBhm)q=2lVLqIC!S{ z4#fR|IXIqzT1PC(kMdjY9oEy+b{qV>W*nCyptgj~KMxvqWCYidPC2I5NQuEVKbd*L z1zfN1!Gz(CGog!%S%>$1nTgFQFQu=-im?TjaV4rUE(5X2t12O!svC2&@NhvAeH3#s zO}*rvV=Ce{NPxR`gMHZ~*PN7KJVTC~qym_VnK+-F41&C2(>?by2TEKk(tf?>vW!i> zs(Ay7^~0xK<#^f@qO+q1Q5tXiD$?1jwlx@J<2?FpI^hnBPKsIvw7?yB4W4?jcXEKq z1elj`ck?lpgZrk58$VvO28a1951v4lQ1V8yf;t!A1!`lJcFXJjb|$`iv4?eI>%Idk z0zm={*kvPYImoIrXPRT+l=Z7L*Ok4jX;MEx$m?Wn&6iDdN=T^=r&lT))!qFkj+yc) z*qW$&4-aZd*P!_jn@z#2(t!v4*X5!%tcPf8$(rk|xkm1-)!= z_72QRe`mb>vgs45P9l&$AmyFu+pBK;lEI*W^^ZWaOJZU|>+TKUa)FD+ji>nMm#|rt zPFvVbLw$gr6wTuubL){Wgqgu>ZE?geUVaMLbKLN3Zkc?{TR^s+nh@m%|C)Mu;eR+; z8}|=)qqKBTTEIdG^0{>?df$DtM$Qp#tvCXhDxEUQB~H;t+ck|heHVz6R{Zt)vB}AR z3c`$8)`z^ly-4#+3*dRL=wpYC9j_7iZf_4tcU>oQZ8JFXs$foNd07`)NG{Myvb}TE z!F{;YPJ`~~p68xo4yZ;01KEHJz6usMQJc2Etw`x=0I&@|zkRC@aj*^=sjhAE$K7xy zWU(F-bB;e@vehSXeXQ*g6`?6&Lb#S9!t4Wl`t3^-UbKN?zGq zN;;$F<$?|*R)w%HHMfkR2~y_8)FLsPH!%@lhgx5D@z^eN?aPJrN~c-#G3VjHDbKqv zgq4)C5EG*XwMy6E+CAS$j%%c)(XQ|kf;7{u>)EY2!@|##Z}|~vB}7?+%S7;6<(WHI z%S%aDPOVtf!Y5K-3=B{Xrg1uiL^7{AWf*bUqttcsDX>`(6-ilmfs-2eB29!Y_M{{Q zw~8ktOTD8{3gNH~fF4c@tH50r_H!9DG6_yvxo*FdYskXBGEhB6BnDitBy1b%@!x#< z*UtOxm>ircxi?P1b^NSu`P0d{8n2LYr=gqFtI)`#oBkc!9>oAXmXJO_l65BultzyT zN%^fcD>UkjRT-B+En(!&f{63i0^6v;+D%g6G}%oq(RBfVSZ?G3VSe?u=l-|S*Xqo{ zEGt=fXZf&Sn>ei&IWXUyc;k&!`BUs=%bPwA>V{`P5`(!FaSSZUB`_ndPIX0H9i1#I zmtOACawks^1<}bXIdyxh6JzCWb$c(Tr=NuLo<|3gMyb@7)pY7Q+%Vk+zN0QMhCWV! z(syx5@=kkGX^eo~V4U8$pAvy~iU~#{S>T|5r6;&Ili5eq-TMqyIi9>=TeFf4Uo_rx~sycKYO)e0SO3Dhk_g%OFh8M>MEY$ zuLZavID$uD>E_N$sRU^&neXD^hx@vd%+GKGTnWzNLY1Q_yb@1!(f9 z_SVzZ_38biUAdtsKPCPXE4U!A#;RFabJF>DRUcG&JDXrH`6gbeLqaKCx6z1wU-UD-d9> zFRDsEEM4jUb_EbZ9(Pr=ua1`f0<;~F!1(8MUiPTjTzKu(!(Zw;KdN64S-1Vt)}q|l zWt;dxAJAs)G!OyiOMq^%h|h4z;SO(#ktps49Nmz}uV24%N#KCG<3`tJX}&bE*FM6@j3!mipxMz*x(9`E zmWQV|1-3EI|ySj~G&!36ZI-`ZKmS{DcIgvt`;2|Rzqh+VwMlxX)| zYp(^Y>IDLE6nY&9vYy}vM{MY%1Bhg+eT^4F=jtSdg;f<4WSAC~L-_kJ3%|-AAPe}s zAXoSw0CRoOUsKFm`BU%z+l$ObAy(IE{7NNa;76hl5Gy*Y=&Z%M8HRR~?lQOdQ28hL zj_17wef6ho%FOTK4j$JuK8+-W3ZE z@HXoHTpr@TdZWl2)*cd5?aTG)Qzjq34-jR{GR_gb;cRv`E`F;a{#}j8e|pr6+qkK} zo%)rpiB~?I*c4FN9Ac-dG}e{Y2&k~(q;m{J#Hnn|O?7JO3b!I}`Q>)=5^i2RYB}O{d^`mhnt7%=(_qaMgsDme$Kq@2zxG!&dQga zn}mK}v1Ef<#l=1fRwmh^Kqu7Deeag#cxu8bpF+(}s|}MBc|hZq{gH^U3{^&Ca+h3D zbMnq|BVjyu$1pXZ&MF= z#_~cp>y5>>saidfgxt$dADx)iC z`8)G|rY>9&_XyP5n%z&?>a4b|rin>7-hZq8cOdBni<57N&E zI&qArQ|q1Gt9O6j*LCWjuMo8gC>QkTFC;Q6@k^k~d3Ii#ZAzV#KO=>Pb zPp@iWOx8MJKS5}sFlcuFW8MBNq7_xDidfIWQ>SM%XD7kTg$Ps)LMJZD<5w!w9LG27 zwpuZCYHzKvx#ctZD)Oz~_?frPH=O(7bKeb95gs$bBwxr@%@!i4x71M(zMO9(w~DPM zya{XKGT-LCN^M2w#VeD8E$U*>{lBZBk-#53`>UM9SFMc)Pf>B$IemSsI}i>@?}}m%&grl(u2|y}6l> zr`M%_ry}B2ujO@eO3$4TJ>>LTk?7Xod4beBLf|d zOE;ii9pPxBs~)cS3N>H$h)2Xeegch%>fIkXxWW%+EBW&^%9++! ziK@vc^|n|@^!R;RUAZH2BqB&xe?-BJ%D|D7UVNW<$Hs=0NgSh$cfQ1+Q+0$)>LmA` z+&RC)8zNiH=DRvvF2c{kNIiiz_sL1}B?h&pAn{O~tz&y8{<1s=j5nerR+_aH>=feS(ul1d6icYn*nS!Kju;t- zLAQ#)rQe0CnI~er*&{llABYq=%r40BD&&Cqh#TlxQ+AyjjdWyzS8LvWJkt(}W}NSZ zV`+NKk+g>&ZrBt*6M#ah)8qvPmIwK~Y29N0M>8){EYD)s=Dv@h(!;eEBB}F2+24$4 zk(f(jEcW&y!NFNWE<2d1Y(3=rt4~VT;J5b55li zMX;|JN9FztLN*R+^pTNoFLd8^h_tsWmJ%DKA5?FTGtDt`nSamu&n=_p6i8Ej-x>A& zTT4fEDn~A<6eNa@Ur7&m)iRy7`aHVHb1gkrt^~plV-R1dYIx)d?2HMq(_5ry(p3SD zJy=c@qg4Z1bFwFrvMk_eq=T8u4?P_nRT3*T?zP%GQO#g1XldI}CK#4v9Fm>VP|$db zyq#={a_cK$gzI4v>Vn2rOjFqhnh-*>vwbo+r@nx79f+=0mNKP`fR)*wTZsyu)o>hi zxv>&!lQEQ=f=ZIz0GYgRr;X~r9!~>yskoN}(aJG)1XhEi6Eljoo`N0VxiIq_XULjn zD^NRzU5}@Op=p_?z>G7S#xNMVx^DQ5HJ!iU8Q`Xwp6#3z19KrhCb5uZE>n#FGBN(Pe9mL}o~vP~^_Sdl6&2t1p3AK6Tl%E^slq&nIdIot z?7uP6CgU&l`sNi9nNer!dA@2HEWYSD4W{V%GcH#C<}tLWi8k9**oXK;LHd!S;Da6> z;OEs?NlmBW>(6CwYo&2d+dKk4Kfq!Nt`lEYUYdFJyFcXsz1qOMR7SjwlX2)bN?q}~ z;swZ2-jCha>(qS)M|4^h1J`i2G{Y--%RY_X#+ySTWJroYFedL7(< zAeVsk)N{Mv$N74U@t1!P()c@>F5!U-oBtaxr)97A|He%&YlC)JfKYcU5Bztpn8GnNsEJJdkON z5$QCEW4M9tp>xzz8Q5GiRS<~k_pN=t4iZQ_+}uo~2;4E+J{A$_pxe>gJx<){(AI0U z#%#XReumCZl?$T&Qb_F`D_=;3(4^&&)OrL8VY>JC{rlsvSq3GCp)}<@Jsa#O#K~cp zB%kL_TjXpc9ImYrc3f>O!H+>VVQAW)_kB$l2|=yKydzC zm}wS$)YST!2?$nxTg;)iT$VCw2tC$>8~z=&(a>-~9yQKic*2W1x+iIE0>&Uir|VF7 zDmZ67CXBrZJ;?2i3_D^+&t&nV>6uMfFeAjqz@V7un;xWC64*J7ZKPf!V>lm&*-au8 z6z@Bx`0wXIF!rEA4!#yN^<3i8r4?lU4@5(-7HLLe_VFuUw?vSX>**dlAHgGOKACP@ z(l}p@b@x5`qKuh#87LjP(X4jjOjC%>-Xb}tpjI-lb~6!LySZf1kZOKycAh7- z=GWJcwo`BN)^{t8<0jT8?zob>(Jdj7!+!JqN^&kHkf{g`QEBP5r^#o%KFL#zu~~O= zUIbNx;zrChht2ZjaW^IZoh9se;WbJbS~UEo%Y%iGZhv?a6Jq4g@2F^dYBsVtFJb2+ zyuCZTG3~XLuJcms+31H`Lfy%wUX&^zfpG>OlxK}mE+X^FW$Pu}44}jsPiynxq^jtn z5>4$2Ht@A_{5UU1Bf%}n)O&eUNii@|Ef9~@>1^r_>E&e5CVQ!uw0U~s558-~02~q|{G~05%7L=^KgKA=!?`6KPi?3T z<4Hy8?$tb+j{1Dv`zd~{nLs}!Bm@a+Sy?*27!*LFKH3~xRUt`9OQ&9U@&+Z7lc?)% z1L-TWAu0QWY6g`4Qk@uq8U&?g*gT7T%gO*y&j^z3e}pbVNqC=jbwa**nM23m;wzRE zzY!4vXlObS*qIl)cL|I$biBeA@M&k7wH>?3ic=6dH%p|OgNfpBS+H-- zWAmrEZ|h8(xGn)UX1^`Tz&s_K%2l?ZqnNt=$yz=n(!KWEvtpY*y`? zS2sxtUC*I+L-zg2bHRkjbH4jio6Seqcu&Ln!Y*#@_eyqZ7=)#+>j7>m#q0c%4+}2`s%^YJXlnu}&(O)5kxrSL z03S~<@=UXcJv@fMRI13VXsT{|+F7p3QPlWtqd>uZe7zm1Mry4l z;$igWI2*{e`aRBz;!eG0Cn8lhh4@@?zP(b9WNKGN#RMs1KL=PsBSB=MFv$;JNp-g;M5C zb@*G^ny@BzV|_C&AVx^E!u2tpb|uc~WF&-d8jHDBbAsfM4uuqsBi!BQP}4 zrtsIjTMI;`%fhbbts+$}O3KCA1WcFVEc?o|Gd$C5mij1jcLGmyfI?hKcd5%jjb3k$ zEti(AF30jc#Q}_VwY|h|A(HhjqYO>* z%+IKS0uRJy8Zp)HHe;bXiMGL6C-7{kU8p;S=U(`%o|>>_CBbvzrK(uZr`NsVymB|E zjuN_U&LOhs`$KVYy`@k4a-pbHTg<~!zusVdBy3Ek*0V`2A%SnLW_fZqg#t8CZ-4*l=W{)Q!@%6eM?m=wm`wGfwtTs5{?^j+ zDX2I;zc{Y;bKn2pU#|np@IQKgU*Es8vGnz}TJwtE|NnlRI(^>0_xG&zE&k8(oP7WN zjg5bA9&Qg?8T52x{U76Phn9-ZkKbQ+H<)d%#k05N=OoIaBAkJh&S#*fftwWU=k42T zm7cO>Yu?7^{|>I-W0?5#)tMXKz%t$H^OwiR`{vJ@9}lj>!ShYP5umekO?Udv4*Tuf z2b>q|DAu2C^6%@ty?wXeF9oLLo~X0H^+fq^UQD;!yY<7;f>!2t8Am7G+?sWDMd0$( zpJ&eA|NYFrzNk1VYHPxIRJ~9$-HHt!U@@?buS_49C{I`^FC*>?+NxzVy+y{F(TkFR!eDxdV8$ zl-Y{#-L9YA*3L4QG*1MrHaRE=JWt>}WETf;jZMabABV244p%odExo$RbpAZxHB8YP zldjf%2Dbk~S1&tq=m_K0b+NCfO!l8)ULW}8%#^<&YooTNc0YUQooKc4f%-$^*1*NU zhECM>j7b~6_ibQhZT&31b_Q@~IPeO)1x6g8(-7h#CvwVsm yN9lkdx#NlN{{!pC|Nj@VtAS<%*+Ao>%na>(S6{w-y*L3V!r_hI_>g$fJ|6--K0NYy>-JR4dQ zQyYH-wr~IpF-}pbPAutbM+e%_CDZvCd(DP!Sia*$udwR5I%cG@G#>QH;p-LA)%tr1 zDpt*CXOCI#!DKqC*)%un>tQYZ$k@2kCkeTBdA%+2;v62GWzhjv5?3Gz+%AxsoS&VW zRDgEJShQAOUYV-69@&BbyBHF6Smo$I4TdBo$;1K+k~di%ZGKSN|cRE>?w<{s|?78c4=Xt0dMHmDL%D&VM6?3Pnmo z@<{sjlN)rga9kY3zvs{tf`RUZLZAf$9Sjgg@CTjx{r~Ayn2}ry6XB$+oa1t4db$BN z5c0pJ z`WT3cVp`&{mjkSZ3+1$3`90sFUiBKC9}fNvNxr+&XP>bLciSo}b> zW=U{VD4{o)^>!zHadDV@;;S{c^H+zH8Kp1&PhXOflR@wL;Y9M{#Kpu*4NF-~^Q46U z25Dnz!|Zxr`{hD=Vk|!Q2EU-B=FHLjDzc~hOt zt;q>e0(DSfc=)f;W-s0C*4m3w)QhCC=p8|?EK71Z5=|Hb)q8jJ ztuDNuAe1(Xi?jC~OzaArfTz~#biXcPj`;Z4yvyBleta&9-cmfeR=>v;S5K|n+;2Xh zzTRU}S50kbu7BZmQRI@70vNR&YIpz|q!u@z>1Bucax-+|N9)XwmNSQ&xy6u3bto8^ znY0I3P%roeTFeitP{r!D7~&^>fQ7i=x8&7ZU>IM*(ilO{)`z&%R1VuwqI`uq4(A4+}9iWt=E(>eD{^%=dYgPVi+UBb-M}H8EnQi=QB=&wAV%a z)z#I~x`qoI)e0)$1zV|pEEbh)1~efUvDMyTwhNvn0grbSIovKlNT%ZU1$ixp&xc9t z`Kj(`SdO<`_0Nv4Kbbn%d~uXn$OSJ@E`#Ou{Z-J8cq>Nth2M`}6YlHdLh&k;yW)jesALY!srCvVIm1D2%{ z_-w5Q4ml70E0`RsJC%pA&tFys17<^fJYDX*UZH&QF%}3u*TQF#wmT(axqd-)nshMf z^G(0N=EU0l4s9s;a<$e#N!5>HrB>3cbcy@igXsQreDaK_fjS{<5~cQdwgzeGj|>m( z&dv2U%&u{cH`}v3;B~2~x6lcE;5%*%LWVg=pQo-JxLWV@hh(N@U_ijuy;B4W`_iT_ z5YmOi9jG)d&rAC*f(n>kb;7>$-u&zvpe~uoIs*0nhxbwyd4fb)SsAMtV^U@4d#m#e zA12bh%fmxC$y|PZey8c85v~JMhqHytAPq^frtzo*+$#CBm*+hZ6#i0i`oRI>$a8g4 zQqt*S)%)qQZrVjMo!Yh-k|$;tJuUER%@{Zx?d=5#+4byXR$5Uplirf{inpY$AE=7W z)HX3OQBhWw&u4XLlEm#G+4X}R(nRPvjZ1uUefYGGumvVM)8_><)Z?alpcmYyHe9$z zfIp>1Ggf$+=|TyEIM-iyt+~>0YL=S9_ekC*{%6(NEV?0qq^1wQZZmd=btU7~mTSe@ zEEyf0xVAPTfA7?;SygGN;&s#)9%4f7b)pa|s_5Olz128ZYwZ!!!s=p#lOBbB)(V|A zvLux*9&swus`1q2xF34#YHEZxIQD2o^1&{Tt)3|;@1&%EIy9DlG$F|`{C*>u1#^?gDeFYH;KSZ+GgZnW6K$irdd zI@}E#r;I$@*D_QBWo1r?aQpn;N;W@YCTwgEOHBhYG+pJoV zm6Q~n#Op(_v(vYh&*8$8rkr6G5!83#_4$m!_eQj$cUmw|z1Dq#iAz?<@#LBOaO&UJ z(qPIlne)3_+Ca6DhmEy2+HoIySgG4H<8mPBwn!YNX|!98Ak=Fr7of*cuZeIozrEQL z{~ec-Vrp)pEIz-zPph$n#IoRU^_A)!$is7{UIlQtWiW?>TW6(4x!DU)jERcUdtCl& z!Fu@G<28ddS{naYX@B)0GKOooK_%<>Pv>M zD%kk9-J|69?`8pZL4REt5KVn?m zArAeXn}84Vt!m0UI+!m+(P`{+$zP|sy|t8EstND#;n91L@fU*Z0$3RCTU%!myx!k7 zR~yeEs_X|3f>YEf<+IR205_)(Yd08#*VExcz(0E>q#uf=ee?xC*tzPGLOX3uZ32c2 z--&r0#O17ii6FGttsyit6Z3q<7(^^+sz+@j(F`6_Ir4scf(IuMb$r^yQOr^d_N!X2 zwTi{K*SS0)G5STaQV3sgx!%b&T1{Nx%KRmv?3}9mucJ$B^C*^FtOd~-EjUnK5KemhVo@_mtRW7xdZ|9ak6|F0z z5fKE1uQ>mFYK6i2-v%VoB*$89QpWKR{-{C=Ig6ujcXnSk+MNZ>iB!{&wK#M^x8)#!UU?fTE$V3^Ts zl*6^a+=-ExpB*mZ_@9MLp!UUO%Jz|{1lo7*v*q!D)}je}Umr1o?>3e|un8&>gI>Gz zEw0X?lO)q}U80#5I6o|O2+2}SP0a)ro;eqX{A<9wNAiHBC64+L2-l@xI(z|hu!K_# zE-yNrNMW3>^RbeD+nTHtpG(;nIgLy6F8U?Ty>F_bG9S}+el^!dSRpGfFYa8b(^j7) zX}F&Uwy>~pdlrFP*w*Ip%2es4o+IESrlD@sQpQC}3;N%MZZq_=D>zPUuDHKC$n6LX{JK_~t{X3^s1b1D1xRi)1$*yqiOCFf0)JBYrlE>R1?lPin zCP(G$d`15*VA*cdRDyIc7W)mRm1`npQsSN%{-`hLMMTNM($ZibkiaXJWN~~AA7bJ0 z{&LaX)lo28Er*y|P@+jGBP^)l5Hfrb0ZZSj`+Die^AA!Khb8vb+N2}twi+x2da#mC z7`)nTwkAmVNw#RWTpb9Uo;KlUJ?-T3cW~5Y4EarmN5c0aYH6893{z7Gzc}Ulv8~a1 zFxc6}5VB%k9Zl<-W=fJYP%HmvsSgjo7SYh35^uIP^mz$pWsQJ@xP6K^PfEfxfm8IC z=oem7NF$b}MX6_lPk^U)mX@)ky1I38C$slz876$RA0Ec-I#Vecfn1j2JGN z&XV9seaOwp=}Y=*B>n#SXkfML`-}-g{PpeED|}YZ@h}4Za?R|YKmA;8*4l{B!{N7& z-w?M@dRX>GzJ#*u4Sv1=r;l*OeJ{pwV@Se`q|oU7LF;eI9`!wzh5_i|R2%};8N6>4 zOGbV%lziA)6U_`E>j*PcWVdz$bk zvxD=~^7w-X2a$j!`h-&!uMd>rOOQP8%EB+C&s!8!f$on=FvJY8)2nr#UQ3e8^p86~ zAd!3m2#%&%_-{ECKhDB?2ci{FHylrW_^howRWVG}Vro=-+5^Ck7eHXpVfRSxeN#Xx z44KFX0G1iXC6{)7EvH+TEW~ydz3gMIv&9)3KhrzvcIB}9-_m~95ISG)@_UYOQPvty zON&+KAdHP5PMVxX6Z?ENK%~#qZt=UmhK~tE8)tAU49olxHV0F+2JGF*db*M5tkHQN zk{KOGe}Ggv@P4AnRT9KECy|^R9Ha=d`06gS)acTYql{8=_$-Ln|K8pGal!LAtoRI* zAR;E~w~$6_&6jU?I58IBi}&4p|H97^ADc{Wc3tJAM5cR{HU~(^5ZZ1Eq*? z0>gplxdHmcCWm%Vttyu*xH65g4~|FSazcz)*b`Bh^< z(2>2N`FptMR!f(Hpp>G-(xw@y7NcyklXrY5=`kF&(B#4yxiRGxD*5GP|= zEXN~BQhI8@CXzfF9!Aa)@Vb%P5~L2vGh~;Gc08tV)5CTOh(sm4Q@RD9`_f_2r_|&Z z+)ds=1C43u={cD#?w8h5X(2%yPh*00>i=i~)Wcsn05bpRALz5;>W9zlr5Y`d z)!FzNYnl#QAyH9CUx;}mEE@ykZ}d-H%V|m?ZCClmp&W4~52ttso9u&| z&30Fp>oo=-2HHSK)ndCfVECBZCq!Dw^8LH}%OrGm$7;2HgLj;+ehdi|ya!lM6vu@Z z2eXyW{#D{Ldu=^iL(AE6ffw~=CZ(#|{!f>)#RKwL z3d-?TBt2R$;j{6_q@GV`rx!L@bSsF%^|UBlX8_!I9=jhN>j=3V2ikk+G^#m4W`P6vbdCkX={Bs>~wIyzQIh{ zyXuMWiGusi5CUz7R{zYcT9oAHlO;*0rlCCcj|uvuPG7V_X!Tf(j4x)s{QaU^Yu{!b zC;}_Zp2lXVYEp_KPl*3cbVmCcd;4VEEn+@vwcyp`S*q5Suuoc|z-7$W3U!&P0Af-R zYV~T4j_boz5Mz;lK;#phag}Vj(n7O0|G=&7&iJ-a2^8xd9&diCq2-%pWS=$Fyv?K|8dlJW2ks&%;`m2S6p z664|&MU|@Cvr~Ak6VB>>A)h`!y#PGlkS-JmT#OvEVDWbUCeI(gO=%O0B3Uk$-&|Mo zp02f6HM!hbQea&H+_SQ>=BjNbWUyoMx~*4P5D*YVVAm)qD4;^Udjoizt3~)tC`Cyh zi5$5vFZRs-HZnsTl-4MAL;R(ur$1WV9*Wf9qRkK!7vJi>0}J`5(m1yIxVk<@XSy*r zcXe=Prf2-tQ&E8oI$11e0FkJ;hAuCc5H7^nu)*tEPDaLy>5WiQQbV1cu$Y*>j)a7K zlb`cc(Hd2ng}+Iz9QNdmjL?@bJik1Ef^QnHmt{Ijk;AFu!8jRgFL7WP;41?3d4CiV z(!D0Xc9E}-=kK@E2W^J!3w-R#s>z^0VIsiIAlcSq>+kaNgsuDok2b{LHfJfXkHkhl z=ZmR8W8lDlgQA-#dCVlbe9nxGsL;J@^w%qX zsBO*G{#a%CIeY$qf5mKElIIH&QR?dI5>(8s*T1_38B&JDzBQK!DoT)^CKc01_<^@0qnU}61^=QdL43G6Y| zKbqh|&WKtl&*tan(MWh)4g+gy{GM+!zE5UqY5n#cz7KaN=5t;zV?C;a&_rNoS96sF zRui(avYxTmBOpflle-)pib=gk_{;hjTVdAb)b8qcdm5Y4DidjQR0TesZZzO0+2%y1 z934!fK=rY)PZ&b49Of5Q7#Z(z)oDPXocw*zuKak8Ono?89KLpRbOg>olxeW*ORxVUj2rNR8ulXpv1)|w6YpQUlIu{pb4w%I>s z`1C2Lsh4XssHys|+F!NcSU~8NLg9W9X16X`3H|PEOm5Qrs#-3KOJ?#>ctQB301O(2 zR=sSOQ=+j;UqBEBNBYY&RN-BH&yv)o&(6d6*+FJgMGA?i(`PfjrNPjiuD$}c3^=HL zy3uQHmkp2h(zCr=pZtXY4rY$B62^`7ng-!i`TP~k!|CdUe~QU!eu#^&Rl>hMV}K_1 zgup@h1>SNp7GytwlO?N0VQg*DJ@m`L$y{FDO=bH1NT=*eZKX zgHvhPoPpq5kXyH}lbWN)AmJ9ot_<&2%7*2%4W=l8@&o{M(t35qOw#0AQZ`v5S`$iz zkr`bvvBnP&0eY*RoMagLGC$YN5YygnzVTaZrO^9*GvwE=9`_rT9`><2$0Jx%G%PHY zs*C^=%DH}ebsPWX{*Is~%cWkahWdKYbin~Af$#4JzA|mXLJ3`7-~80HWXz02dUQJa zRzT$1J}-n_;AzV9;zCZAk4i8?OY{oMGWWoFdWaJVANl!2Z@D!~bXh?8xuoFXLE-5ajEw-Y%T5JLb;RGxGz7rmuKP~Dm%FHFs{93M2ii(>_%qz|uIDhcje7MaCN{Q4wGOT%f_Fb5 zkeCSI`gEEcX}MetZ|pQ#O8R}R0%t+wKI;J&6=yJ97BB!}W@TCfaj7~9g0r$YT%N;? z@PGgQ81;L+T*cxb&kFB)&0@~MU!Ep(<=$Lr1y_y-2N$}zfd};%>4(Y)im>ZvlOrUa zNI*!a1ZHvHls52svqK_vztE%*ymqzNz ziY0?}?pniTQuO_yb4E%O>frha`FD>WZWmYn4A?s`mkkycu3H60rX3wH8SG~^pSOEH zvnhAmLu*EF_zNh?gCx27DLS=A*wu%IMvEB`(xG8N@rQS-8c*i1)5P5d@==m9%wW5c z{u#-^W++tnrwi?43!Lh9G!pYW!dcq`dfcc?8Mz(SJx=dEp7>@mS#2Jq|A-_SQ%?%Y z*b(c)p@~w6N8?0YLt7u^4Nwmbz2id&d(;%LlJ$IHrE)o489qBWTR#%($5^seR<6&s zxf1AV&zo%uCB&a;CiKcfACAKNJ2;3lx0RnR>0Qf(1fW$Hq^yUQ_z(yu`N&X<%u^x> z2SJ)tot8H|gRgKQ6{V%MpOdGXRwR6xe|KkL3jqX}vgO-F#wfijk7pK!OBBFOg0bFs zQEtw}lBoa&aWbEeb=)gRi082R)27UsHa&4gA{i5d-y_~s;;EOAaWcI_^~5X0*boBX z6{$Zc{TC5YVh4xBjz+zVZSq$Bo2_o(U6Y6K3-Yc8 zy@uUn)eOj&#H@pWyZ6&8Q#K?P4AwF;SP3JC7DR=*q4$TN87C0jDFRTxcy31A`*DK! zSx?;O=wKRybsAUv;+SJN9(ZS{Dk z{xb-JK83*x3k&75Sln)eVQs3U34~Cgl=z;sxc$fGKQ-G->FEm;LA!6ZjlzF6E5{oH z%5M!|%;Ny{&!1*)=F9!h*WqhKX8)OpTsIAT{$HmUXL20`ocBfjI+$g?av#6lY5xGV zQj|Fljt}f3m5q$NfZ`MDx8XZas&1@KtFz6l|7KgPueW`FTi z8;-?IFf#^W-+!j+?y`~z0@$y&-J%MF=P5e#$D>`WEt6NPl$zh=)zfw<^9);~g|r%P z-&^Yc(0qHje1#dN!f7zc+wW6QM<>+n$`E&Hy@Iy-X=;(ZwMncc>j53|wQ6l_Y)ocx zxqA6`ee)M!*Fvev9f|3hl1gh@#Ir-kTLgle z4y7a_L3h!laEr)y`B3sf{kwGP6_?pn=&63hceR+-JkE3HH>aV$SZ6Iv`W;2eKmW8u z<1JS6+8;|+r-3IgZTXg_he-OaBI8aAYGcN`+UkGiuYU3>%9KQ?=K5Tm zPcdXVYz6Mu1JBLYs>}doTHeXHcOU1hz1Nrq-0nm z+-kjcHiFL;e2c)?U)bv0qgdAe{|KeytLe5@Ji|r4VVB5!`Cww=q0(6BJ>9s(Df_M=&4d#kHEOj$gxg(AyDv6maJ z9orEZuG#GwTI||9e(GuictH|Tv?Y1ZC&rVTHktjxfL?Q()n*Y-&vtpN4QLSBZo+4v zrY@;4`>o#2RtRtFmX!h6%TKoYmd*Qmy=fyEU|2xSkj2VROA79a%RqA9Y(CWPR1(AQ zbN_qWV>K3Mt+X!Ox=h(uz|PHXH7z~8&g5aZ7kG8LWVe0xZn~U;Jf+=qu~HS9P>(>@ zQmvnz9v@S>+)B%OeKRpW+39c*aaOjM?R8&gy##UA;5)KOd1p4_^D^2#H8$q!{&{&b z082TeS)Q~W=752ADY&||Y<5)aqNVlukr1$eV6I+D(Uu2e>V)HjOArTQ7ezVyS zc_}#8S_^sVM#ap?I{fBlXU9-^K8au^%ptLGNnJ+5?4IHec98Mc5ft9N+op>J=+v@p z@FNLfZ($?hw@5Pudv1FG2~T|X`@x9s4(eN*(NyP2qKZnw4kr>0pH7#o1%n;!>NfyE zO=UTc@3^e4Pf+1tuoHDGf2SL4aw}7}-4@&6P(reg`{#6>2y9_Wm)Dgq2*$S(Q<(iJ zHa?<8xzgpK_Izn`d6n1P9C9+d4bo|`LHR<=Vo6{`+qwi-=zRQ_aG8gN3@J0$$cT-N zo`H;v&9zwmA1K9=_0AHaF1zvh6rGinA`~;8buIElNePQ zHM7Rq(PBF}Hl0H7yfrOv%}==u0ER}AM=*<}L&h;+3+9CGONeTkOFQ0ThH`JbuJj)S zM|s_yZsk6Ty*WJg7oD6~zydElA9lHugf?K1xg9jW!Uv^(<9qk%nyR$cG?x5y^X)cg zb@*F(TuK9%R5& z)kM{2Z1@gBnv^CE<2|^hJg{HG_g9Pdl`mDX=}0UWvBO{OjapM; z-aoN4rRC+jL80TUBeCi=S`|Qlp%%XCtBqzY8EsfcROFY{ReGAyJrUo#cCWPp2`DKm_bYq5^y(LR4@qljX?5PiDz@zUH!(oHhutKSkq^|RfeiBl zHg>Hbsd+OIDZy(G0S`Wd(Xfc*rWF?p6C+21tC$Mg)XK~j;i83lWAXBGOXY^%>|ryO z$tp3>@Fli4uhmHRH1$MCjz^83A9 z1z4%?5>f|-nX$W^E&|RQfo~@m;x4CkthdV zw=i-mQa=9p&CaIStbyb}X3~$lO+xMMiAXOIE=STAod#z#Ymv@tQqk|&8NKU&sVHc^ zvr`@{Jyb|4&_+TdKoJPYwYEtNV@@>qf z1@TDpXJ$;n_MVpx*1PgE zOSK~UkysQ0?Zt;nZnkf)rC9yu?qj3lBGhzM!((a^Se zYH6ad?}XN$j8+=Y&z%7#l`1^}QJOUSoH^@O_)N_hs|*N$Z9a?5VjC=e-GPiILxgSl z(FQrj0-jcz<|_SYui=r`sh0~c?z&JYM957XC^9|N;%Ge1Hb

UOlId;OcA3)#7X^ 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 X3oV+_oE?1y9RZUPlNYTPHVFJ*<2Pfq literal 0 HcmV?d00001 diff --git a/docs/pr-screenshots/proxy-pin-credential-modal-rotate.png b/docs/pr-screenshots/proxy-pin-credential-modal-rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..f887ada9310ebcac705cdf022d2287adedfeaf6c GIT binary patch literal 56046 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%0A^3w0-c-3PvMoPn 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 literal 0 HcmV?d00001 diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index 9e2f2c856..9bbb85841 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -90,6 +90,9 @@ export type GeoTarget = { export type ProxyLocation = LegacyProxyLocation | GeoTarget | null; +export const PINNED_RESIDENTIAL_ISP_PROXY_LOCATION = + "RESIDENTIAL_ISP" satisfies LegacyProxyLocation; + export type ArtifactApiResponse = { created_at: string; modified_at: string; @@ -794,6 +797,8 @@ export type BrowserProfileApiResponse = { name: string; description: string | null; source_browser_type: string | null; + proxy_location?: ProxyLocation | null; + proxy_session_id?: string | null; created_at: string; modified_at: string; deleted_at: string | null; @@ -832,6 +837,8 @@ export type CredentialApiResponse = { user_context?: string | null; save_browser_session_intent?: boolean | null; folder_id?: string | null; + proxy_location?: ProxyLocation | null; + proxy_session_id?: string | null; }; export function isPasswordCredential( @@ -866,6 +873,9 @@ export type CreateCredentialRequest = { credential_type: "password" | "credit_card" | "secret"; credential: PasswordCredential | CreditCardCredential | SecretCredential; vault_type?: "custom"; + proxy_location?: ProxyLocation | null; + proxy_session_id?: string | null; + rotate_proxy_session_id?: boolean; }; export type PasswordCredential = { diff --git a/skyvern-frontend/src/routes/browserProfiles/BrowserProfileDetailPage.tsx b/skyvern-frontend/src/routes/browserProfiles/BrowserProfileDetailPage.tsx index d62ffc2ce..067b44641 100644 --- a/skyvern-frontend/src/routes/browserProfiles/BrowserProfileDetailPage.tsx +++ b/skyvern-frontend/src/routes/browserProfiles/BrowserProfileDetailPage.tsx @@ -1,8 +1,16 @@ -import { useState } from "react"; -import { ChevronLeftIcon, Pencil1Icon } from "@radix-ui/react-icons"; +import { useEffect, useState } from "react"; +import { + ChevronLeftIcon, + Pencil1Icon, + ReloadIcon, +} from "@radix-ui/react-icons"; import { useNavigate, useParams } from "react-router-dom"; +import { PINNED_RESIDENTIAL_ISP_PROXY_LOCATION } from "@/api/types"; +import { HelpTooltip } from "@/components/HelpTooltip"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { CopyText } from "@/routes/workflows/editor/Workspace"; import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat"; @@ -10,11 +18,22 @@ import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat"; import { DeleteBrowserProfileButton } from "./DeleteBrowserProfileButton"; import { RenameBrowserProfileDialog } from "./RenameBrowserProfileDialog"; import { useBrowserProfileQuery } from "./hooks/useBrowserProfileQuery"; +import { useUpdateBrowserProfileMutation } from "./hooks/useBrowserProfileMutations"; + +function formatProxyIdentity(value?: string | null) { + if (!value) { + return null; + } + return `${value.slice(0, 3)}...${value.slice(-2)}`; +} function BrowserProfileDetailPage() { const navigate = useNavigate(); const { profileId } = useParams<{ profileId: string }>(); const [renameOpen, setRenameOpen] = useState(false); + const [pinResidentialIspProxy, setPinResidentialIspProxy] = useState(false); + const [rotateProxyPin, setRotateProxyPin] = useState(false); + const updateProfileMutation = useUpdateBrowserProfileMutation(); const { data: profile, @@ -22,6 +41,37 @@ function BrowserProfileDetailPage() { isError, } = useBrowserProfileQuery(profileId); + useEffect(() => { + if (!profile) { + return; + } + setPinResidentialIspProxy(Boolean(profile.proxy_session_id)); + setRotateProxyPin(false); + }, [profile]); + + const existingProxyPinEnabled = Boolean(profile?.proxy_session_id); + const existingProxyIdentity = formatProxyIdentity(profile?.proxy_session_id); + const proxyPinChanged = Boolean( + profile && + (pinResidentialIspProxy !== existingProxyPinEnabled || + (pinResidentialIspProxy && rotateProxyPin)), + ); + + const handleSaveProxyPin = () => { + if (!profile || !proxyPinChanged) { + return; + } + updateProfileMutation.mutate({ + profileId: profile.browser_profile_id, + proxy_location: pinResidentialIspProxy + ? PINNED_RESIDENTIAL_ISP_PROXY_LOCATION + : null, + proxy_session_id: pinResidentialIspProxy ? undefined : null, + rotate_proxy_session_id: + pinResidentialIspProxy && rotateProxyPin ? true : undefined, + }); + }; + return (

@@ -124,6 +174,91 @@ function BrowserProfileDetailPage() { {basicLocalTimeFormat(profile.modified_at)} + +
+
+ + setPinResidentialIspProxy(checked === true) + } + disabled={Boolean(profile.deleted_at)} + className="mt-0.5" + /> +
+
+ + +
+

+ Helps profiles keep the same residential IP across launches, + so saved logins are less likely to be challenged. +

+ {pinResidentialIspProxy && + existingProxyIdentity && + profile.proxy_session_id && ( +
+

+ + Consistent IP active: identity{" "} + {existingProxyIdentity} + + +

+
+ + {rotateProxyPin && ( + + A new IP identity will be created when you save. + + )} +
+
+ )} + {pinResidentialIspProxy && !existingProxyIdentity && ( +

+ Skyvern will create an IP identity for this profile when + you save. +

+ )} +
+
+
+ +
+
state.active !== null, @@ -21,30 +35,83 @@ function CreateBrowserProfileButton({ const disabled = createBrowserSessionMutation.isPending || isBackgroundCreateInProgress; + const handleCreate = () => { + createBrowserSessionMutation.mutate({ + proxyLocation: pinResidentialIspProxy + ? PINNED_RESIDENTIAL_ISP_PROXY_LOCATION + : null, + proxySessionId: null, + timeout: null, + generateBrowserProfile: true, + }); + setOpen(false); + }; + return ( - + <> + + + event.preventDefault()}> + + Create Browser Profile + +
+
+ + setPinResidentialIspProxy(checked === true) + } + /> +
+
+ + +
+

+ Skyvern will create a residential IP identity for this + profile. +

+
+
+
+ + + + +
+
+ ); } diff --git a/skyvern-frontend/src/routes/browserProfiles/hooks/useBrowserProfileMutations.test.tsx b/skyvern-frontend/src/routes/browserProfiles/hooks/useBrowserProfileMutations.test.tsx new file mode 100644 index 000000000..51e1b92d4 --- /dev/null +++ b/skyvern-frontend/src/routes/browserProfiles/hooks/useBrowserProfileMutations.test.tsx @@ -0,0 +1,103 @@ +// @vitest-environment jsdom + +import type { ReactNode } from "react"; +import { act, renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { PINNED_RESIDENTIAL_ISP_PROXY_LOCATION } from "@/api/types"; + +const { mockPatch } = vi.hoisted(() => ({ mockPatch: vi.fn() })); + +vi.mock("@/api/AxiosClient", () => ({ + getClient: () => Promise.resolve({ patch: mockPatch }), +})); + +vi.mock("@/hooks/useCredentialGetter", () => ({ + useCredentialGetter: () => () => Promise.resolve("test-token"), +})); + +vi.mock("@/components/ui/use-toast", () => ({ toast: vi.fn() })); + +import { useUpdateBrowserProfileMutation } from "./useBrowserProfileMutations"; + +function wrapper({ children }: { children: ReactNode }) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + return ( + {children} + ); +} + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("useUpdateBrowserProfileMutation proxy pin payloads", () => { + it("omits proxy_session_id when keeping the existing consistent IP identity", async () => { + mockPatch.mockResolvedValue({ data: {} }); + const { result } = renderHook(() => useUpdateBrowserProfileMutation(), { + wrapper, + }); + + act(() => { + result.current.mutate({ + profileId: "bp_123", + proxy_location: PINNED_RESIDENTIAL_ISP_PROXY_LOCATION, + proxy_session_id: undefined, + }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(mockPatch).toHaveBeenCalledWith("/browser_profiles/bp_123", { + proxy_location: PINNED_RESIDENTIAL_ISP_PROXY_LOCATION, + }); + }); + + it("sends rotate_proxy_session_id when rotating the consistent IP identity", async () => { + mockPatch.mockResolvedValue({ data: {} }); + const { result } = renderHook(() => useUpdateBrowserProfileMutation(), { + wrapper, + }); + + act(() => { + result.current.mutate({ + profileId: "bp_123", + proxy_location: PINNED_RESIDENTIAL_ISP_PROXY_LOCATION, + rotate_proxy_session_id: true, + }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(mockPatch).toHaveBeenCalledWith("/browser_profiles/bp_123", { + proxy_location: PINNED_RESIDENTIAL_ISP_PROXY_LOCATION, + rotate_proxy_session_id: true, + }); + }); + + it("sends null proxy fields when disabling consistent IP", async () => { + mockPatch.mockResolvedValue({ data: {} }); + const { result } = renderHook(() => useUpdateBrowserProfileMutation(), { + wrapper, + }); + + act(() => { + result.current.mutate({ + profileId: "bp_123", + proxy_location: null, + proxy_session_id: null, + }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(mockPatch).toHaveBeenCalledWith("/browser_profiles/bp_123", { + proxy_location: null, + proxy_session_id: null, + }); + }); +}); diff --git a/skyvern-frontend/src/routes/browserProfiles/hooks/useBrowserProfileMutations.ts b/skyvern-frontend/src/routes/browserProfiles/hooks/useBrowserProfileMutations.ts index f92758480..a678a0900 100644 --- a/skyvern-frontend/src/routes/browserProfiles/hooks/useBrowserProfileMutations.ts +++ b/skyvern-frontend/src/routes/browserProfiles/hooks/useBrowserProfileMutations.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { getClient } from "@/api/AxiosClient"; -import { BrowserProfileApiResponse } from "@/api/types"; +import { BrowserProfileApiResponse, ProxyLocation } from "@/api/types"; import { toast } from "@/components/ui/use-toast"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; @@ -10,6 +10,9 @@ type UpdateBrowserProfileInput = { profileId: string; name?: string; description?: string | null; + proxy_location?: ProxyLocation | null; + proxy_session_id?: string | null; + rotate_proxy_session_id?: boolean; }; function useUpdateBrowserProfileMutation() { @@ -21,15 +24,28 @@ function useUpdateBrowserProfileMutation() { profileId, name, description, + proxy_location, + proxy_session_id, + rotate_proxy_session_id, }: UpdateBrowserProfileInput) => { const client = await getClient(credentialGetter, "sans-api-v1"); - const payload: Record = {}; + const payload: Record = + {}; if (name !== undefined) { payload.name = name; } if (description !== undefined) { payload.description = description; } + if (proxy_location !== undefined) { + payload.proxy_location = proxy_location; + } + if (proxy_session_id !== undefined) { + payload.proxy_session_id = proxy_session_id; + } + if (rotate_proxy_session_id !== undefined) { + payload.rotate_proxy_session_id = rotate_proxy_session_id; + } return client .patch( `/browser_profiles/${profileId}`, diff --git a/skyvern-frontend/src/routes/browserSessions/hooks/useCreateBrowserSessionMutation.test.tsx b/skyvern-frontend/src/routes/browserSessions/hooks/useCreateBrowserSessionMutation.test.tsx new file mode 100644 index 000000000..8c37ebc09 --- /dev/null +++ b/skyvern-frontend/src/routes/browserSessions/hooks/useCreateBrowserSessionMutation.test.tsx @@ -0,0 +1,87 @@ +// @vitest-environment jsdom + +import type { ReactNode } from "react"; +import { act, renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { MemoryRouter } from "react-router-dom"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { PINNED_RESIDENTIAL_ISP_PROXY_LOCATION } from "@/api/types"; + +const { mockNavigate, mockPost } = vi.hoisted(() => ({ + mockNavigate: vi.fn(), + mockPost: vi.fn(), +})); + +vi.mock("@/api/AxiosClient", () => ({ + getClient: () => Promise.resolve({ post: mockPost }), +})); + +vi.mock("@/hooks/useCredentialGetter", () => ({ + useCredentialGetter: () => () => Promise.resolve("test-token"), +})); + +vi.mock("@/components/ui/use-toast", () => ({ toast: vi.fn() })); + +vi.mock("react-router-dom", async () => { + const actual = + await vi.importActual( + "react-router-dom", + ); + return { + ...actual, + useNavigate: () => mockNavigate, + }; +}); + +import { useCreateBrowserSessionMutation } from "./useCreateBrowserSessionMutation"; + +function wrapper({ children }: { children: ReactNode }) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + return ( + + {children} + + ); +} + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("useCreateBrowserSessionMutation proxy pin payloads", () => { + it("sends null proxy_session_id when creating a pinned profile", async () => { + mockPost.mockResolvedValue({ + data: { browser_session_id: "pbs_123" }, + }); + const { result } = renderHook(() => useCreateBrowserSessionMutation(), { + wrapper, + }); + + act(() => { + result.current.mutate({ + proxyLocation: PINNED_RESIDENTIAL_ISP_PROXY_LOCATION, + proxySessionId: null, + timeout: null, + generateBrowserProfile: true, + }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(mockPost).toHaveBeenCalledWith("/browser_sessions", { + proxy_location: PINNED_RESIDENTIAL_ISP_PROXY_LOCATION, + proxy_session_id: null, + timeout: null, + extensions: [], + browser_type: null, + generate_browser_profile: true, + }); + expect(mockNavigate).toHaveBeenCalledWith("/browser-session/pbs_123"); + }); +}); diff --git a/skyvern-frontend/src/routes/browserSessions/hooks/useCreateBrowserSessionMutation.ts b/skyvern-frontend/src/routes/browserSessions/hooks/useCreateBrowserSessionMutation.ts index dc1526ee6..5027dd985 100644 --- a/skyvern-frontend/src/routes/browserSessions/hooks/useCreateBrowserSessionMutation.ts +++ b/skyvern-frontend/src/routes/browserSessions/hooks/useCreateBrowserSessionMutation.ts @@ -19,12 +19,14 @@ function useCreateBrowserSessionMutation() { return useMutation({ mutationFn: async ({ proxyLocation = null, + proxySessionId = null, timeout = null, extensions = [], browserType = null, generateBrowserProfile = false, }: { proxyLocation: ProxyLocation | null; + proxySessionId?: string | null; timeout: number | null; extensions?: BrowserSessionExtension[]; browserType?: BrowserSessionType | null; @@ -35,6 +37,7 @@ function useCreateBrowserSessionMutation() { "/browser_sessions", { proxy_location: proxyLocation, + proxy_session_id: proxySessionId, timeout, extensions, browser_type: browserType, diff --git a/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx b/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx index 633a1cedd..0fb5c572c 100644 --- a/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx +++ b/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx @@ -24,11 +24,13 @@ import { type CreditCardBillingAddress, type CreditCardCredential, type CredentialApiResponse, + PINNED_RESIDENTIAL_ISP_PROXY_LOCATION, isPasswordCredential, isCreditCardCredential, isSecretCredential, type TestCredentialStatusResponse, type TestLoginResponse, + type ProxyLocation, } from "@/api/types"; import { getClient } from "@/api/AxiosClient"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; @@ -191,6 +193,19 @@ type Props = { ) => void; }; +type ProxyPinPayload = { + proxy_location: ProxyLocation | null; + proxy_session_id?: string | null; + rotate_proxy_session_id?: boolean; +}; + +function formatProxyIdentity(value?: string | null) { + if (!value) { + return null; + } + return `${value.slice(0, 3)}...${value.slice(-2)}`; +} + function CredentialsModal({ onCredentialCreated, isOpen: controlledIsOpen, @@ -246,6 +261,11 @@ function CredentialsModal({ const [testAndSave, setTestAndSave] = useState(false); const [testUrl, setTestUrl] = useState(""); const [userContext, setUserContext] = useState(""); + const [pinResidentialIspProxy, setPinResidentialIspProxy] = useState(false); + const [rotateProxyPin, setRotateProxyPin] = useState(false); + const existingProxyIdentity = formatProxyIdentity( + editingCredential?.proxy_session_id, + ); const [testStatus, setTestStatus] = useState< "idle" | "testing" | "completed" | "failed" | "profile_failed" >("idle"); @@ -269,6 +289,30 @@ function CredentialsModal({ // Guards against in-flight poll responses updating state after cancel/close const pollCancelledRef = useRef(false); + const getProxyPinPayload = useCallback((): ProxyPinPayload => { + if (!pinResidentialIspProxy) { + return { + proxy_location: null, + proxy_session_id: null, + }; + } + return { + proxy_location: PINNED_RESIDENTIAL_ISP_PROXY_LOCATION, + ...(rotateProxyPin ? { rotate_proxy_session_id: true } : {}), + }; + }, [pinResidentialIspProxy, rotateProxyPin]); + + const hasProxyPinChanges = useCallback(() => { + if (!editingCredential) { + return pinResidentialIspProxy; + } + const existingPinEnabled = Boolean(editingCredential.proxy_session_id); + return ( + pinResidentialIspProxy !== existingPinEnabled || + (pinResidentialIspProxy && rotateProxyPin) + ); + }, [editingCredential, pinResidentialIspProxy, rotateProxyPin]); + // Captures save intent before mutation fires — testAndSave/testUrl may be // reset by the time onSuccess runs, so we snapshot them here. const saveIntentRef = useRef<{ @@ -277,12 +321,18 @@ function CredentialsModal({ testUrl: string; userContext: string; name: string; + proxyLocation: ProxyLocation | null; + proxySessionId?: string | null; + proxyPinChanged: boolean; }>({ shouldTestAfterSave: false, saveBrowserSessionIntent: false, testUrl: "", userContext: "", name: "", + proxyLocation: null, + proxySessionId: null, + proxyPinChanged: false, }); // Cleanup polling on unmount @@ -319,6 +369,8 @@ function CredentialsModal({ passwordCredentialValues.totp_type, passwordCredentialValues.totp_identifier, testUrl, + pinResidentialIspProxy, + rotateProxyPin, ]); const formInitializedRef = useRef(false); @@ -352,6 +404,8 @@ function CredentialsModal({ if (editingCredential.user_context) { setUserContext(editingCredential.user_context); } + setPinResidentialIspProxy(Boolean(editingCredential.proxy_session_id)); + setRotateProxyPin(false); if (isPasswordCredential(cred)) { setPasswordCredentialValues({ name: editingCredential.name, @@ -419,12 +473,17 @@ function CredentialsModal({ pollErrorCountRef.current = 0; pollCancelledRef.current = false; setUserContext(""); + setPinResidentialIspProxy(false); + setRotateProxyPin(false); saveIntentRef.current = { shouldTestAfterSave: false, saveBrowserSessionIntent: false, testUrl: "", userContext: "", name: "", + proxyLocation: null, + proxySessionId: null, + proxyPinChanged: false, }; if (pollIntervalRef.current) { clearTimeout(pollIntervalRef.current); @@ -561,6 +620,7 @@ function CredentialsModal({ const startTest = useCallback(async () => { try { const client = await getClient(credentialGetter, "sans-api-v1"); + const proxyPinPayload = getProxyPinPayload(); const response = await client.post( `/credentials/test-login`, { @@ -572,6 +632,7 @@ function CredentialsModal({ totp_identifier: passwordCredentialValues.totp_identifier.trim() || null, user_context: userContext.trim() || null, + ...proxyPinPayload, }, ); const data = response.data; @@ -619,6 +680,7 @@ function CredentialsModal({ passwordCredentialValues, userContext, pollTestStatus, + getProxyPinPayload, ]); const createCredentialMutation = useMutation({ @@ -783,15 +845,23 @@ function CredentialsModal({ tested_url, user_context, save_browser_session_intent, + proxy_location, + proxy_session_id, + rotate_proxy_session_id, }: { id: string; name: string; tested_url?: string; user_context?: string | null; save_browser_session_intent?: boolean; + proxy_location?: ProxyLocation | null; + proxy_session_id?: string | null; + rotate_proxy_session_id?: boolean; }) => { const client = await getClient(credentialGetter, "sans-api-v1"); - const body: Record = { name }; + const body: Record = { + name, + }; if (tested_url) { body.tested_url = tested_url; } @@ -801,6 +871,15 @@ function CredentialsModal({ if (save_browser_session_intent !== undefined) { body.save_browser_session_intent = save_browser_session_intent; } + if (proxy_location !== undefined) { + body.proxy_location = proxy_location; + } + if (proxy_session_id !== undefined) { + body.proxy_session_id = proxy_session_id; + } + if (rotate_proxy_session_id !== undefined) { + body.rotate_proxy_session_id = rotate_proxy_session_id; + } const response = await client.patch( `/credentials/${id}`, body, @@ -834,20 +913,6 @@ function CredentialsModal({ ? updateCredentialMutation : createCredentialMutation; - const handleRenameOnly = (name: string) => { - if (!editingCredential) return; - // Skip the API call if the name hasn't actually changed - if (name === editingCredential.name) { - reset(); - setIsOpen(false); - return; - } - renameCredentialMutation.mutate({ - id: editingCredential.credential_id, - name, - }); - }; - const handleSave = () => { const name = type === CredentialModalTypes.PASSWORD @@ -864,16 +929,25 @@ function CredentialsModal({ return; } + const proxyPinPayload = getProxyPinPayload(); + const proxyPinChanged = hasProxyPinChanges(); + const credentialSaveProxyPinPayload = + !isEditMode || proxyPinChanged ? proxyPinPayload : {}; + // In edit mode, use editingGroups to determine what changed (type-agnostic) if (isEditMode && editingCredential) { - if (!editingGroups.name && !editingGroups.values) { + if (!editingGroups.name && !editingGroups.values && !proxyPinChanged) { // Nothing was edited — close silently reset(); setIsOpen(false); return; } - if (editingGroups.name && !editingGroups.values) { - handleRenameOnly(name); + if (!editingGroups.values) { + renameCredentialMutation.mutate({ + id: editingCredential.credential_id, + name, + ...(proxyPinChanged ? proxyPinPayload : {}), + }); return; } } @@ -906,6 +980,7 @@ function CredentialsModal({ tested_url: url || undefined, user_context: ctx || null, save_browser_session_intent: true, + ...proxyPinPayload, }); return; } @@ -928,6 +1003,9 @@ function CredentialsModal({ testUrl: testUrl.trim(), userContext: userContext.trim(), name, + proxyLocation: proxyPinPayload.proxy_location, + proxySessionId: proxyPinPayload.proxy_session_id, + proxyPinChanged, }; activeMutation.mutate({ @@ -941,6 +1019,7 @@ function CredentialsModal({ totp_identifier: totpIdentifier === "" ? null : totpIdentifier, }, ...(vaultType === "custom" ? { vault_type: "custom" } : {}), + ...credentialSaveProxyPinPayload, }); } else if (type === CredentialModalTypes.CREDIT_CARD) { const cardNumber = creditCardCredentialValues.cardNumber.trim(); @@ -1016,11 +1095,22 @@ function CredentialsModal({ ...(billingPhone ? { billing_phone: billingPhone } : {}), ...(metadata ? { metadata } : {}), }; + saveIntentRef.current = { + shouldTestAfterSave: false, + saveBrowserSessionIntent: false, + testUrl: "", + userContext: "", + name, + proxyLocation: proxyPinPayload.proxy_location, + proxySessionId: proxyPinPayload.proxy_session_id, + proxyPinChanged, + }; activeMutation.mutate({ name, credential_type: "credit_card", credential: credentialPayload, ...(vaultType === "custom" ? { vault_type: "custom" } : {}), + ...credentialSaveProxyPinPayload, }); } else if (type === CredentialModalTypes.SECRET) { const secretValue = secretCredentialValues.secretValue.trim(); @@ -1035,6 +1125,16 @@ function CredentialsModal({ return; } + saveIntentRef.current = { + shouldTestAfterSave: false, + saveBrowserSessionIntent: false, + testUrl: "", + userContext: "", + name, + proxyLocation: proxyPinPayload.proxy_location, + proxySessionId: proxyPinPayload.proxy_session_id, + proxyPinChanged, + }; activeMutation.mutate({ name, credential_type: "secret", @@ -1043,6 +1143,7 @@ function CredentialsModal({ secret_label: secretLabel === "" ? null : secretLabel, }, ...(vaultType === "custom" ? { vault_type: "custom" } : {}), + ...credentialSaveProxyPinPayload, }); } }; @@ -1097,6 +1198,66 @@ function CredentialsModal({ ? getAuthenticatorKeyError(passwordCredentialValues) : null; + const proxyPinContent = ( +
+
+ + setPinResidentialIspProxy(checked === true) + } + disabled={isTestInProgress} + className="mt-0.5" + /> +
+
+ + +
+

+ Helps avoid extra 2FA, captchas, or temporary locks caused by + signing in from a new location. +

+ {pinResidentialIspProxy && existingProxyIdentity && ( +
+

+ Consistent IP active: identity {existingProxyIdentity} +

+
+ + {rotateProxyPin && ( + + A new IP identity will be created when you save. + + )} +
+
+ )} + {pinResidentialIspProxy && !existingProxyIdentity && ( +

+ Skyvern will create an IP identity for this credential when you + save. +

+ )} +
+
+
+ ); + const credentialContent = (() => { if (type === CredentialModalTypes.PASSWORD) { return ( @@ -1386,6 +1547,7 @@ function CredentialsModal({ )} {credentialContent} + {proxyPinContent}
diff --git a/skyvern/client/client.py b/skyvern/client/client.py index e895338ae..0a6c887d8 100644 --- a/skyvern/client/client.py +++ b/skyvern/client/client.py @@ -1620,6 +1620,8 @@ class Skyvern: description: typing.Optional[str] = OMIT, browser_session_id: typing.Optional[str] = OMIT, workflow_run_id: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> BrowserProfile: """ @@ -1663,6 +1665,8 @@ class Skyvern: description=description, browser_session_id=browser_session_id, workflow_run_id=workflow_run_id, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, request_options=request_options, ) return _response.data @@ -1738,6 +1742,9 @@ class Skyvern: *, name: typing.Optional[str] = OMIT, description: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> BrowserProfile: """ @@ -1754,6 +1761,9 @@ class Skyvern: description : typing.Optional[str] New description for the browser profile + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this browser profile. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1774,7 +1784,13 @@ class Skyvern: ) """ _response = self._raw_client.update_browser_profile( - profile_id, name=name, description=description, request_options=request_options + profile_id, + name=name, + description=description, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + rotate_proxy_session_id=rotate_proxy_session_id, + request_options=request_options, ) return _response.data @@ -1811,6 +1827,7 @@ class Skyvern: *, timeout: typing.Optional[int] = OMIT, proxy_location: typing.Optional[CreateBrowserSessionRequestProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, extensions: typing.Optional[typing.Sequence[Extensions]] = OMIT, browser_type: typing.Optional[PersistentBrowserType] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, @@ -1861,6 +1878,9 @@ class Skyvern: For self-hosted deployments, you can pass a custom proxy URL as a dict: {"url": "http://user:password@proxy.example.com:8080"}. This routes the browser through your own proxy server and takes precedence over any globally configured proxy pool. Can also be a GeoTarget object for granular city/state targeting: {"country": "US", "subdivision": "CA", "city": "San Francisco"}, or a custom proxy URL dict for self-hosted deployments: {"url": "http://user:password@proxy.example.com:8080"} + proxy_session_id : typing.Optional[str] + Opaque Skyvern-managed proxy sticky-session id for pinned Residential ISP sessions. + extensions : typing.Optional[typing.Sequence[Extensions]] A list of extensions to install in the browser session. @@ -1890,6 +1910,7 @@ class Skyvern: _response = self._raw_client.create_browser_session( timeout=timeout, proxy_location=proxy_location, + proxy_session_id=proxy_session_id, extensions=extensions, browser_type=browser_type, browser_profile_id=browser_profile_id, @@ -2093,6 +2114,9 @@ class Skyvern: credential_type: SkyvernForgeSdkSchemasCredentialsCredentialType, credential: CreateCredentialRequestCredential, vault_type: typing.Optional[CredentialVaultType] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> CredentialResponse: """ @@ -2112,6 +2136,9 @@ class Skyvern: vault_type : typing.Optional[CredentialVaultType] Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization. + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this credential. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -2141,6 +2168,9 @@ class Skyvern: credential_type=credential_type, credential=credential, vault_type=vault_type, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + rotate_proxy_session_id=rotate_proxy_session_id, request_options=request_options, ) return _response.data @@ -2153,6 +2183,9 @@ class Skyvern: credential_type: SkyvernForgeSdkSchemasCredentialsCredentialType, credential: CreateCredentialRequestCredential, vault_type: typing.Optional[CredentialVaultType] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> CredentialResponse: """ @@ -2175,6 +2208,9 @@ class Skyvern: vault_type : typing.Optional[CredentialVaultType] Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization. + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this credential. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -2206,6 +2242,9 @@ class Skyvern: credential_type=credential_type, credential=credential, vault_type=vault_type, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + rotate_proxy_session_id=rotate_proxy_session_id, request_options=request_options, ) return _response.data @@ -4568,6 +4607,8 @@ class AsyncSkyvern: description: typing.Optional[str] = OMIT, browser_session_id: typing.Optional[str] = OMIT, workflow_run_id: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> BrowserProfile: """ @@ -4619,6 +4660,8 @@ class AsyncSkyvern: description=description, browser_session_id=browser_session_id, workflow_run_id=workflow_run_id, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, request_options=request_options, ) return _response.data @@ -4710,6 +4753,9 @@ class AsyncSkyvern: *, name: typing.Optional[str] = OMIT, description: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> BrowserProfile: """ @@ -4726,6 +4772,9 @@ class AsyncSkyvern: description : typing.Optional[str] New description for the browser profile + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this browser profile. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -4754,7 +4803,13 @@ class AsyncSkyvern: asyncio.run(main()) """ _response = await self._raw_client.update_browser_profile( - profile_id, name=name, description=description, request_options=request_options + profile_id, + name=name, + description=description, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + rotate_proxy_session_id=rotate_proxy_session_id, + request_options=request_options, ) return _response.data @@ -4799,6 +4854,7 @@ class AsyncSkyvern: *, timeout: typing.Optional[int] = OMIT, proxy_location: typing.Optional[CreateBrowserSessionRequestProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, extensions: typing.Optional[typing.Sequence[Extensions]] = OMIT, browser_type: typing.Optional[PersistentBrowserType] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, @@ -4849,6 +4905,9 @@ class AsyncSkyvern: For self-hosted deployments, you can pass a custom proxy URL as a dict: {"url": "http://user:password@proxy.example.com:8080"}. This routes the browser through your own proxy server and takes precedence over any globally configured proxy pool. Can also be a GeoTarget object for granular city/state targeting: {"country": "US", "subdivision": "CA", "city": "San Francisco"}, or a custom proxy URL dict for self-hosted deployments: {"url": "http://user:password@proxy.example.com:8080"} + proxy_session_id : typing.Optional[str] + Opaque Skyvern-managed proxy sticky-session id for pinned Residential ISP sessions. + extensions : typing.Optional[typing.Sequence[Extensions]] A list of extensions to install in the browser session. @@ -4886,6 +4945,7 @@ class AsyncSkyvern: _response = await self._raw_client.create_browser_session( timeout=timeout, proxy_location=proxy_location, + proxy_session_id=proxy_session_id, extensions=extensions, browser_type=browser_type, browser_profile_id=browser_profile_id, @@ -5121,6 +5181,9 @@ class AsyncSkyvern: credential_type: SkyvernForgeSdkSchemasCredentialsCredentialType, credential: CreateCredentialRequestCredential, vault_type: typing.Optional[CredentialVaultType] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> CredentialResponse: """ @@ -5140,6 +5203,9 @@ class AsyncSkyvern: vault_type : typing.Optional[CredentialVaultType] Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization. + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this credential. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -5177,6 +5243,9 @@ class AsyncSkyvern: credential_type=credential_type, credential=credential, vault_type=vault_type, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + rotate_proxy_session_id=rotate_proxy_session_id, request_options=request_options, ) return _response.data @@ -5189,6 +5258,9 @@ class AsyncSkyvern: credential_type: SkyvernForgeSdkSchemasCredentialsCredentialType, credential: CreateCredentialRequestCredential, vault_type: typing.Optional[CredentialVaultType] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> CredentialResponse: """ @@ -5211,6 +5283,9 @@ class AsyncSkyvern: vault_type : typing.Optional[CredentialVaultType] Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization. + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this credential. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -5250,6 +5325,9 @@ class AsyncSkyvern: credential_type=credential_type, credential=credential, vault_type=vault_type, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + rotate_proxy_session_id=rotate_proxy_session_id, request_options=request_options, ) return _response.data diff --git a/skyvern/client/raw_client.py b/skyvern/client/raw_client.py index 1b7aed09c..a60a95ed6 100644 --- a/skyvern/client/raw_client.py +++ b/skyvern/client/raw_client.py @@ -2181,6 +2181,8 @@ class RawSkyvern: description: typing.Optional[str] = OMIT, browser_session_id: typing.Optional[str] = OMIT, workflow_run_id: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[BrowserProfile]: """ @@ -2200,6 +2202,12 @@ class RawSkyvern: workflow_run_id : typing.Optional[str] Workflow run whose persisted session should be captured. Omit for a blank profile. + proxy_location : typing.Optional[ProxyLocation] + Optional proxy location for this browser profile's pinned proxy identity. + + proxy_session_id : typing.Optional[str] + Optional advanced reuse key for this browser profile's pinned proxy identity. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -2216,6 +2224,8 @@ class RawSkyvern: "description": description, "browser_session_id": browser_session_id, "workflow_run_id": workflow_run_id, + "proxy_location": proxy_location, + "proxy_session_id": proxy_session_id, }, headers={ "content-type": "application/json", @@ -2391,6 +2401,9 @@ class RawSkyvern: *, name: typing.Optional[str] = OMIT, description: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[BrowserProfile]: """ @@ -2407,6 +2420,15 @@ class RawSkyvern: description : typing.Optional[str] New description for the browser profile + proxy_location : typing.Optional[ProxyLocation] + Optional proxy location for this browser profile's pinned proxy identity. + + proxy_session_id : typing.Optional[str] + Optional advanced reuse key for this browser profile's pinned proxy identity. + + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this browser profile. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -2421,6 +2443,9 @@ class RawSkyvern: json={ "name": name, "description": description, + "proxy_location": proxy_location, + "proxy_session_id": proxy_session_id, + "rotate_proxy_session_id": rotate_proxy_session_id, }, headers={ "content-type": "application/json", @@ -2539,6 +2564,7 @@ class RawSkyvern: *, timeout: typing.Optional[int] = OMIT, proxy_location: typing.Optional[CreateBrowserSessionRequestProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, extensions: typing.Optional[typing.Sequence[Extensions]] = OMIT, browser_type: typing.Optional[PersistentBrowserType] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, @@ -2589,6 +2615,9 @@ class RawSkyvern: For self-hosted deployments, you can pass a custom proxy URL as a dict: {"url": "http://user:password@proxy.example.com:8080"}. This routes the browser through your own proxy server and takes precedence over any globally configured proxy pool. Can also be a GeoTarget object for granular city/state targeting: {"country": "US", "subdivision": "CA", "city": "San Francisco"}, or a custom proxy URL dict for self-hosted deployments: {"url": "http://user:password@proxy.example.com:8080"} + proxy_session_id : typing.Optional[str] + Opaque Skyvern-managed proxy sticky-session id for pinned Residential ISP sessions. + extensions : typing.Optional[typing.Sequence[Extensions]] A list of extensions to install in the browser session. @@ -2614,6 +2643,7 @@ class RawSkyvern: "proxy_location": convert_and_respect_annotation_metadata( object_=proxy_location, annotation=CreateBrowserSessionRequestProxyLocation, direction="write" ), + "proxy_session_id": proxy_session_id, "extensions": extensions, "browser_type": browser_type, "browser_profile_id": browser_profile_id, @@ -2975,6 +3005,9 @@ class RawSkyvern: credential_type: SkyvernForgeSdkSchemasCredentialsCredentialType, credential: CreateCredentialRequestCredential, vault_type: typing.Optional[CredentialVaultType] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[CredentialResponse]: """ @@ -2994,6 +3027,15 @@ class RawSkyvern: vault_type : typing.Optional[CredentialVaultType] Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization. + proxy_location : typing.Optional[ProxyLocation] + Optional proxy location for this credential's pinned proxy identity. + + proxy_session_id : typing.Optional[str] + Optional advanced reuse key for this credential's pinned proxy identity. + + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this credential. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -3012,6 +3054,9 @@ class RawSkyvern: object_=credential, annotation=CreateCredentialRequestCredential, direction="write" ), "vault_type": vault_type, + "proxy_location": proxy_location, + "proxy_session_id": proxy_session_id, + "rotate_proxy_session_id": rotate_proxy_session_id, }, headers={ "content-type": "application/json", @@ -3053,6 +3098,9 @@ class RawSkyvern: credential_type: SkyvernForgeSdkSchemasCredentialsCredentialType, credential: CreateCredentialRequestCredential, vault_type: typing.Optional[CredentialVaultType] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[CredentialResponse]: """ @@ -3075,6 +3123,15 @@ class RawSkyvern: vault_type : typing.Optional[CredentialVaultType] Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization. + proxy_location : typing.Optional[ProxyLocation] + Optional proxy location for this credential's pinned proxy identity. + + proxy_session_id : typing.Optional[str] + Optional advanced reuse key for this credential's pinned proxy identity. + + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this credential. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -3093,6 +3150,9 @@ class RawSkyvern: object_=credential, annotation=CreateCredentialRequestCredential, direction="write" ), "vault_type": vault_type, + "proxy_location": proxy_location, + "proxy_session_id": proxy_session_id, + "rotate_proxy_session_id": rotate_proxy_session_id, }, headers={ "content-type": "application/json", @@ -5973,6 +6033,8 @@ class AsyncRawSkyvern: description: typing.Optional[str] = OMIT, browser_session_id: typing.Optional[str] = OMIT, workflow_run_id: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[BrowserProfile]: """ @@ -5992,6 +6054,12 @@ class AsyncRawSkyvern: workflow_run_id : typing.Optional[str] Workflow run whose persisted session should be captured. Omit for a blank profile. + proxy_location : typing.Optional[ProxyLocation] + Optional proxy location for this browser profile's pinned proxy identity. + + proxy_session_id : typing.Optional[str] + Optional advanced reuse key for this browser profile's pinned proxy identity. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -6008,6 +6076,8 @@ class AsyncRawSkyvern: "description": description, "browser_session_id": browser_session_id, "workflow_run_id": workflow_run_id, + "proxy_location": proxy_location, + "proxy_session_id": proxy_session_id, }, headers={ "content-type": "application/json", @@ -6183,6 +6253,9 @@ class AsyncRawSkyvern: *, name: typing.Optional[str] = OMIT, description: typing.Optional[str] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[BrowserProfile]: """ @@ -6199,6 +6272,15 @@ class AsyncRawSkyvern: description : typing.Optional[str] New description for the browser profile + proxy_location : typing.Optional[ProxyLocation] + Optional proxy location for this browser profile's pinned proxy identity. + + proxy_session_id : typing.Optional[str] + Optional advanced reuse key for this browser profile's pinned proxy identity. + + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this browser profile. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -6213,6 +6295,9 @@ class AsyncRawSkyvern: json={ "name": name, "description": description, + "proxy_location": proxy_location, + "proxy_session_id": proxy_session_id, + "rotate_proxy_session_id": rotate_proxy_session_id, }, headers={ "content-type": "application/json", @@ -6331,6 +6416,7 @@ class AsyncRawSkyvern: *, timeout: typing.Optional[int] = OMIT, proxy_location: typing.Optional[CreateBrowserSessionRequestProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, extensions: typing.Optional[typing.Sequence[Extensions]] = OMIT, browser_type: typing.Optional[PersistentBrowserType] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, @@ -6381,6 +6467,9 @@ class AsyncRawSkyvern: For self-hosted deployments, you can pass a custom proxy URL as a dict: {"url": "http://user:password@proxy.example.com:8080"}. This routes the browser through your own proxy server and takes precedence over any globally configured proxy pool. Can also be a GeoTarget object for granular city/state targeting: {"country": "US", "subdivision": "CA", "city": "San Francisco"}, or a custom proxy URL dict for self-hosted deployments: {"url": "http://user:password@proxy.example.com:8080"} + proxy_session_id : typing.Optional[str] + Opaque Skyvern-managed proxy sticky-session id for pinned Residential ISP sessions. + extensions : typing.Optional[typing.Sequence[Extensions]] A list of extensions to install in the browser session. @@ -6406,6 +6495,7 @@ class AsyncRawSkyvern: "proxy_location": convert_and_respect_annotation_metadata( object_=proxy_location, annotation=CreateBrowserSessionRequestProxyLocation, direction="write" ), + "proxy_session_id": proxy_session_id, "extensions": extensions, "browser_type": browser_type, "browser_profile_id": browser_profile_id, @@ -6767,6 +6857,9 @@ class AsyncRawSkyvern: credential_type: SkyvernForgeSdkSchemasCredentialsCredentialType, credential: CreateCredentialRequestCredential, vault_type: typing.Optional[CredentialVaultType] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[CredentialResponse]: """ @@ -6786,6 +6879,15 @@ class AsyncRawSkyvern: vault_type : typing.Optional[CredentialVaultType] Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization. + proxy_location : typing.Optional[ProxyLocation] + Optional proxy location for this credential's pinned proxy identity. + + proxy_session_id : typing.Optional[str] + Optional advanced reuse key for this credential's pinned proxy identity. + + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this credential. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -6804,6 +6906,9 @@ class AsyncRawSkyvern: object_=credential, annotation=CreateCredentialRequestCredential, direction="write" ), "vault_type": vault_type, + "proxy_location": proxy_location, + "proxy_session_id": proxy_session_id, + "rotate_proxy_session_id": rotate_proxy_session_id, }, headers={ "content-type": "application/json", @@ -6845,6 +6950,9 @@ class AsyncRawSkyvern: credential_type: SkyvernForgeSdkSchemasCredentialsCredentialType, credential: CreateCredentialRequestCredential, vault_type: typing.Optional[CredentialVaultType] = OMIT, + proxy_location: typing.Optional[ProxyLocation] = OMIT, + proxy_session_id: typing.Optional[str] = OMIT, + rotate_proxy_session_id: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[CredentialResponse]: """ @@ -6867,6 +6975,15 @@ class AsyncRawSkyvern: vault_type : typing.Optional[CredentialVaultType] Which vault to store this credential in. If omitted, uses the instance default. Use this to mix Skyvern-hosted and custom credentials within the same organization. + proxy_location : typing.Optional[ProxyLocation] + Optional proxy location for this credential's pinned proxy identity. + + proxy_session_id : typing.Optional[str] + Optional advanced reuse key for this credential's pinned proxy identity. + + rotate_proxy_session_id : typing.Optional[bool] + Rotate the Skyvern-managed proxy sticky-session id for this credential. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -6885,6 +7002,9 @@ class AsyncRawSkyvern: object_=credential, annotation=CreateCredentialRequestCredential, direction="write" ), "vault_type": vault_type, + "proxy_location": proxy_location, + "proxy_session_id": proxy_session_id, + "rotate_proxy_session_id": rotate_proxy_session_id, }, headers={ "content-type": "application/json", diff --git a/skyvern/client/types/browser_profile.py b/skyvern/client/types/browser_profile.py index 3e9bc7e39..f3547fa82 100644 --- a/skyvern/client/types/browser_profile.py +++ b/skyvern/client/types/browser_profile.py @@ -5,6 +5,7 @@ import typing import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .proxy_location import ProxyLocation class BrowserProfile(UniversalBaseModel): @@ -13,6 +14,8 @@ class BrowserProfile(UniversalBaseModel): name: str description: typing.Optional[str] = None source_browser_type: typing.Optional[str] = None + proxy_location: typing.Optional[ProxyLocation] = None + proxy_session_id: typing.Optional[str] = None created_at: dt.datetime modified_at: dt.datetime deleted_at: typing.Optional[dt.datetime] = None diff --git a/skyvern/client/types/credential_response.py b/skyvern/client/types/credential_response.py index 5c2a836b7..16b357b46 100644 --- a/skyvern/client/types/credential_response.py +++ b/skyvern/client/types/credential_response.py @@ -7,6 +7,7 @@ from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .credential_response_credential import CredentialResponseCredential from .credential_type_output import CredentialTypeOutput from .credential_vault_type import CredentialVaultType +from .proxy_location import ProxyLocation class CredentialResponse(UniversalBaseModel): @@ -44,6 +45,16 @@ class CredentialResponse(UniversalBaseModel): Browser profile ID linked to this credential """ + proxy_location: typing.Optional[ProxyLocation] = pydantic.Field(default=None) + """ + Optional proxy location for this credential's pinned proxy identity. + """ + + proxy_session_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional advanced reuse key for this credential's pinned proxy identity. + """ + tested_url: typing.Optional[str] = pydantic.Field(default=None) """ Login page URL used during the credential test diff --git a/skyvern/forge/agent_functions.py b/skyvern/forge/agent_functions.py index e4b50ed42..977fde902 100644 --- a/skyvern/forge/agent_functions.py +++ b/skyvern/forge/agent_functions.py @@ -667,6 +667,26 @@ class AgentFunction: workflow_schedules_use_local_scheduler: bool = settings.ENABLE_WORKFLOW_SCHEDULES """Whether the API process should run the built-in local scheduler loop.""" + def build_proxy_session_extra_http_headers(self, proxy_session_id: str | None) -> dict[str, str] | None: + return None + + def has_proxy_session_extra_http_headers(self, extra_http_headers: dict[str, str] | None) -> bool: + return False + + def merge_proxy_session_extra_http_headers( + self, + extra_http_headers: dict[str, str] | None, + proxy_session_id: str | None, + ) -> dict[str, str] | None: + proxy_session_headers = self.build_proxy_session_extra_http_headers(proxy_session_id) + if not proxy_session_headers: + return extra_http_headers + + headers = dict(extra_http_headers or {}) + for key, value in proxy_session_headers.items(): + headers.setdefault(key, value) + return headers + def get_flex_llm_key(self, llm_key: str | None) -> str | None: """Return a flex-tier router key for the given LLM key, or None if no flex twin exists. diff --git a/skyvern/forge/sdk/db/models.py b/skyvern/forge/sdk/db/models.py index cce3db4ef..96dfa69d3 100644 --- a/skyvern/forge/sdk/db/models.py +++ b/skyvern/forge/sdk/db/models.py @@ -1188,6 +1188,7 @@ class PersistentBrowserSessionModel(Base): ip_address = Column(String, nullable=True) ecs_task_arn = Column(String, nullable=True) proxy_location = Column(String, nullable=True) + proxy_session_id = Column(String, nullable=True) extensions = Column(JSON, nullable=True) browser_type = Column(String, nullable=True) browser_profile_id = Column(String, nullable=True, index=True) @@ -1218,6 +1219,8 @@ class BrowserProfileModel(Base): name = Column(String, nullable=False) description = Column(String, nullable=True) source_browser_type = Column(String, nullable=True) + proxy_location = Column(String, nullable=True) + proxy_session_id = Column(String, nullable=True) created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False) deleted_at = Column(DateTime, nullable=True) @@ -1350,6 +1353,8 @@ class CredentialModel(Base): tested_url = Column(String, nullable=True) user_context = Column(String(1000), nullable=True) save_browser_session_intent = Column(Boolean, nullable=True, default=False) + proxy_location = Column(String, nullable=True) + proxy_session_id = Column(String, nullable=True) folder_id = Column(String, ForeignKey("credential_folders.folder_id", ondelete="SET NULL"), nullable=True) created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) diff --git a/skyvern/forge/sdk/db/repositories/browser_sessions.py b/skyvern/forge/sdk/db/repositories/browser_sessions.py index 4cf0b77b5..9cbd729d1 100644 --- a/skyvern/forge/sdk/db/repositories/browser_sessions.py +++ b/skyvern/forge/sdk/db/repositories/browser_sessions.py @@ -13,10 +13,12 @@ from skyvern.forge.sdk.db.base_alchemy_db import read_retry from skyvern.forge.sdk.db.base_repository import BaseRepository from skyvern.forge.sdk.db.datetime_utils import naive_utc_now, to_naive_utc from skyvern.forge.sdk.db.exceptions import NotFoundError +from skyvern.forge.sdk.db.id import generate_browser_profile_id from skyvern.forge.sdk.db.models import ( BrowserProfileModel, PersistentBrowserSessionModel, ) +from skyvern.forge.sdk.db.repositories.proxy_pin_update import apply_proxy_pin_to_model, normalize_proxy_pin_for_create from skyvern.forge.sdk.db.utils import serialize_proxy_location from skyvern.forge.sdk.schemas.browser_profiles import BrowserProfile from skyvern.forge.sdk.schemas.persistent_browser_sessions import ( @@ -24,9 +26,11 @@ from skyvern.forge.sdk.schemas.persistent_browser_sessions import ( PersistentBrowserSession, PersistentBrowserType, ) +from skyvern.schemas.proxy_pinning import generate_proxy_session_id from skyvern.schemas.runs import ProxyLocation, ProxyLocationInput LOG = structlog.get_logger() +_UNSET = object() class BrowserSessionsRepository(BaseRepository): @@ -39,13 +43,24 @@ class BrowserSessionsRepository(BaseRepository): name: str, description: str | None = None, source_browser_type: str | None = None, + proxy_location: ProxyLocationInput = None, + proxy_session_id: str | None = None, ) -> BrowserProfile: async with self.Session() as session: + browser_profile_id = generate_browser_profile_id() + proxy_location, proxy_session_id = normalize_proxy_pin_for_create( + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + entity_id=browser_profile_id, + ) browser_profile = BrowserProfileModel( + browser_profile_id=browser_profile_id, organization_id=organization_id, name=name, description=description, source_browser_type=source_browser_type, + proxy_location=serialize_proxy_location(proxy_location), + proxy_session_id=proxy_session_id, ) session.add(browser_profile) await session.commit() @@ -151,6 +166,9 @@ class BrowserSessionsRepository(BaseRepository): organization_id: str, name: str | None = None, description: str | None = None, + proxy_location: ProxyLocationInput | object = _UNSET, + proxy_session_id: str | None | object = _UNSET, + rotate_proxy_session_id: bool = False, ) -> BrowserProfile: async with self.Session() as session: query = ( @@ -167,6 +185,14 @@ class BrowserSessionsRepository(BaseRepository): browser_profile.name = name if description is not None: browser_profile.description = description + apply_proxy_pin_to_model( + browser_profile, + entity_id=profile_id, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + unset=_UNSET, + rotate_proxy_session_id=rotate_proxy_session_id, + ) await session.commit() await session.refresh(browser_profile) @@ -288,28 +314,43 @@ class BrowserSessionsRepository(BaseRepository): runnable_id: str | None = None, timeout_minutes: int | None = None, proxy_location: ProxyLocationInput = ProxyLocation.RESIDENTIAL, + proxy_session_id: str | None = None, extensions: list[Extensions] | None = None, browser_type: PersistentBrowserType | None = None, browser_profile_id: str | None = None, generate_browser_profile: bool = False, ) -> PersistentBrowserSession: """Create a new persistent browser session.""" + proxy_location, proxy_session_id = normalize_proxy_pin_for_create( + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + ) extensions_str: list[str] | None = ( [extension.value for extension in extensions] if extensions is not None else None ) + serialized_proxy_location = serialize_proxy_location(proxy_location) async with self.Session() as session: browser_session = PersistentBrowserSessionModel( organization_id=organization_id, runnable_type=runnable_type, runnable_id=runnable_id, timeout_minutes=timeout_minutes, - proxy_location=serialize_proxy_location(proxy_location), + proxy_location=serialized_proxy_location, + proxy_session_id=proxy_session_id, extensions=extensions_str, browser_type=browser_type.value if browser_type else None, browser_profile_id=browser_profile_id, generate_browser_profile=generate_browser_profile, ) session.add(browser_session) + await session.flush() + if ( + serialized_proxy_location == ProxyLocation.RESIDENTIAL_ISP.value + and browser_session.proxy_session_id is None + ): + browser_session.proxy_session_id = generate_proxy_session_id( + browser_session.persistent_browser_session_id + ) await session.commit() await session.refresh(browser_session) return PersistentBrowserSession.model_validate(browser_session) diff --git a/skyvern/forge/sdk/db/repositories/credentials.py b/skyvern/forge/sdk/db/repositories/credentials.py index 5d0c01c0c..5d47d4c38 100644 --- a/skyvern/forge/sdk/db/repositories/credentials.py +++ b/skyvern/forge/sdk/db/repositories/credentials.py @@ -8,8 +8,18 @@ from skyvern.forge.sdk.db._error_handling import db_operation from skyvern.forge.sdk.db.base_repository import BaseRepository from skyvern.forge.sdk.db.exceptions import NotFoundError from skyvern.forge.sdk.db.models import CredentialModel, OrganizationBitwardenCollectionModel -from skyvern.forge.sdk.schemas.credentials import Credential, CredentialType, CredentialVaultType +from skyvern.forge.sdk.db.repositories.proxy_pin_update import apply_proxy_pin_to_model, normalize_proxy_pin_for_create +from skyvern.forge.sdk.db.utils import serialize_proxy_location +from skyvern.forge.sdk.schemas.credentials import ( + Credential, + CredentialType, + CredentialVaultType, +) from skyvern.forge.sdk.schemas.organization_bitwarden_collections import OrganizationBitwardenCollection +from skyvern.schemas.proxy_pinning import generate_proxy_session_id, should_generate_proxy_session_id +from skyvern.schemas.runs import ProxyLocationInput + +_UNSET = object() class CredentialRepository(BaseRepository): @@ -29,7 +39,14 @@ class CredentialRepository(BaseRepository): card_brand: str | None, totp_identifier: str | None = None, secret_label: str | None = None, + proxy_location: ProxyLocationInput = None, + proxy_session_id: str | None = None, ) -> Credential: + proxy_location, proxy_session_id = normalize_proxy_pin_for_create( + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + ) + serialized_proxy_location = serialize_proxy_location(proxy_location) async with self.Session() as session: credential = CredentialModel( organization_id=organization_id, @@ -43,8 +60,13 @@ class CredentialRepository(BaseRepository): card_last4=card_last4, card_brand=card_brand, secret_label=secret_label, + proxy_location=serialized_proxy_location, + proxy_session_id=proxy_session_id, ) session.add(credential) + await session.flush() + if should_generate_proxy_session_id(proxy_location) and credential.proxy_session_id is None: + credential.proxy_session_id = generate_proxy_session_id(credential.credential_id) await session.commit() await session.refresh(credential) return Credential.model_validate(credential) @@ -132,6 +154,9 @@ class CredentialRepository(BaseRepository): tested_url: str | None = None, user_context: str | None = None, save_browser_session_intent: bool | None = None, + proxy_location: ProxyLocationInput | object = _UNSET, + proxy_session_id: str | None | object = _UNSET, + rotate_proxy_session_id: bool = False, ) -> Credential: async with self.Session() as session: credential = ( @@ -154,6 +179,14 @@ class CredentialRepository(BaseRepository): credential.user_context = user_context if save_browser_session_intent is not None: credential.save_browser_session_intent = save_browser_session_intent + apply_proxy_pin_to_model( + credential, + entity_id=credential_id, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + unset=_UNSET, + rotate_proxy_session_id=rotate_proxy_session_id, + ) await session.commit() await session.refresh(credential) return Credential.model_validate(credential) @@ -172,6 +205,9 @@ class CredentialRepository(BaseRepository): card_last4: str | None = None, card_brand: str | None = None, secret_label: str | None = None, + proxy_location: ProxyLocationInput | object = _UNSET, + proxy_session_id: str | None | object = _UNSET, + rotate_proxy_session_id: bool = False, ) -> Credential: async with self.Session() as session: credential = ( @@ -194,6 +230,14 @@ class CredentialRepository(BaseRepository): credential.card_last4 = card_last4 credential.card_brand = card_brand credential.secret_label = secret_label + apply_proxy_pin_to_model( + credential, + entity_id=credential_id, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + unset=_UNSET, + rotate_proxy_session_id=rotate_proxy_session_id, + ) await session.commit() await session.refresh(credential) return Credential.model_validate(credential) diff --git a/skyvern/forge/sdk/db/repositories/proxy_pin_update.py b/skyvern/forge/sdk/db/repositories/proxy_pin_update.py new file mode 100644 index 000000000..ae429b561 --- /dev/null +++ b/skyvern/forge/sdk/db/repositories/proxy_pin_update.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import Any, cast + +from skyvern.forge.sdk.db.utils import serialize_proxy_location +from skyvern.schemas.proxy_pinning import generate_proxy_session_id, should_generate_proxy_session_id +from skyvern.schemas.runs import ProxyLocation, ProxyLocationInput + + +def normalize_proxy_pin_for_create( + *, + proxy_location: ProxyLocationInput, + proxy_session_id: str | None, + entity_id: str | None = None, +) -> tuple[ProxyLocationInput, str | None]: + if proxy_session_id and proxy_location is None: + return ProxyLocation.RESIDENTIAL_ISP, proxy_session_id + if proxy_location is None: + return None, None + if should_generate_proxy_session_id(proxy_location): + if proxy_session_id is None and entity_id is not None: + proxy_session_id = generate_proxy_session_id(entity_id) + return proxy_location, proxy_session_id + return proxy_location, None + + +def apply_proxy_pin_to_model( + model: Any, + *, + entity_id: str, + proxy_location: ProxyLocationInput | object, + proxy_session_id: str | None | object, + unset: object, + rotate_proxy_session_id: bool = False, +) -> None: + if proxy_location is not unset: + model.proxy_location = serialize_proxy_location(cast(ProxyLocationInput, proxy_location)) + if proxy_location is None or not should_generate_proxy_session_id(proxy_location): + model.proxy_session_id = None + elif rotate_proxy_session_id or (proxy_session_id is unset and not model.proxy_session_id): + model.proxy_session_id = generate_proxy_session_id(entity_id) + + if rotate_proxy_session_id and proxy_location is unset: + if not should_generate_proxy_session_id(model.proxy_location): + model.proxy_location = serialize_proxy_location(ProxyLocation.RESIDENTIAL_ISP) + model.proxy_session_id = generate_proxy_session_id(entity_id) + + if proxy_session_id is not unset: + if proxy_session_id: + if proxy_location is unset: + model.proxy_location = serialize_proxy_location(ProxyLocation.RESIDENTIAL_ISP) + if proxy_location is unset or should_generate_proxy_session_id(proxy_location): + model.proxy_session_id = cast(str, proxy_session_id) + elif proxy_location is unset: + model.proxy_location = None + model.proxy_session_id = cast(str | None, proxy_session_id) diff --git a/skyvern/forge/sdk/db/repositories/workflow_runs.py b/skyvern/forge/sdk/db/repositories/workflow_runs.py index e33117abd..b186bb16d 100644 --- a/skyvern/forge/sdk/db/repositories/workflow_runs.py +++ b/skyvern/forge/sdk/db/repositories/workflow_runs.py @@ -264,6 +264,7 @@ class WorkflowRunsRepository(BaseRepository): verification_code_identifier: str | None = None, verification_code_polling_started_at: datetime | None = None, browser_profile_id: str | None | object = _UNSET, + proxy_location: ProxyLocationInput | object = _UNSET, browser_address: str | None = None, extra_http_headers: dict[str, str] | None = None, cdp_connect_headers: dict[str, str] | None = None, @@ -327,6 +328,10 @@ class WorkflowRunsRepository(BaseRepository): workflow_run.verification_code_polling_started_at = None if browser_profile_id is not _UNSET: workflow_run.browser_profile_id = browser_profile_id + if proxy_location is not _UNSET: + workflow_run.proxy_location = serialize_proxy_location( + typing_cast(ProxyLocationInput, proxy_location) + ) if failure_category is not None: workflow_run.failure_category = failure_category # Explicit timestamp overrides (used when resetting workflow runs) diff --git a/skyvern/forge/sdk/routes/browser_profiles.py b/skyvern/forge/sdk/routes/browser_profiles.py index 38e0ac1db..4dbea6500 100644 --- a/skyvern/forge/sdk/routes/browser_profiles.py +++ b/skyvern/forge/sdk/routes/browser_profiles.py @@ -1,7 +1,7 @@ import asyncio import shutil from pathlib import Path as FilePath -from typing import NoReturn +from typing import Any, NoReturn import structlog from fastapi import Depends, HTTPException, Path, Query, status @@ -37,6 +37,9 @@ from skyvern.forge.sdk.schemas.browser_profiles import ( from skyvern.forge.sdk.schemas.organizations import Organization from skyvern.forge.sdk.schemas.persistent_browser_sessions import export_profile_storage_id from skyvern.forge.sdk.services import org_auth_service +from skyvern.schemas.proxy_pinning import apply_proxy_pin_update as _apply_proxy_pin_update +from skyvern.schemas.proxy_pinning import should_generate_proxy_session_id +from skyvern.schemas.runs import ProxyLocation, ProxyLocationInput LOG = structlog.get_logger() @@ -62,6 +65,16 @@ DEFAULT_PROFILE_COPY_IGNORE = { } +def _normalize_proxy_pin_fields( + *, + proxy_location: ProxyLocationInput, + proxy_session_id: str | None, +) -> tuple[ProxyLocationInput, str | None]: + if proxy_session_id: + return proxy_location or ProxyLocation.RESIDENTIAL_ISP, proxy_session_id + return proxy_location, None + + def _handle_duplicate_profile_name(*, organization_id: str, name: str, exc: IntegrityError) -> NoReturn: LOG.warning( "Duplicate browser profile name", @@ -117,26 +130,45 @@ async def create_browser_profile( if request.browser_session_id is not None: browser_session_id = request.browser_session_id + proxy_location, proxy_session_id = _normalize_proxy_pin_fields( + proxy_location=request.proxy_location, + proxy_session_id=request.proxy_session_id, + ) return await _create_profile_from_session( organization_id=organization_id, name=request.name, description=request.description, browser_session_id=browser_session_id, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, ) if request.workflow_run_id is not None: + workflow_run_id = request.workflow_run_id + proxy_location, proxy_session_id = _normalize_proxy_pin_fields( + proxy_location=request.proxy_location, + proxy_session_id=request.proxy_session_id, + ) return await _create_profile_from_workflow_run( organization_id=organization_id, name=request.name, description=request.description, - workflow_run_id=request.workflow_run_id, + workflow_run_id=workflow_run_id, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, ) await app.RATE_LIMITER.rate_limit_submit_run(organization_id) + proxy_location, proxy_session_id = _normalize_proxy_pin_fields( + proxy_location=request.proxy_location, + proxy_session_id=request.proxy_session_id, + ) return await _create_empty_profile( organization_id=organization_id, name=request.name, description=request.description, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, ) @@ -316,12 +348,21 @@ async def update_browser_profile( ) try: - profile = await app.DATABASE.browser_sessions.update_browser_profile( - profile_id=profile_id, - organization_id=organization_id, - name=request.name, - description=request.description, + update_kwargs: dict[str, Any] = { + "profile_id": profile_id, + "organization_id": organization_id, + "name": request.name, + "description": request.description, + } + _apply_proxy_pin_update( + update_kwargs, + proxy_location_was_set="proxy_location" in request.model_fields_set, + proxy_location=request.proxy_location, + proxy_session_id_was_set="proxy_session_id" in request.model_fields_set, + proxy_session_id=request.proxy_session_id, + rotate_proxy_session_id=request.rotate_proxy_session_id, ) + profile = await app.DATABASE.browser_sessions.update_browser_profile(**update_kwargs) except BrowserProfileNotFound: LOG.warning( "Browser profile not found for update", @@ -547,6 +588,8 @@ async def _create_empty_profile( organization_id: str, name: str, description: str | None, + proxy_location: ProxyLocationInput = None, + proxy_session_id: str | None = None, ) -> BrowserProfile: # Seed before inserting so local setup failures never reserve a profile name. profile_dir = _create_empty_browser_profile_directory() @@ -556,6 +599,8 @@ async def _create_empty_profile( organization_id=organization_id, name=name, description=description, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, ) except IntegrityError as exc: _handle_duplicate_profile_name(organization_id=organization_id, name=name, exc=exc) @@ -596,6 +641,8 @@ async def _create_profile_from_session( name: str, description: str | None, browser_session_id: str, + proxy_location: ProxyLocationInput = None, + proxy_session_id: str | None = None, ) -> BrowserProfile: browser_session = await app.DATABASE.browser_sessions.get_persistent_browser_session( browser_session_id, organization_id @@ -669,6 +716,13 @@ async def _create_profile_from_session( ) source_browser_type = browser_session.browser_type.value if browser_session.browser_type else None + if ( + proxy_session_id is None + and browser_session.proxy_session_id + and (proxy_location is None or should_generate_proxy_session_id(proxy_location)) + ): + proxy_location = browser_session.proxy_location or ProxyLocation.RESIDENTIAL_ISP + proxy_session_id = browser_session.proxy_session_id try: profile = await app.DATABASE.browser_sessions.create_browser_profile( @@ -676,6 +730,8 @@ async def _create_profile_from_session( name=name, description=description, source_browser_type=source_browser_type, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, ) except IntegrityError as exc: _handle_duplicate_profile_name(organization_id=organization_id, name=name, exc=exc) @@ -731,6 +787,8 @@ async def _create_profile_from_workflow_run( name: str, description: str | None, workflow_run_id: str, + proxy_location: ProxyLocationInput = None, + proxy_session_id: str | None = None, ) -> BrowserProfile: workflow_run = await app.DATABASE.workflow_runs.get_workflow_run(workflow_run_id, organization_id=organization_id) if not workflow_run: @@ -802,6 +860,8 @@ async def _create_profile_from_workflow_run( organization_id=organization_id, name=name, description=description, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, ) except IntegrityError as exc: _handle_duplicate_profile_name(organization_id=organization_id, name=name, exc=exc) diff --git a/skyvern/forge/sdk/routes/browser_sessions.py b/skyvern/forge/sdk/routes/browser_sessions.py index 3229c6947..208b710ed 100644 --- a/skyvern/forge/sdk/routes/browser_sessions.py +++ b/skyvern/forge/sdk/routes/browser_sessions.py @@ -26,6 +26,8 @@ from skyvern.schemas.browser_sessions import ( ProcessBrowserSessionRecordingResponse, UpdateBrowserSessionRequest, ) +from skyvern.schemas.proxy_pinning import should_generate_proxy_session_id +from skyvern.schemas.runs import ProxyLocation from skyvern.webeye.schemas import BrowserSessionResponse @@ -93,6 +95,8 @@ async def create_browser_session( browser_session_request: CreateBrowserSessionRequest = CreateBrowserSessionRequest(), current_org: Organization = Depends(org_auth_service.get_current_org), ) -> BrowserSessionResponse: + proxy_location = browser_session_request.proxy_location + proxy_session_id = browser_session_request.proxy_session_id if browser_session_request.browser_profile_id: profile = await app.DATABASE.browser_sessions.get_browser_profile( browser_session_request.browser_profile_id, @@ -103,11 +107,27 @@ async def create_browser_session( status_code=404, detail=f"Browser profile {browser_session_request.browser_profile_id} not found", ) + proxy_location_was_set = "proxy_location" in browser_session_request.model_fields_set + if not proxy_location_was_set: + proxy_location = profile.proxy_location + if "proxy_session_id" not in browser_session_request.model_fields_set: + if not proxy_location_was_set or should_generate_proxy_session_id(proxy_location): + proxy_session_id = profile.proxy_session_id + else: + proxy_session_id = None + if proxy_session_id and proxy_location is None: + proxy_location = ProxyLocation.RESIDENTIAL_ISP + if proxy_session_id and not should_generate_proxy_session_id(proxy_location): + raise HTTPException( + status_code=400, + detail="proxy_session_id is only supported with RESIDENTIAL_ISP proxy_location", + ) browser_session = await app.PERSISTENT_SESSIONS_MANAGER.create_session( organization_id=current_org.organization_id, timeout_minutes=browser_session_request.timeout, - proxy_location=browser_session_request.proxy_location, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, extensions=browser_session_request.extensions, browser_type=browser_session_request.browser_type, browser_profile_id=browser_session_request.browser_profile_id, diff --git a/skyvern/forge/sdk/routes/credentials.py b/skyvern/forge/sdk/routes/credentials.py index 32f8d0cd5..3079b92d7 100644 --- a/skyvern/forge/sdk/routes/credentials.py +++ b/skyvern/forge/sdk/routes/credentials.py @@ -34,7 +34,7 @@ import asyncio import json import time from dataclasses import dataclass -from typing import Annotated +from typing import Annotated, Any import pyotp import structlog @@ -121,6 +121,9 @@ from skyvern.schemas.credential_folders import ( CredentialFolderUpdate, UpdateCredentialFolderRequest, ) +from skyvern.schemas.proxy_pinning import apply_proxy_pin_update as _apply_proxy_pin_update +from skyvern.schemas.proxy_pinning import redact_proxy_session_id +from skyvern.schemas.runs import ProxyLocation from skyvern.schemas.workflows import ( BLOCK_YAML_TYPES, LoginBlockYAML, @@ -516,42 +519,7 @@ async def create_credential( # Early resyncing the Bitwarden vault background_tasks.add_task(fetch_credential_item_background, credential.item_id) - if data.credential_type == CredentialType.PASSWORD: - credential_response = PasswordCredentialResponse( - username=data.credential.username, - totp_type=data.credential.totp_type if hasattr(data.credential, "totp_type") else "none", - totp_identifier=data.credential.totp_identifier if hasattr(data.credential, "totp_identifier") else None, - ) - return CredentialResponse( - credential=credential_response, - credential_id=credential.credential_id, - credential_type=data.credential_type, - name=data.name, - vault_type=credential.vault_type, - ) - elif data.credential_type == CredentialType.CREDIT_CARD: - credential_response = CreditCardCredentialResponse( - last_four=data.credential.card_number[-4:], - brand=data.credential.card_brand, - ) - return CredentialResponse( - credential=credential_response, - credential_id=credential.credential_id, - credential_type=data.credential_type, - name=data.name, - vault_type=credential.vault_type, - ) - elif data.credential_type == CredentialType.SECRET: - credential_response = SecretCredentialResponse(secret_label=data.credential.secret_label) - return CredentialResponse( - credential=credential_response, - credential_id=credential.credential_id, - credential_type=data.credential_type, - name=data.name, - vault_type=credential.vault_type, - ) - else: - raise HTTPException(status_code=400, detail=f"Unsupported credential type: {data.credential_type}") + return _convert_to_response(credential) LOGIN_TEST_PROMPT = ( @@ -675,17 +643,26 @@ async def rename_credential( if not credential: raise HTTPException(status_code=404, detail=f"Credential not found, credential_id={credential_id}") - update_kwargs: dict = { + update_kwargs: dict[str, Any] = { "credential_id": credential_id, "organization_id": current_org.organization_id, - "name": data.name, } + if "name" in data.model_fields_set: + update_kwargs["name"] = data.name if data.tested_url is not None: update_kwargs["tested_url"] = data.tested_url if data.user_context is not None: update_kwargs["user_context"] = data.user_context if data.save_browser_session_intent is not None: update_kwargs["save_browser_session_intent"] = data.save_browser_session_intent + _apply_proxy_pin_update( + update_kwargs, + proxy_location_was_set="proxy_location" in data.model_fields_set, + proxy_location=data.proxy_location, + proxy_session_id_was_set="proxy_session_id" in data.model_fields_set, + proxy_session_id=data.proxy_session_id, + rotate_proxy_session_id=data.rotate_proxy_session_id, + ) updated = await app.DATABASE.credentials.update_credential(**update_kwargs) if not updated: raise HTTPException(status_code=500, detail="Failed to update credential") @@ -745,6 +722,19 @@ async def test_login( background_tasks.add_task(fetch_credential_item_background, credential.item_id) credential_id = credential.credential_id + if "proxy_location" in data.model_fields_set or "proxy_session_id" in data.model_fields_set: + update_kwargs: dict[str, Any] = { + "credential_id": credential_id, + "organization_id": organization_id, + } + _apply_proxy_pin_update( + update_kwargs, + proxy_location_was_set="proxy_location" in data.model_fields_set, + proxy_location=data.proxy_location, + proxy_session_id_was_set="proxy_session_id" in data.model_fields_set, + proxy_session_id=data.proxy_session_id, + ) + credential = await app.DATABASE.credentials.update_credential(**update_kwargs) LOG.info( "Testing login with inline credentials", @@ -798,7 +788,16 @@ async def test_login( request=workflow_create_request, ) - run_request = WorkflowRequestBody() + credential_proxy_session_id = getattr(credential, "proxy_session_id", None) + if credential_proxy_session_id: + run_request = WorkflowRequestBody( + proxy_location=getattr(credential, "proxy_location", None) or ProxyLocation.RESIDENTIAL_ISP, + extra_http_headers=app.AGENT_FUNCTION.build_proxy_session_extra_http_headers( + credential_proxy_session_id + ), + ) + else: + run_request = WorkflowRequestBody() workflow_run = await app.WORKFLOW_SERVICE.setup_workflow_run( request_id=None, @@ -984,7 +983,15 @@ async def test_credential( # Boot fresh (don't seed the saved profile): a reused profile is loaded read-only and the # refreshed session would never persist. A fresh login persists via the normal session path, # which the saver then writes onto existing_browser_profile_id. - run_request = WorkflowRequestBody() + if credential.proxy_session_id: + run_request = WorkflowRequestBody( + proxy_location=credential.proxy_location or ProxyLocation.RESIDENTIAL_ISP, + extra_http_headers=app.AGENT_FUNCTION.build_proxy_session_extra_http_headers( + credential.proxy_session_id + ), + ) + else: + run_request = WorkflowRequestBody() workflow_run = await app.WORKFLOW_SERVICE.setup_workflow_run( request_id=None, @@ -1396,7 +1403,16 @@ async def _create_browser_profile_after_workflow( # Re-save overwrites the existing profile in place so references to its id keep # working; a first-time save (or a since-deleted profile) creates a new one. + credential = await app.DATABASE.credentials.get_credential( + credential_id=credential_id, + organization_id=organization_id, + ) + proxy_location = credential.proxy_location if credential else None + proxy_session_id = credential.proxy_session_id if credential else None + credential_has_proxy_pin = proxy_session_id is not None + target_profile_id = existing_browser_profile_id + existing_profile = None reused_existing = False if target_profile_id: existing_profile = await app.DATABASE.browser_sessions.get_browser_profile( @@ -1407,13 +1423,33 @@ async def _create_browser_profile_after_workflow( if not reused_existing: target_profile_id = None + should_update_existing_profile_pin = False if not target_profile_id: profile = await app.DATABASE.browser_sessions.create_browser_profile( organization_id=organization_id, name=f"Profile - {credential_name} ({credential_id})", description=f"Browser profile from credential test for {credential_name}", + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, ) target_profile_id = profile.browser_profile_id + else: + should_update_existing_profile_pin = credential_has_proxy_pin + existing_profile_proxy_session_id = getattr(existing_profile, "proxy_session_id", None) + if ( + existing_profile_proxy_session_id + and proxy_session_id + and existing_profile_proxy_session_id != proxy_session_id + ): + should_update_existing_profile_pin = False + LOG.warning( + "Skipping credential proxy pin mirror because linked browser profile already has a different pin", + credential_id=credential_id, + browser_profile_id=target_profile_id, + organization_id=organization_id, + credential_proxy_session_id=redact_proxy_session_id(proxy_session_id), + browser_profile_proxy_session_id=redact_proxy_session_id(existing_profile_proxy_session_id), + ) # Overwrites in place when reusing an existing id. await app.STORAGE.store_browser_profile( @@ -1422,6 +1458,13 @@ async def _create_browser_profile_after_workflow( directory=session_dir, ) if reused_existing: + if should_update_existing_profile_pin: + await app.DATABASE.browser_sessions.update_browser_profile( + profile_id=target_profile_id, + organization_id=organization_id, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, + ) # Bump modified_at so the status poll can tell this run's re-save actually landed. await app.DATABASE.browser_sessions.touch_browser_profile( profile_id=target_profile_id, @@ -2778,6 +2821,8 @@ def _convert_to_response(credential: Credential) -> CredentialResponse: user_context=credential.user_context, save_browser_session_intent=credential.save_browser_session_intent, folder_id=credential.folder_id, + proxy_location=credential.proxy_location, + proxy_session_id=credential.proxy_session_id, ) elif credential.credential_type == CredentialType.CREDIT_CARD: credential_response = CreditCardCredentialResponse( @@ -2795,6 +2840,8 @@ def _convert_to_response(credential: Credential) -> CredentialResponse: user_context=credential.user_context, save_browser_session_intent=credential.save_browser_session_intent, folder_id=credential.folder_id, + proxy_location=credential.proxy_location, + proxy_session_id=credential.proxy_session_id, ) elif credential.credential_type == CredentialType.SECRET: credential_response = SecretCredentialResponse(secret_label=credential.secret_label) @@ -2809,6 +2856,8 @@ def _convert_to_response(credential: Credential) -> CredentialResponse: user_context=credential.user_context, save_browser_session_intent=credential.save_browser_session_intent, folder_id=credential.folder_id, + proxy_location=credential.proxy_location, + proxy_session_id=credential.proxy_session_id, ) else: raise HTTPException(status_code=400, detail="Credential type not supported") diff --git a/skyvern/forge/sdk/schemas/browser_profiles.py b/skyvern/forge/sdk/schemas/browser_profiles.py index 2c259de5c..cb99d3efd 100644 --- a/skyvern/forge/sdk/schemas/browser_profiles.py +++ b/skyvern/forge/sdk/schemas/browser_profiles.py @@ -1,6 +1,9 @@ from datetime import datetime -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +from skyvern.schemas.proxy_location import ProxyLocationInput +from skyvern.schemas.proxy_pinning import parse_proxy_location_input, validate_proxy_session_id class BrowserProfile(BaseModel): @@ -11,10 +14,22 @@ class BrowserProfile(BaseModel): name: str description: str | None = None source_browser_type: str | None = None + proxy_location: ProxyLocationInput = None + proxy_session_id: str | None = None created_at: datetime modified_at: datetime deleted_at: datetime | None = None + @field_validator("proxy_location", mode="before") + @classmethod + def deserialize_proxy_location_field(cls, value: object) -> object: + return parse_proxy_location_input(value) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) + class CreateBrowserProfileRequest(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) @@ -31,6 +46,14 @@ class CreateBrowserProfileRequest(BaseModel): min_length=1, description="Workflow run whose persisted session should be captured. Omit for a blank profile.", ) + proxy_location: ProxyLocationInput = Field( + default=None, + description="Optional proxy location for this profile's pinned proxy identity.", + ) + proxy_session_id: str | None = Field( + default=None, + description="Explicit sticky-session id for this profile's pinned proxy identity.", + ) @model_validator(mode="after") def _validate_source(self) -> "CreateBrowserProfileRequest": @@ -38,6 +61,16 @@ class CreateBrowserProfileRequest(BaseModel): raise ValueError("Provide only one of browser_session_id or workflow_run_id") return self + @field_validator("proxy_location", mode="before") + @classmethod + def deserialize_proxy_location_field(cls, value: object) -> object: + return parse_proxy_location_input(value) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) + class UpdateBrowserProfileRequest(BaseModel): # `None` for either field means "no change" — there is currently no way to @@ -46,9 +79,31 @@ class UpdateBrowserProfileRequest(BaseModel): name: str | None = Field(default=None, min_length=1, description="New name for the browser profile") description: str | None = Field(default=None, description="New description for the browser profile") + proxy_location: ProxyLocationInput = Field( + default=None, + description="Optional proxy location for this profile's pinned proxy identity.", + ) + proxy_session_id: str | None = Field( + default=None, + description="Opaque Skyvern-managed proxy sticky-session id.", + ) + rotate_proxy_session_id: bool = Field( + default=False, + description="Rotate the Skyvern-managed proxy sticky-session id for this profile.", + ) @model_validator(mode="after") def _require_at_least_one_field(self) -> "UpdateBrowserProfileRequest": - if self.name is None and self.description is None: - raise ValueError("At least one of `name` or `description` must be provided") + if not self.model_fields_set: + raise ValueError("At least one browser profile field must be provided") return self + + @field_validator("proxy_location", mode="before") + @classmethod + def deserialize_proxy_location_field(cls, value: object) -> object: + return parse_proxy_location_input(value) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) diff --git a/skyvern/forge/sdk/schemas/credentials.py b/skyvern/forge/sdk/schemas/credentials.py index b33348447..4b0e87807 100644 --- a/skyvern/forge/sdk/schemas/credentials.py +++ b/skyvern/forge/sdk/schemas/credentials.py @@ -6,6 +6,8 @@ from fastapi import status from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from skyvern.exceptions import SkyvernHTTPException +from skyvern.schemas.proxy_location import ProxyLocationInput +from skyvern.schemas.proxy_pinning import parse_proxy_location_input, validate_proxy_session_id from skyvern.utils.url_validators import validate_url @@ -220,6 +222,28 @@ class CreateCredentialRequest(BaseModel): "Use this to mix Skyvern-hosted and custom credentials within the same organization.", examples=["custom", "azure_vault", "bitwarden"], ) + proxy_location: ProxyLocationInput = Field( + default=None, + description="Optional proxy location for this credential's pinned proxy identity.", + ) + proxy_session_id: str | None = Field( + default=None, + description="Optional advanced reuse key for this credential's pinned proxy identity.", + ) + rotate_proxy_session_id: bool = Field( + default=False, + description="Rotate the Skyvern-managed proxy sticky-session id when updating this credential.", + ) + + @field_validator("proxy_location", mode="before") + @classmethod + def deserialize_proxy_location_field(cls, value: object) -> object: + return parse_proxy_location_input(value) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) class CredentialResponse(BaseModel): @@ -250,6 +274,19 @@ class CredentialResponse(BaseModel): description="ID of the credential folder this credential belongs to, if any", examples=["cfld_1234567890"], ) + proxy_location: ProxyLocationInput = Field( + default=None, + description="Optional proxy location used for the credential's pinned proxy identity.", + ) + proxy_session_id: str | None = Field( + default=None, + description="Opaque Skyvern-managed proxy sticky-session id.", + ) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) class OnePasswordItemOverview(BaseModel): @@ -331,17 +368,35 @@ class Credential(BaseModel): default=None, description="ID of the credential folder this credential belongs to, if any", ) + proxy_location: ProxyLocationInput = Field( + default=None, + description="Optional proxy location used for the credential's pinned proxy identity.", + ) + proxy_session_id: str | None = Field( + default=None, + description="Opaque Skyvern-managed proxy sticky-session id.", + ) created_at: datetime = Field(..., description="Timestamp when the credential was created") modified_at: datetime = Field(..., description="Timestamp when the credential was last modified") deleted_at: datetime | None = Field(None, description="Timestamp when the credential was deleted, if applicable") + @field_validator("proxy_location", mode="before") + @classmethod + def deserialize_proxy_location_field(cls, value: object) -> object: + return parse_proxy_location_input(value) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) + class UpdateCredentialRequest(BaseModel): """Request model for updating credential metadata.""" - name: str = Field( - ..., + name: str | None = Field( + default=None, min_length=1, description="New name for the credential", examples=["My Updated Credential"], @@ -360,12 +415,40 @@ class UpdateCredentialRequest(BaseModel): default=None, description="Whether the user intends to save a browser session, regardless of test outcome", ) + proxy_location: ProxyLocationInput = Field( + default=None, + description="Optional proxy location for this credential's pinned proxy identity.", + ) + proxy_session_id: str | None = Field( + default=None, + description="Opaque Skyvern-managed proxy sticky-session id.", + ) + rotate_proxy_session_id: bool = Field( + default=False, + description="Rotate the Skyvern-managed proxy sticky-session id for this credential.", + ) @field_validator("user_context", mode="before") @classmethod def normalize_user_context(cls, v: str | None) -> str | None: return _normalize_optional_str(v) + @field_validator("proxy_location", mode="before") + @classmethod + def deserialize_proxy_location_field(cls, value: object) -> object: + return parse_proxy_location_input(value) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) + + @model_validator(mode="after") + def _require_at_least_one_field(self) -> Self: + if not self.model_fields_set: + raise ValueError("At least one credential metadata field must be provided") + return self + def _normalize_optional_str(v: str | None) -> str | None: """Normalize whitespace-only strings to None.""" @@ -443,12 +526,30 @@ class TestLoginRequest(BaseModel): max_length=1000, description="Optional user-provided context describing the login sequence (e.g., 'click SSO button first')", ) + proxy_location: ProxyLocationInput = Field( + default=None, + description="Optional proxy location for this test credential's pinned proxy identity.", + ) + proxy_session_id: str | None = Field( + default=None, + description="Opaque Skyvern-managed proxy sticky-session id.", + ) @field_validator("user_context", mode="before") @classmethod def normalize_user_context(cls, v: str | None) -> str | None: return _normalize_optional_str(v) + @field_validator("proxy_location", mode="before") + @classmethod + def deserialize_proxy_location_field(cls, value: object) -> object: + return parse_proxy_location_input(value) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) + @model_validator(mode="after") def validate_url(self) -> Self: result = validate_url(self.url) diff --git a/skyvern/forge/sdk/schemas/persistent_browser_sessions.py b/skyvern/forge/sdk/schemas/persistent_browser_sessions.py index e5ab427d2..b297d7025 100644 --- a/skyvern/forge/sdk/schemas/persistent_browser_sessions.py +++ b/skyvern/forge/sdk/schemas/persistent_browser_sessions.py @@ -5,6 +5,7 @@ from enum import StrEnum from pydantic import BaseModel, ConfigDict, field_validator from skyvern.forge.sdk.db.utils import deserialize_proxy_location +from skyvern.schemas.proxy_pinning import validate_proxy_session_id from skyvern.schemas.runs import ProxyLocationInput @@ -71,6 +72,7 @@ class PersistentBrowserSession(BaseModel): status: str | None = None timeout_minutes: int | None = None proxy_location: ProxyLocationInput = None + proxy_session_id: str | None = None instance_type: str | None = None vcpu_millicores: int | None = None memory_mb: int | None = None @@ -96,6 +98,11 @@ class PersistentBrowserSession(BaseModel): return deserialize_proxy_location(value, raise_on_invalid_geo_target=True) return value + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) + def should_export_profile(self) -> bool: """A session persists its profile at teardown only when it opted in or is reusing a saved profile. diff --git a/skyvern/forge/sdk/services/credential/credential_vault_service.py b/skyvern/forge/sdk/services/credential/credential_vault_service.py index 2a32e428e..a98a2a33c 100644 --- a/skyvern/forge/sdk/services/credential/credential_vault_service.py +++ b/skyvern/forge/sdk/services/credential/credential_vault_service.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import Any from skyvern.forge import app from skyvern.forge.sdk.schemas.credentials import ( @@ -118,6 +119,8 @@ class CredentialVaultService(ABC): totp_identifier=data.credential.totp_identifier, card_last4=None, card_brand=None, + proxy_location=data.proxy_location, + proxy_session_id=data.proxy_session_id, ) elif data.credential_type == CredentialType.CREDIT_CARD: return await app.DATABASE.credentials.create_credential( @@ -131,6 +134,8 @@ class CredentialVaultService(ABC): card_last4=data.credential.card_number[-4:], card_brand=data.credential.card_brand, totp_identifier=None, + proxy_location=data.proxy_location, + proxy_session_id=data.proxy_session_id, ) elif data.credential_type == CredentialType.SECRET: return await app.DATABASE.credentials.create_credential( @@ -145,6 +150,8 @@ class CredentialVaultService(ABC): card_brand=None, totp_identifier=None, secret_label=data.credential.secret_label, + proxy_location=data.proxy_location, + proxy_session_id=data.proxy_session_id, ) else: raise Exception(f"Unsupported credential type: {data.credential_type}") @@ -155,6 +162,14 @@ class CredentialVaultService(ABC): data: CreateCredentialRequest, item_id: str, ) -> Credential: + proxy_kwargs: dict[str, Any] = {} + if "proxy_location" in data.model_fields_set: + proxy_kwargs["proxy_location"] = data.proxy_location + if "proxy_session_id" in data.model_fields_set: + proxy_kwargs["proxy_session_id"] = data.proxy_session_id + if data.rotate_proxy_session_id: + proxy_kwargs["rotate_proxy_session_id"] = True + if data.credential_type == CredentialType.PASSWORD: return await app.DATABASE.credentials.update_credential_vault_data( credential_id=credential.credential_id, @@ -167,6 +182,7 @@ class CredentialVaultService(ABC): totp_identifier=data.credential.totp_identifier, card_last4=None, card_brand=None, + **proxy_kwargs, ) elif data.credential_type == CredentialType.CREDIT_CARD: return await app.DATABASE.credentials.update_credential_vault_data( @@ -180,6 +196,7 @@ class CredentialVaultService(ABC): card_last4=data.credential.card_number[-4:], card_brand=data.credential.card_brand, totp_identifier=None, + **proxy_kwargs, ) elif data.credential_type == CredentialType.SECRET: return await app.DATABASE.credentials.update_credential_vault_data( @@ -194,6 +211,7 @@ class CredentialVaultService(ABC): card_brand=None, totp_identifier=None, secret_label=data.credential.secret_label, + **proxy_kwargs, ) else: raise Exception(f"Unsupported credential type: {data.credential_type}") diff --git a/skyvern/forge/sdk/workflow/service.py b/skyvern/forge/sdk/workflow/service.py index 2c4839186..2793b8568 100644 --- a/skyvern/forge/sdk/workflow/service.py +++ b/skyvern/forge/sdk/workflow/service.py @@ -133,6 +133,7 @@ from skyvern.forge.sdk.workflow.status_mapping import ( from skyvern.forge.sdk.workflow.workflow_definition_converter import convert_workflow_definition from skyvern.schemas.run_enums import RunEngine from skyvern.schemas.runs import ( + ProxyLocation, ProxyLocationInput, RunStatus, RunType, @@ -2795,6 +2796,12 @@ class WorkflowService: ) if block.block_type == BlockType.LOGIN: + await self._apply_login_block_credential_proxy_pin( + block=block, + workflow_run=workflow_run, + workflow_run_id=workflow_run_id, + organization_id=organization_id, + ) resolved_browser_profile_id = await self._resolve_login_block_browser_profile_id( block=block, workflow_run_id=workflow_run_id, @@ -3484,10 +3491,60 @@ class WorkflowService: # so it must not reuse (and boot read-only from) the credential's saved profile. if isinstance(block, LoginBlock) and block.skip_saved_profile: return None + credential_ids = await self._resolve_login_block_credential_ids( + block=block, + workflow_run_id=workflow_run_id, + ) + + for credential_id in credential_ids: + if not organization_id: + continue + try: + db_cred = await app.DATABASE.credentials.get_credential( + credential_id=credential_id, + organization_id=organization_id, + ) + if db_cred and db_cred.browser_profile_id: + # Verify the browser profile still exists before using it + profile = await app.DATABASE.browser_sessions.get_browser_profile( + profile_id=db_cred.browser_profile_id, + organization_id=organization_id, + ) + if not profile: + LOG.warning( + "Credential has browser_profile_id but profile not found, ignoring", + credential_id=credential_id, + browser_profile_id=db_cred.browser_profile_id, + workflow_run_id=workflow_run_id, + ) + continue + LOG.info( + "Resolved browser_profile_id from LoginBlock credential", + credential_id=credential_id, + browser_profile_id=db_cred.browser_profile_id, + workflow_run_id=workflow_run_id, + ) + return db_cred.browser_profile_id + except Exception: + LOG.warning( + "Failed to look up credential for browser profile", + credential_id=credential_id, + workflow_run_id=workflow_run_id, + exc_info=True, + ) + return None + + async def _resolve_login_block_credential_ids( + self, + block: Block, + workflow_run_id: str | None, + ) -> list[str]: + """Return credential ids bound to this block, preserving parameter order.""" params = block.parameters # Pre-fetch run parameters once (used by WorkflowParameter/CREDENTIAL_ID style). run_param_tuples: list[tuple[Any, Any]] | None = None + credential_ids: list[str] = [] for param in params: credential_id: str | None = None @@ -3532,8 +3589,29 @@ class WorkflowService: if not credential_id: continue + credential_ids.append(credential_id) - # Look up the credential and check for a browser_profile_id + return credential_ids + + async def _apply_login_block_credential_proxy_pin( + self, + *, + block: Block, + workflow_run: WorkflowRun, + workflow_run_id: str, + organization_id: str | None, + ) -> None: + if not organization_id: + return + headers = dict(workflow_run.extra_http_headers or {}) + if app.AGENT_FUNCTION.has_proxy_session_extra_http_headers(headers): + return + + credential_ids = await self._resolve_login_block_credential_ids( + block=block, + workflow_run_id=workflow_run_id, + ) + for credential_id in credential_ids: if not organization_id: continue try: @@ -3541,35 +3619,67 @@ class WorkflowService: credential_id=credential_id, organization_id=organization_id, ) - if db_cred and db_cred.browser_profile_id: - # Verify the browser profile still exists before using it + if not db_cred: + continue + proxy_session_id = db_cred.proxy_session_id + proxy_location = db_cred.proxy_location + browser_profile_id = getattr(db_cred, "browser_profile_id", None) + if browser_profile_id: profile = await app.DATABASE.browser_sessions.get_browser_profile( - profile_id=db_cred.browser_profile_id, + profile_id=browser_profile_id, organization_id=organization_id, ) - if not profile: - LOG.warning( - "Credential has browser_profile_id but profile not found, ignoring", + if profile: + if not profile.proxy_session_id: + LOG.info( + "Skipping LoginBlock credential proxy pin because linked browser profile is unpinned", + credential_id=credential_id, + browser_profile_id=browser_profile_id, + workflow_run_id=workflow_run_id, + ) + return + proxy_session_id = profile.proxy_session_id + proxy_location = profile.proxy_location or ProxyLocation.RESIDENTIAL_ISP + LOG.info( + "Using linked browser profile proxy pin for LoginBlock", credential_id=credential_id, - browser_profile_id=db_cred.browser_profile_id, + browser_profile_id=browser_profile_id, workflow_run_id=workflow_run_id, ) - continue - LOG.info( - "Resolved browser_profile_id from LoginBlock credential", - credential_id=credential_id, - browser_profile_id=db_cred.browser_profile_id, - workflow_run_id=workflow_run_id, + if proxy_session_id: + updated_headers = app.AGENT_FUNCTION.merge_proxy_session_extra_http_headers( + headers, proxy_session_id ) - return db_cred.browser_profile_id + # The OSS AgentFunction stub returns the original headers; only persist when cloud injected a pin. + if updated_headers == headers: + return + proxy_location_update: ProxyLocationInput | None = None + if workflow_run.proxy_location is None: + proxy_location_update = proxy_location or ProxyLocation.RESIDENTIAL_ISP + update_kwargs: dict[str, Any] = { + "workflow_run_id": workflow_run_id, + "extra_http_headers": updated_headers, + } + if proxy_location_update is not None: + update_kwargs["proxy_location"] = proxy_location_update + await app.DATABASE.workflow_runs.update_workflow_run(**update_kwargs) + workflow_run.extra_http_headers = updated_headers + if proxy_location_update is not None: + workflow_run.proxy_location = proxy_location_update + LOG.info( + "Applied LoginBlock credential proxy pin to workflow run", + credential_id=credential_id, + workflow_run_id=workflow_run_id, + proxy_location=str(workflow_run.proxy_location), + ) + return except Exception: - LOG.warning( - "Failed to look up credential for browser profile", + LOG.error( + "Failed to apply LoginBlock credential proxy pin", credential_id=credential_id, workflow_run_id=workflow_run_id, exc_info=True, ) - return None async def _evaluate_debug_session_profile_decision( self, diff --git a/skyvern/schemas/browser_sessions.py b/skyvern/schemas/browser_sessions.py index fcee70b7d..34920c0ce 100644 --- a/skyvern/schemas/browser_sessions.py +++ b/skyvern/schemas/browser_sessions.py @@ -4,6 +4,7 @@ from skyvern.client.types.workflow_definition_yaml_blocks_item import WorkflowDe from skyvern.client.types.workflow_definition_yaml_parameters_item import WorkflowDefinitionYamlParametersItem_Workflow from skyvern.forge.sdk.schemas.persistent_browser_sessions import Extensions, PersistentBrowserType from skyvern.schemas.docs.doc_strings import PROXY_LOCATION_DOC_STRING +from skyvern.schemas.proxy_pinning import validate_proxy_session_id from skyvern.schemas.runs import GeoTarget, ProxyLocationInput from skyvern.services.browser_recording.types import RecordingDraftStep @@ -37,6 +38,16 @@ class CreateBrowserSessionRequest(BaseModel): return GeoTarget.model_validate(proxy_location) return proxy_location + proxy_session_id: str | None = Field( + default=None, + description="Opaque Skyvern-managed proxy sticky-session id for pinned Residential ISP sessions.", + ) + + @field_validator("proxy_session_id") + @classmethod + def validate_proxy_session_id_field(cls, value: str | None) -> str | None: + return validate_proxy_session_id(value) + extensions: list[Extensions] | None = Field( default=None, description="A list of extensions to install in the browser session.", diff --git a/skyvern/schemas/proxy_pinning.py b/skyvern/schemas/proxy_pinning.py new file mode 100644 index 000000000..e1e486136 --- /dev/null +++ b/skyvern/schemas/proxy_pinning.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import json +import secrets +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 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 diff --git a/skyvern/webeye/default_persistent_sessions_manager.py b/skyvern/webeye/default_persistent_sessions_manager.py index 93f4003bb..e2e2de4af 100644 --- a/skyvern/webeye/default_persistent_sessions_manager.py +++ b/skyvern/webeye/default_persistent_sessions_manager.py @@ -311,6 +311,7 @@ class DefaultPersistentSessionsManager(PersistentSessionsManager): timeout_minutes: int | None = None, extensions: list[Extensions] | None = None, browser_type: PersistentBrowserType | None = None, + proxy_session_id: str | None = None, is_high_priority: bool = False, browser_profile_id: str | None = None, generate_browser_profile: bool = False, @@ -327,6 +328,7 @@ class DefaultPersistentSessionsManager(PersistentSessionsManager): runnable_id=runnable_id, timeout_minutes=timeout_minutes, proxy_location=proxy_location, + proxy_session_id=proxy_session_id, extensions=extensions, browser_type=browser_type, browser_profile_id=browser_profile_id, @@ -356,15 +358,28 @@ class DefaultPersistentSessionsManager(PersistentSessionsManager): url: str | None = None, ) -> None: try: + session = await self.get_session(session_id, organization_id) + if session is None or is_final_status(session.status) or session.completed_at is not None: + LOG.info( + "Session closed before browser launch, skipping browser launch", + browser_session_id=session_id, + ) + return + + launch_proxy_location = session.proxy_location if session.proxy_location is not None else proxy_location + extra_http_headers = app.AGENT_FUNCTION.build_proxy_session_extra_http_headers(session.proxy_session_id) + browser_state = await RealBrowserManager._create_browser_state( - proxy_location=proxy_location, + proxy_location=launch_proxy_location, url=url, organization_id=organization_id, + extra_http_headers=extra_http_headers, ) await browser_state.get_or_create_page( url=url or "about:blank", - proxy_location=proxy_location, + proxy_location=launch_proxy_location, organization_id=organization_id, + extra_http_headers=extra_http_headers, ) session = await self.get_session(session_id, organization_id) diff --git a/skyvern/webeye/persistent_sessions_manager.py b/skyvern/webeye/persistent_sessions_manager.py index dded80fba..a12676e5a 100644 --- a/skyvern/webeye/persistent_sessions_manager.py +++ b/skyvern/webeye/persistent_sessions_manager.py @@ -84,6 +84,7 @@ class PersistentSessionsManager(Protocol): timeout_minutes: int | None = None, extensions: list[Extensions] | None = None, browser_type: PersistentBrowserType | None = None, + proxy_session_id: str | None = None, is_high_priority: bool = False, browser_profile_id: str | None = None, generate_browser_profile: bool = False, diff --git a/skyvern/webeye/real_browser_manager.py b/skyvern/webeye/real_browser_manager.py index 4a7d6f1ca..c82bc424b 100644 --- a/skyvern/webeye/real_browser_manager.py +++ b/skyvern/webeye/real_browser_manager.py @@ -23,6 +23,15 @@ from skyvern.webeye.video_utils import finalize_webm LOG = structlog.get_logger() +def _merge_proxy_session_headers( + extra_http_headers: dict[str, str] | None, + proxy_session_id: str | None, +) -> dict[str, str] | None: + if not proxy_session_id: + return extra_http_headers + return app.AGENT_FUNCTION.merge_proxy_session_extra_http_headers(extra_http_headers, proxy_session_id) + + class RealBrowserManager(BrowserManager): def __init__(self) -> None: self.pages: dict[str, BrowserState] = {} @@ -120,20 +129,23 @@ class RealBrowserManager(BrowserManager): else: LOG.warning("Browser state has no page", workflow_run_id=task.workflow_run_id) + proxy_location = task.proxy_location + extra_http_headers = task.extra_http_headers if browser_state is None: LOG.info("Creating browser state for task", task_id=task.task_id) - proxy_location = task.proxy_location if browser_session_id and task.organization_id: session = await app.PERSISTENT_SESSIONS_MANAGER.get_session(browser_session_id, task.organization_id) - if session and session.proxy_location is not None: - proxy_location = session.proxy_location + if session: + if session.proxy_location is not None: + proxy_location = session.proxy_location + extra_http_headers = _merge_proxy_session_headers(extra_http_headers, session.proxy_session_id) browser_state = await self._create_browser_state( proxy_location=proxy_location, url=task.url, task_id=task.task_id, workflow_permanent_id=task.workflow_permanent_id, organization_id=task.organization_id, - extra_http_headers=task.extra_http_headers, + extra_http_headers=extra_http_headers, cdp_connect_headers=task.cdp_connect_headers, browser_address=task.browser_address, ) @@ -152,11 +164,11 @@ class RealBrowserManager(BrowserManager): # This will make sure browser_state.page is not None. await browser_state.get_or_create_page( url=task.url, - proxy_location=task.proxy_location, + proxy_location=proxy_location, task_id=task.task_id, workflow_permanent_id=task.workflow_permanent_id, organization_id=task.organization_id, - extra_http_headers=task.extra_http_headers, + extra_http_headers=extra_http_headers, cdp_connect_headers=task.cdp_connect_headers, browser_address=task.browser_address, ) @@ -235,26 +247,29 @@ class RealBrowserManager(BrowserManager): else: LOG.warning("Browser state has no page", workflow_run_id=workflow_run.workflow_run_id) + proxy_location = workflow_run.proxy_location + extra_http_headers = workflow_run.extra_http_headers if browser_state is None: LOG.info( "Creating browser state for workflow run", sampling=True, workflow_run_id=workflow_run.workflow_run_id, ) - proxy_location = workflow_run.proxy_location if browser_session_id and workflow_run.organization_id: session = await app.PERSISTENT_SESSIONS_MANAGER.get_session( browser_session_id, workflow_run.organization_id ) - if session and session.proxy_location is not None: - proxy_location = session.proxy_location + if session: + if session.proxy_location is not None: + proxy_location = session.proxy_location + extra_http_headers = _merge_proxy_session_headers(extra_http_headers, session.proxy_session_id) browser_state = await self._create_browser_state( proxy_location=proxy_location, url=url, workflow_run_id=workflow_run.workflow_run_id, workflow_permanent_id=workflow_run.workflow_permanent_id, organization_id=workflow_run.organization_id, - extra_http_headers=workflow_run.extra_http_headers, + extra_http_headers=extra_http_headers, cdp_connect_headers=workflow_run.cdp_connect_headers, browser_address=workflow_run.browser_address, browser_profile_id=browser_profile_id, @@ -281,11 +296,11 @@ class RealBrowserManager(BrowserManager): # script) performs the first goto itself, avoiding a redundant page load. await browser_state.get_or_create_page( url=url if navigate else None, - proxy_location=workflow_run.proxy_location, + proxy_location=proxy_location, workflow_run_id=workflow_run.workflow_run_id, workflow_permanent_id=workflow_run.workflow_permanent_id, organization_id=workflow_run.organization_id, - extra_http_headers=workflow_run.extra_http_headers, + extra_http_headers=extra_http_headers, cdp_connect_headers=workflow_run.cdp_connect_headers, browser_address=workflow_run.browser_address, browser_profile_id=browser_profile_id, diff --git a/tests/unit/forge/sdk/test_create_browser_profile_from_session_route.py b/tests/unit/forge/sdk/test_create_browser_profile_from_session_route.py index 52441d61f..c6ceb2b8a 100644 --- a/tests/unit/forge/sdk/test_create_browser_profile_from_session_route.py +++ b/tests/unit/forge/sdk/test_create_browser_profile_from_session_route.py @@ -21,6 +21,7 @@ from skyvern.forge.sdk.routes import browser_profiles as browser_profiles_route from skyvern.forge.sdk.schemas.browser_profiles import BrowserProfile, CreateBrowserProfileRequest from skyvern.forge.sdk.schemas.persistent_browser_sessions import PersistentBrowserSession from skyvern.forge.sdk.services import org_auth_service +from skyvern.schemas.runs import ProxyLocation _default_profile_template_candidates = browser_profiles_route._default_browser_profile_template_candidates @@ -204,6 +205,8 @@ def test_create_empty_profile_stores_default_profile_seed( organization_id="org_oss", name="fresh profile", description="blank", + proxy_location=None, + proxy_session_id=None, ) mocks.store_profile.assert_awaited_once() assert captured_directory["path"] @@ -212,6 +215,29 @@ def test_create_empty_profile_stores_default_profile_seed( mocks.hard_delete_db_profile.assert_not_awaited() +def test_create_empty_profile_persists_requested_proxy_pin(monkeypatch: pytest.MonkeyPatch) -> None: + client, mocks = _build_client(monkeypatch) + + response = client.post( + "/v1/browser_profiles/", + json={ + "name": "fresh profile", + "proxy_location": ProxyLocation.RESIDENTIAL_ISP, + "proxy_session_id": "abc1234567", + }, + ) + + assert response.status_code == 200 + mocks.create_profile.assert_awaited_once_with( + organization_id="org_oss", + name="fresh profile", + description=None, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="abc1234567", + ) + mocks.store_profile.assert_awaited_once() + + def test_default_profile_template_candidates_come_from_configured_directory( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, @@ -377,6 +403,8 @@ def test_create_profile_route_delegates_workflow_run_source(monkeypatch: pytest. "name": "my profile", "description": "from workflow", "workflow_run_id": "wr_1", + "proxy_location": None, + "proxy_session_id": None, } ] mocks.rate_limit_submit_run.assert_not_awaited() @@ -459,6 +487,48 @@ def test_promote_deletes_source_session_blob(monkeypatch: pytest.MonkeyPatch) -> mocks.delete_profile_blob.assert_awaited_once_with(organization_id="org_oss", profile_id="pbs_1") +def test_promote_inherits_session_proxy_pin(monkeypatch: pytest.MonkeyPatch) -> None: + client, mocks = _build_client(monkeypatch) + mocks.get_session.return_value = _session( + generate_browser_profile=True, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="abc1234567", + ) + mocks.retrieve_profile.return_value = "/tmp/session_dir" + + response = client.post("/v1/browser_profiles/", json={"name": "my profile", "browser_session_id": "pbs_1"}) + + assert response.status_code == 200 + assert mocks.create_profile.await_args.kwargs["proxy_location"] == ProxyLocation.RESIDENTIAL_ISP + assert mocks.create_profile.await_args.kwargs["proxy_session_id"] == "abc1234567" + + +def test_promote_inherits_session_proxy_pin_for_blank_residential_request( + monkeypatch: pytest.MonkeyPatch, +) -> None: + client, mocks = _build_client(monkeypatch) + mocks.get_session.return_value = _session( + generate_browser_profile=True, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="abc1234567", + ) + mocks.retrieve_profile.return_value = "/tmp/session_dir" + + response = client.post( + "/v1/browser_profiles/", + json={ + "name": "my profile", + "browser_session_id": "pbs_1", + "proxy_location": ProxyLocation.RESIDENTIAL_ISP, + "proxy_session_id": None, + }, + ) + + assert response.status_code == 200 + assert mocks.create_profile.await_args.kwargs["proxy_location"] == ProxyLocation.RESIDENTIAL_ISP + assert mocks.create_profile.await_args.kwargs["proxy_session_id"] == "abc1234567" + + def test_failed_promote_does_not_delete_source_blob(monkeypatch: pytest.MonkeyPatch) -> None: client, mocks = _build_client(monkeypatch) mocks.get_session.return_value = _session(generate_browser_profile=True) diff --git a/tests/unit/forge/sdk/test_update_browser_profile_route.py b/tests/unit/forge/sdk/test_update_browser_profile_route.py index d87d0066f..2684b9466 100644 --- a/tests/unit/forge/sdk/test_update_browser_profile_route.py +++ b/tests/unit/forge/sdk/test_update_browser_profile_route.py @@ -16,6 +16,8 @@ from skyvern.forge import app as forge_app from skyvern.forge.sdk.routes import routers as routers_module from skyvern.forge.sdk.schemas.browser_profiles import BrowserProfile from skyvern.forge.sdk.services import org_auth_service +from skyvern.schemas.proxy_pinning import generate_proxy_session_id +from skyvern.schemas.runs import ProxyLocation ORG_ID = "org_oss" PROFILE_ID = "bp_test_profile" @@ -26,6 +28,8 @@ def _profile( description: str | None = "A profile", organization_id: str = ORG_ID, profile_id: str = PROFILE_ID, + proxy_location: ProxyLocation | None = None, + proxy_session_id: str | None = None, ) -> BrowserProfile: now = datetime.now(timezone.utc) return BrowserProfile( @@ -34,6 +38,8 @@ def _profile( name=name, description=description, source_browser_type=None, + proxy_location=proxy_location, + proxy_session_id=proxy_session_id, created_at=now, modified_at=now, deleted_at=None, @@ -124,6 +130,132 @@ def test_update_browser_profile_name_and_description(monkeypatch: pytest.MonkeyP ) +def test_update_browser_profile_enables_proxy_pin_with_generated_session_id(monkeypatch: pytest.MonkeyPatch) -> None: + client, update_browser_profile = _build_client(monkeypatch) + generated_session_id = generate_proxy_session_id(PROFILE_ID) + update_browser_profile.return_value = _profile( + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id=generated_session_id, + ) + + response = client.patch( + f"/v1/browser_profiles/{PROFILE_ID}", + json={"proxy_location": "RESIDENTIAL_ISP", "proxy_session_id": None}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["proxy_location"] == "RESIDENTIAL_ISP" + assert body["proxy_session_id"] == generated_session_id + update_browser_profile.assert_awaited_once_with( + profile_id=PROFILE_ID, + organization_id=ORG_ID, + name=None, + description=None, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + +def test_update_browser_profile_rotates_proxy_pin(monkeypatch: pytest.MonkeyPatch) -> None: + client, update_browser_profile = _build_client(monkeypatch) + rotated_session_id = generate_proxy_session_id(PROFILE_ID) + update_browser_profile.return_value = _profile( + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id=rotated_session_id, + ) + + response = client.patch( + f"/v1/browser_profiles/{PROFILE_ID}", + json={"proxy_location": "RESIDENTIAL_ISP", "rotate_proxy_session_id": True}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["proxy_location"] == "RESIDENTIAL_ISP" + assert body["proxy_session_id"] == rotated_session_id + update_browser_profile.assert_awaited_once_with( + profile_id=PROFILE_ID, + organization_id=ORG_ID, + name=None, + description=None, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + rotate_proxy_session_id=True, + ) + + +def test_update_browser_profile_sets_explicit_proxy_pin(monkeypatch: pytest.MonkeyPatch) -> None: + client, update_browser_profile = _build_client(monkeypatch) + update_browser_profile.return_value = _profile( + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="abc1234567", + ) + + response = client.patch( + f"/v1/browser_profiles/{PROFILE_ID}", + json={"proxy_location": "RESIDENTIAL_ISP", "proxy_session_id": "abc1234567"}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["proxy_location"] == "RESIDENTIAL_ISP" + assert body["proxy_session_id"] == "abc1234567" + update_browser_profile.assert_awaited_once_with( + profile_id=PROFILE_ID, + organization_id=ORG_ID, + name=None, + description=None, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="abc1234567", + ) + + +def test_update_browser_profile_stores_advanced_proxy_identity_verbatim(monkeypatch: pytest.MonkeyPatch) -> None: + client, update_browser_profile = _build_client(monkeypatch) + advanced_key = "support-shared-login@example.com" + update_browser_profile.return_value = _profile( + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id=advanced_key, + ) + + response = client.patch( + f"/v1/browser_profiles/{PROFILE_ID}", + json={"proxy_session_id": advanced_key}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["proxy_location"] == "RESIDENTIAL_ISP" + assert body["proxy_session_id"] == advanced_key + update_browser_profile.assert_awaited_once_with( + profile_id=PROFILE_ID, + organization_id=ORG_ID, + name=None, + description=None, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id=advanced_key, + ) + + +def test_update_browser_profile_clears_proxy_pin(monkeypatch: pytest.MonkeyPatch) -> None: + client, update_browser_profile = _build_client(monkeypatch) + update_browser_profile.return_value = _profile(proxy_location=None, proxy_session_id=None) + + response = client.patch( + f"/v1/browser_profiles/{PROFILE_ID}", + json={"proxy_location": None}, + ) + + assert response.status_code == 200 + update_browser_profile.assert_awaited_once_with( + profile_id=PROFILE_ID, + organization_id=ORG_ID, + name=None, + description=None, + proxy_location=None, + proxy_session_id=None, + ) + + def test_update_browser_profile_not_found(monkeypatch: pytest.MonkeyPatch) -> None: client, update_browser_profile = _build_client(monkeypatch) update_browser_profile.side_effect = BrowserProfileNotFound(profile_id=PROFILE_ID, organization_id=ORG_ID) diff --git a/tests/unit/test_client_proxy_pin_types.py b/tests/unit/test_client_proxy_pin_types.py new file mode 100644 index 000000000..53a331b47 --- /dev/null +++ b/tests/unit/test_client_proxy_pin_types.py @@ -0,0 +1,104 @@ +import datetime as dt +from types import SimpleNamespace +from typing import cast +from unittest.mock import Mock + +from skyvern.client.core.client_wrapper import SyncClientWrapper +from skyvern.client.raw_client import RawSkyvern +from skyvern.client.types.browser_profile import BrowserProfile +from skyvern.client.types.credential_response import CredentialResponse +from skyvern.client.types.non_empty_password_credential import NonEmptyPasswordCredential +from skyvern.client.types.password_credential_response import PasswordCredentialResponse + + +def test_client_browser_profile_exposes_proxy_pin_fields() -> None: + profile = BrowserProfile( + browser_profile_id="bprof_123", + organization_id="org_123", + name="Pinned profile", + proxy_location="RESIDENTIAL_ISP", + proxy_session_id="abc1234567", + created_at=dt.datetime(2026, 1, 1, tzinfo=dt.UTC), + modified_at=dt.datetime(2026, 1, 2, tzinfo=dt.UTC), + ) + + assert profile.proxy_location == "RESIDENTIAL_ISP" + assert profile.proxy_session_id == "abc1234567" + + +def test_client_credential_response_exposes_proxy_pin_fields() -> None: + response = CredentialResponse( + credential_id="cred_123", + credential=PasswordCredentialResponse(username="user@example.com"), + credential_type="password", + name="Pinned credential", + proxy_location="RESIDENTIAL_ISP", + proxy_session_id="abc1234567", + ) + + assert response.proxy_location == "RESIDENTIAL_ISP" + assert response.proxy_session_id == "abc1234567" + + +def _response(body: dict[str, object]) -> SimpleNamespace: + return SimpleNamespace( + status_code=200, + headers={}, + text="", + json=lambda: body, + ) + + +def _raw_client(request: Mock) -> RawSkyvern: + client_wrapper = cast(SyncClientWrapper, SimpleNamespace(httpx_client=SimpleNamespace(request=request))) + return RawSkyvern(client_wrapper=client_wrapper) + + +def test_raw_client_update_browser_profile_sends_rotate_proxy_session_id() -> None: + request = Mock( + return_value=_response( + { + "browser_profile_id": "bp_123", + "organization_id": "org_123", + "name": "Pinned profile", + "proxy_location": "RESIDENTIAL_ISP", + "proxy_session_id": "abc1234567", + "created_at": "2026-01-01T00:00:00Z", + "modified_at": "2026-01-02T00:00:00Z", + } + ) + ) + + _raw_client(request).update_browser_profile( + "bp_123", + proxy_location="RESIDENTIAL_ISP", + rotate_proxy_session_id=True, + ) + + assert request.call_args.kwargs["json"]["rotate_proxy_session_id"] is True + + +def test_raw_client_update_credential_sends_rotate_proxy_session_id() -> None: + request = Mock( + return_value=_response( + { + "credential_id": "cred_123", + "credential": {"username": "user@example.com"}, + "credential_type": "password", + "name": "Pinned credential", + "proxy_location": "RESIDENTIAL_ISP", + "proxy_session_id": "abc1234567", + } + ) + ) + + _raw_client(request).update_credential( + "cred_123", + name="Pinned credential", + credential_type="password", + credential=NonEmptyPasswordCredential(username="user@example.com", password="pw"), + proxy_location="RESIDENTIAL_ISP", + rotate_proxy_session_id=True, + ) + + assert request.call_args.kwargs["json"]["rotate_proxy_session_id"] is True diff --git a/tests/unit/test_credential_test_trigger_type.py b/tests/unit/test_credential_test_trigger_type.py index 5c1d4b420..3126082c3 100644 --- a/tests/unit/test_credential_test_trigger_type.py +++ b/tests/unit/test_credential_test_trigger_type.py @@ -12,6 +12,7 @@ from skyvern.forge.sdk.schemas.credentials import CredentialType from skyvern.forge.sdk.schemas.credentials import TestCredentialRequest as CredentialTestRequest from skyvern.forge.sdk.schemas.credentials import TestLoginRequest as LoginTestRequest from skyvern.forge.sdk.services import org_auth_service +from skyvern.schemas.runs import ProxyLocation USER_AGENT_CASES = [ ("skyvern-ui", WorkflowRunTriggerType.manual), @@ -43,6 +44,15 @@ def _patch_executor(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(credentials.AsyncExecutorFactory, "get_executor", lambda: executor) +def _patch_cloud_proxy_session_headers(monkeypatch: pytest.MonkeyPatch) -> None: + agent_function = SimpleNamespace( + build_proxy_session_extra_http_headers=lambda proxy_session_id: {"dedicated-ip": proxy_session_id} + if proxy_session_id + else None + ) + monkeypatch.setattr(credentials.app, "AGENT_FUNCTION", agent_function) + + @pytest.mark.parametrize("x_user_agent, expected", USER_AGENT_CASES) @pytest.mark.asyncio async def test_test_credential_trigger_type_from_user_agent( @@ -58,11 +68,14 @@ async def test_test_credential_trigger_type_from_user_agent( browser_profile_id=None, totp_identifier=None, name="Test Credential", + proxy_location=None, + proxy_session_id=None, ) ) ) monkeypatch.setattr(credentials.app, "DATABASE", SimpleNamespace(credentials=mock_credentials)) monkeypatch.setattr(credentials.app, "WORKFLOW_SERVICE", _workflow_service(setup_mock)) + _patch_cloud_proxy_session_headers(monkeypatch) _patch_executor(monkeypatch) response = await credentials.test_credential( @@ -78,6 +91,39 @@ async def test_test_credential_trigger_type_from_user_agent( assert response.workflow_run_id == "wr_test" +@pytest.mark.asyncio +async def test_test_credential_seeds_proxy_pin_workflow_request(monkeypatch: pytest.MonkeyPatch) -> None: + setup_mock = _setup_mock() + mock_credentials = SimpleNamespace( + get_credential=AsyncMock( + return_value=SimpleNamespace( + credential_type=CredentialType.PASSWORD, + browser_profile_id=None, + totp_identifier=None, + name="Test Credential", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="abc1234567", + ) + ) + ) + monkeypatch.setattr(credentials.app, "DATABASE", SimpleNamespace(credentials=mock_credentials)) + monkeypatch.setattr(credentials.app, "WORKFLOW_SERVICE", _workflow_service(setup_mock)) + _patch_cloud_proxy_session_headers(monkeypatch) + _patch_executor(monkeypatch) + + await credentials.test_credential( + background_tasks=BackgroundTasks(), + credential_id="cred_test", + data=CredentialTestRequest(url="https://example.com/login", save_browser_profile=False), + current_org=SimpleNamespace(organization_id="org_test"), + x_user_agent=None, + ) + + workflow_request = setup_mock.call_args.kwargs["workflow_request"] + assert workflow_request.proxy_location == ProxyLocation.RESIDENTIAL_ISP + assert workflow_request.extra_http_headers == {"dedicated-ip": "abc1234567"} + + @pytest.mark.parametrize("x_user_agent, expected", USER_AGENT_CASES) @pytest.mark.asyncio async def test_test_login_trigger_type_from_user_agent( @@ -111,6 +157,50 @@ async def test_test_login_trigger_type_from_user_agent( await asyncio.sleep(0) +@pytest.mark.asyncio +async def test_test_login_seeds_proxy_pin_workflow_request(monkeypatch: pytest.MonkeyPatch) -> None: + setup_mock = _setup_mock() + vault_service = SimpleNamespace( + create_credential=AsyncMock( + return_value=SimpleNamespace(vault_type=None, credential_id="cred_test", totp_identifier=None) + ) + ) + mock_credentials = SimpleNamespace( + update_credential=AsyncMock( + return_value=SimpleNamespace( + vault_type=None, + credential_id="cred_test", + totp_identifier=None, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="abc1234567", + ) + ) + ) + monkeypatch.setattr(credentials, "_get_credential_vault_service", AsyncMock(return_value=vault_service)) + monkeypatch.setattr(credentials, "_create_browser_profile_after_workflow", AsyncMock()) + monkeypatch.setattr(credentials.app, "DATABASE", SimpleNamespace(credentials=mock_credentials)) + monkeypatch.setattr(credentials.app, "WORKFLOW_SERVICE", _workflow_service(setup_mock)) + _patch_cloud_proxy_session_headers(monkeypatch) + _patch_executor(monkeypatch) + + await credentials.test_login( + background_tasks=BackgroundTasks(), + data=LoginTestRequest( + url="https://example.com/login", + username="user@example.com", + password="pw", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ), + current_org=SimpleNamespace(organization_id="org_test"), + x_user_agent=None, + ) + + workflow_request = setup_mock.call_args.kwargs["workflow_request"] + assert workflow_request.proxy_location == ProxyLocation.RESIDENTIAL_ISP + assert workflow_request.extra_http_headers == {"dedicated-ip": "abc1234567"} + await asyncio.sleep(0) + + def _auth_override() -> SimpleNamespace: return SimpleNamespace(organization_id="org_test") @@ -140,11 +230,14 @@ def test_test_credential_endpoint_wires_user_agent_header( browser_profile_id=None, totp_identifier=None, name="Test Credential", + proxy_location=None, + proxy_session_id=None, ) ) ) monkeypatch.setattr(credentials.app, "DATABASE", SimpleNamespace(credentials=mock_credentials)) monkeypatch.setattr(credentials.app, "WORKFLOW_SERVICE", _workflow_service(setup_mock)) + _patch_cloud_proxy_session_headers(monkeypatch) _patch_executor(monkeypatch) app = FastAPI() diff --git a/tests/unit/test_credential_update_params.py b/tests/unit/test_credential_update_params.py index 34f769b87..10894b439 100644 --- a/tests/unit/test_credential_update_params.py +++ b/tests/unit/test_credential_update_params.py @@ -1,11 +1,31 @@ """Tests that update_credential() accepts user_context and save_browser_session_intent on CredentialRepository.""" -from unittest.mock import MagicMock, patch +from datetime import datetime +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from fastapi import BackgroundTasks +from skyvern.forge import app as forge_app +from skyvern.forge.sdk.db.repositories.browser_sessions import BrowserSessionsRepository from skyvern.forge.sdk.db.repositories.credentials import CredentialRepository +from skyvern.forge.sdk.routes import credentials as credentials_routes +from skyvern.forge.sdk.schemas.credentials import ( + CreateCredentialRequest, + Credential, + CredentialType, + CredentialVaultType, +) +from skyvern.forge.sdk.services.credential.credential_vault_service import CredentialVaultService +from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus +from skyvern.schemas.proxy_pinning import apply_proxy_pin_update as _apply_proxy_pin_update +from skyvern.schemas.proxy_pinning import ( + generate_proxy_session_id, + is_proxy_session_id, +) +from skyvern.schemas.runs import ProxyLocation from tests.unit.conftest import MockAsyncSessionCtx, make_mock_session @@ -14,9 +34,108 @@ def _make_credential_repo(mock_credential: MagicMock) -> CredentialRepository: return CredentialRepository(session_factory=lambda: MockAsyncSessionCtx(mock_session)) +def _make_browser_sessions_repo(mock_browser_profile: MagicMock) -> BrowserSessionsRepository: + mock_session = make_mock_session(mock_browser_profile) + return BrowserSessionsRepository(session_factory=lambda: MockAsyncSessionCtx(mock_session)) + + # --- CredentialRepository tests --- +@pytest.mark.asyncio +async def test_credential_vault_service_create_db_credential_passes_proxy_pin( + monkeypatch: pytest.MonkeyPatch, +) -> None: + create_credential = AsyncMock(return_value=MagicMock()) + monkeypatch.setattr(forge_app.DATABASE.credentials, "create_credential", create_credential) + + data = CreateCredentialRequest( + name="test", + credential_type=CredentialType.PASSWORD, + credential={"username": "user@example.com", "password": "pw"}, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + await CredentialVaultService._create_db_credential( + organization_id="org_123", + data=data, + item_id="item_123", + vault_type=CredentialVaultType.AZURE_VAULT, + ) + + create_credential.assert_awaited_once() + assert create_credential.await_args.kwargs["proxy_location"] == ProxyLocation.RESIDENTIAL_ISP + assert create_credential.await_args.kwargs["proxy_session_id"] is None + + +@pytest.mark.asyncio +async def test_create_credential_response_includes_generated_proxy_pin(monkeypatch: pytest.MonkeyPatch) -> None: + proxy_session_id = generate_proxy_session_id("cred_123") + stored_credential = Credential( + credential_id="cred_123", + organization_id="org_123", + name="test", + vault_type=CredentialVaultType.AZURE_VAULT, + item_id="item_123", + credential_type=CredentialType.PASSWORD, + username="user@example.com", + card_last4=None, + card_brand=None, + secret_label=None, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id=proxy_session_id, + created_at=datetime(2026, 1, 1), + modified_at=datetime(2026, 1, 1), + ) + vault_service = SimpleNamespace(create_credential=AsyncMock(return_value=stored_credential)) + monkeypatch.setattr(credentials_routes, "_get_credential_vault_service", AsyncMock(return_value=vault_service)) + + data = CreateCredentialRequest( + name="test", + credential_type=CredentialType.PASSWORD, + credential={"username": "user@example.com", "password": "pw"}, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + response = await credentials_routes.create_credential( + background_tasks=BackgroundTasks(), + data=data, + current_org=SimpleNamespace(organization_id="org_123"), + ) + + assert response.proxy_location == ProxyLocation.RESIDENTIAL_ISP + assert response.proxy_session_id == proxy_session_id + + +@pytest.mark.asyncio +async def test_repo_create_credential_clears_incompatible_proxy_pin() -> None: + mock_session = AsyncMock() + mock_session.add = MagicMock() + mock_session.flush = AsyncMock() + mock_session.commit = AsyncMock() + mock_session.refresh = AsyncMock() + repo = CredentialRepository(session_factory=lambda: MockAsyncSessionCtx(mock_session)) + + with patch("skyvern.forge.sdk.schemas.credentials.Credential.model_validate", return_value=MagicMock()): + await repo.create_credential( + organization_id="org_123", + name="test", + vault_type=CredentialVaultType.AZURE_VAULT, + item_id="item_123", + credential_type=CredentialType.PASSWORD, + username="user@example.com", + totp_type="none", + card_last4=None, + card_brand=None, + proxy_location=ProxyLocation.NONE, + proxy_session_id="abc1234567", + ) + + stored_credential = mock_session.add.call_args.args[0] + assert stored_credential.proxy_location == ProxyLocation.NONE.value + assert stored_credential.proxy_session_id is None + + @pytest.mark.asyncio async def test_repo_update_credential_accepts_user_context() -> None: mock_credential = MagicMock() @@ -67,3 +186,454 @@ async def test_repo_update_credential_unset_params_not_applied() -> None: assert mock_credential.user_context == "existing" assert mock_credential.save_browser_session_intent is True + + +@pytest.mark.asyncio +async def test_repo_update_credential_generates_proxy_session_id_for_proxy_location() -> None: + mock_credential = MagicMock() + mock_credential.name = "test" + mock_credential.proxy_location = None + mock_credential.proxy_session_id = None + repo = _make_credential_repo(mock_credential) + + with patch("skyvern.forge.sdk.schemas.credentials.Credential.model_validate", return_value=MagicMock()): + await repo.update_credential( + credential_id="cred_123", + organization_id="org_123", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + assert mock_credential.proxy_location == ProxyLocation.RESIDENTIAL_ISP.value + assert mock_credential.proxy_session_id is not None + assert is_proxy_session_id(mock_credential.proxy_session_id) + + +@pytest.mark.asyncio +async def test_repo_update_credential_rotates_proxy_session_id_when_requested() -> None: + mock_credential = MagicMock() + mock_credential.name = "test" + mock_credential.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_credential.proxy_session_id = "existing-pin" + repo = _make_credential_repo(mock_credential) + + with patch("skyvern.forge.sdk.schemas.credentials.Credential.model_validate", return_value=MagicMock()): + await repo.update_credential( + credential_id="cred_123", + organization_id="org_123", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + rotate_proxy_session_id=True, + ) + + assert mock_credential.proxy_location == ProxyLocation.RESIDENTIAL_ISP.value + assert mock_credential.proxy_session_id is not None + assert mock_credential.proxy_session_id != "existing-pin" + assert is_proxy_session_id(mock_credential.proxy_session_id) + + +@pytest.mark.asyncio +async def test_repo_update_credential_vault_data_persists_proxy_pin() -> None: + mock_credential = MagicMock() + mock_credential.name = "test" + mock_credential.proxy_location = None + mock_credential.proxy_session_id = None + repo = _make_credential_repo(mock_credential) + + with patch("skyvern.forge.sdk.schemas.credentials.Credential.model_validate", return_value=MagicMock()): + await repo.update_credential_vault_data( + credential_id="cred_123", + organization_id="org_123", + item_id="item_123", + name="test", + credential_type="password", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + assert mock_credential.proxy_location == ProxyLocation.RESIDENTIAL_ISP.value + assert mock_credential.proxy_session_id is not None + assert is_proxy_session_id(mock_credential.proxy_session_id) + + +@pytest.mark.asyncio +async def test_repo_update_credential_preserves_existing_proxy_pin_on_resave() -> None: + mock_credential = MagicMock() + mock_credential.name = "test" + mock_credential.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_credential.proxy_session_id = "support-shared-login@example.com" + repo = _make_credential_repo(mock_credential) + + with patch("skyvern.forge.sdk.schemas.credentials.Credential.model_validate", return_value=MagicMock()): + await repo.update_credential( + credential_id="cred_123", + organization_id="org_123", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + assert mock_credential.proxy_location == ProxyLocation.RESIDENTIAL_ISP.value + assert mock_credential.proxy_session_id == "support-shared-login@example.com" + + +@pytest.mark.asyncio +async def test_repo_update_credential_explicit_null_session_id_clears_location() -> None: + mock_credential = MagicMock() + mock_credential.name = "test" + mock_credential.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_credential.proxy_session_id = "abc1234567" + repo = _make_credential_repo(mock_credential) + + with patch("skyvern.forge.sdk.schemas.credentials.Credential.model_validate", return_value=MagicMock()): + await repo.update_credential( + credential_id="cred_123", + organization_id="org_123", + proxy_session_id=None, + ) + + assert mock_credential.proxy_location is None + assert mock_credential.proxy_session_id is None + + +@pytest.mark.asyncio +async def test_repo_update_credential_non_isp_location_clears_stale_proxy_pin() -> None: + mock_credential = MagicMock() + mock_credential.name = "test" + mock_credential.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_credential.proxy_session_id = "abc1234567" + repo = _make_credential_repo(mock_credential) + + with patch("skyvern.forge.sdk.schemas.credentials.Credential.model_validate", return_value=MagicMock()): + await repo.update_credential( + credential_id="cred_123", + organization_id="org_123", + proxy_location=ProxyLocation.RESIDENTIAL, + ) + + assert mock_credential.proxy_location == ProxyLocation.RESIDENTIAL.value + assert mock_credential.proxy_session_id is None + + +@pytest.mark.asyncio +async def test_repo_update_browser_profile_preserves_existing_proxy_pin_on_resave() -> None: + mock_profile = MagicMock() + mock_profile.name = "Profile" + mock_profile.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_profile.proxy_session_id = "support-shared-login@example.com" + repo = _make_browser_sessions_repo(mock_profile) + + with patch("skyvern.forge.sdk.schemas.browser_profiles.BrowserProfile.model_validate", return_value=MagicMock()): + await repo.update_browser_profile( + profile_id="bp_123", + organization_id="org_123", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + assert mock_profile.proxy_location == ProxyLocation.RESIDENTIAL_ISP.value + assert mock_profile.proxy_session_id == "support-shared-login@example.com" + + +@pytest.mark.asyncio +async def test_repo_update_browser_profile_rotates_proxy_session_id_when_requested() -> None: + mock_profile = MagicMock() + mock_profile.name = "Profile" + mock_profile.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_profile.proxy_session_id = "existing-pin" + repo = _make_browser_sessions_repo(mock_profile) + + with patch("skyvern.forge.sdk.schemas.browser_profiles.BrowserProfile.model_validate", return_value=MagicMock()): + await repo.update_browser_profile( + profile_id="bp_123", + organization_id="org_123", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + rotate_proxy_session_id=True, + ) + + assert mock_profile.proxy_location == ProxyLocation.RESIDENTIAL_ISP.value + assert mock_profile.proxy_session_id is not None + assert mock_profile.proxy_session_id != "existing-pin" + assert is_proxy_session_id(mock_profile.proxy_session_id) + + +@pytest.mark.asyncio +async def test_repo_update_browser_profile_explicit_null_session_id_clears_location() -> None: + mock_profile = MagicMock() + mock_profile.name = "Profile" + mock_profile.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_profile.proxy_session_id = "abc1234567" + repo = _make_browser_sessions_repo(mock_profile) + + with patch("skyvern.forge.sdk.schemas.browser_profiles.BrowserProfile.model_validate", return_value=MagicMock()): + await repo.update_browser_profile( + profile_id="bp_123", + organization_id="org_123", + proxy_session_id=None, + ) + + assert mock_profile.proxy_location is None + assert mock_profile.proxy_session_id is None + + +@pytest.mark.asyncio +async def test_repo_update_browser_profile_non_isp_location_clears_stale_proxy_pin() -> None: + mock_profile = MagicMock() + mock_profile.name = "Profile" + mock_profile.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_profile.proxy_session_id = "abc1234567" + repo = _make_browser_sessions_repo(mock_profile) + + with patch("skyvern.forge.sdk.schemas.browser_profiles.BrowserProfile.model_validate", return_value=MagicMock()): + await repo.update_browser_profile( + profile_id="bp_123", + organization_id="org_123", + proxy_location=ProxyLocation.RESIDENTIAL, + ) + + assert mock_profile.proxy_location == ProxyLocation.RESIDENTIAL.value + assert mock_profile.proxy_session_id is None + + +@pytest.mark.asyncio +async def test_repo_create_browser_profile_clears_incompatible_proxy_pin() -> None: + mock_session = AsyncMock() + mock_session.add = MagicMock() + mock_session.commit = AsyncMock() + mock_session.refresh = AsyncMock() + repo = BrowserSessionsRepository(session_factory=lambda: MockAsyncSessionCtx(mock_session)) + + with patch("skyvern.forge.sdk.schemas.browser_profiles.BrowserProfile.model_validate", return_value=MagicMock()): + await repo.create_browser_profile( + organization_id="org_123", + name="Profile", + proxy_location=ProxyLocation.NONE, + proxy_session_id="abc1234567", + ) + + stored_profile = mock_session.add.call_args.args[0] + assert stored_profile.proxy_location == ProxyLocation.NONE.value + assert stored_profile.proxy_session_id is None + + +@pytest.mark.asyncio +async def test_browser_profile_resave_updates_proxy_pin_after_storage_write(monkeypatch: pytest.MonkeyPatch) -> None: + events: list[str] = [] + + async def store_browser_profile(**_kwargs: object) -> None: + events.append("store") + + async def update_browser_profile(**_kwargs: object) -> SimpleNamespace: + events.append("update_profile") + return SimpleNamespace() + + async def touch_browser_profile(**_kwargs: object) -> None: + events.append("touch_profile") + + async def update_credential(**_kwargs: object) -> SimpleNamespace: + events.append("update_credential") + return SimpleNamespace() + + monkeypatch.setattr( + forge_app.DATABASE.workflow_runs, + "get_workflow_run", + AsyncMock(return_value=SimpleNamespace(status=WorkflowRunStatus.completed)), + ) + monkeypatch.setattr(forge_app.STORAGE, "retrieve_browser_session", AsyncMock(return_value="/tmp/session")) + monkeypatch.setattr(forge_app.STORAGE, "store_browser_profile", store_browser_profile) + monkeypatch.setattr( + forge_app.DATABASE.credentials, + "get_credential", + AsyncMock( + return_value=SimpleNamespace( + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="abc1234567", + ) + ), + ) + monkeypatch.setattr(forge_app.DATABASE.credentials, "update_credential", update_credential) + monkeypatch.setattr( + forge_app.DATABASE.browser_sessions, + "get_browser_profile", + AsyncMock(return_value=SimpleNamespace(browser_profile_id="bp_existing")), + ) + monkeypatch.setattr(forge_app.DATABASE.browser_sessions, "update_browser_profile", update_browser_profile) + monkeypatch.setattr(forge_app.DATABASE.browser_sessions, "touch_browser_profile", touch_browser_profile) + + await credentials_routes._create_browser_profile_after_workflow( + credential_id="cred_123", + workflow_run_id="wr_123", + workflow_id="wf_123", + workflow_permanent_id="wp_123", + organization_id="org_123", + credential_name="test", + test_url="https://example.com/login", + existing_browser_profile_id="bp_existing", + ) + + assert events == ["store", "update_profile", "touch_profile", "update_credential"] + + +@pytest.mark.asyncio +async def test_browser_profile_resave_leaves_existing_profile_pin_when_credential_unpinned( + monkeypatch: pytest.MonkeyPatch, +) -> None: + events: list[str] = [] + + async def store_browser_profile(**_kwargs: object) -> None: + events.append("store") + + async def update_browser_profile(**_kwargs: object) -> SimpleNamespace: + events.append("update_profile") + return SimpleNamespace() + + async def touch_browser_profile(**_kwargs: object) -> None: + events.append("touch_profile") + + async def update_credential(**_kwargs: object) -> SimpleNamespace: + events.append("update_credential") + return SimpleNamespace() + + monkeypatch.setattr( + forge_app.DATABASE.workflow_runs, + "get_workflow_run", + AsyncMock(return_value=SimpleNamespace(status=WorkflowRunStatus.completed)), + ) + monkeypatch.setattr(forge_app.STORAGE, "retrieve_browser_session", AsyncMock(return_value="/tmp/session")) + monkeypatch.setattr(forge_app.STORAGE, "store_browser_profile", store_browser_profile) + monkeypatch.setattr( + forge_app.DATABASE.credentials, + "get_credential", + AsyncMock(return_value=SimpleNamespace(proxy_location=None, proxy_session_id=None)), + ) + monkeypatch.setattr(forge_app.DATABASE.credentials, "update_credential", update_credential) + monkeypatch.setattr( + forge_app.DATABASE.browser_sessions, + "get_browser_profile", + AsyncMock(return_value=SimpleNamespace(browser_profile_id="bp_existing")), + ) + monkeypatch.setattr(forge_app.DATABASE.browser_sessions, "update_browser_profile", update_browser_profile) + monkeypatch.setattr(forge_app.DATABASE.browser_sessions, "touch_browser_profile", touch_browser_profile) + + await credentials_routes._create_browser_profile_after_workflow( + credential_id="cred_123", + workflow_run_id="wr_123", + workflow_id="wf_123", + workflow_permanent_id="wp_123", + organization_id="org_123", + credential_name="test", + test_url="https://example.com/login", + existing_browser_profile_id="bp_existing", + ) + + assert events == ["store", "touch_profile", "update_credential"] + + +@pytest.mark.asyncio +async def test_browser_profile_resave_preserves_different_existing_profile_pin( + monkeypatch: pytest.MonkeyPatch, +) -> None: + events: list[str] = [] + + async def store_browser_profile(**_kwargs: object) -> None: + events.append("store") + + async def update_browser_profile(**_kwargs: object) -> SimpleNamespace: + events.append("update_profile") + return SimpleNamespace() + + async def touch_browser_profile(**_kwargs: object) -> None: + events.append("touch_profile") + + async def update_credential(**_kwargs: object) -> SimpleNamespace: + events.append("update_credential") + return SimpleNamespace() + + monkeypatch.setattr( + forge_app.DATABASE.workflow_runs, + "get_workflow_run", + AsyncMock(return_value=SimpleNamespace(status=WorkflowRunStatus.completed)), + ) + monkeypatch.setattr(forge_app.STORAGE, "retrieve_browser_session", AsyncMock(return_value="/tmp/session")) + monkeypatch.setattr(forge_app.STORAGE, "store_browser_profile", store_browser_profile) + monkeypatch.setattr( + forge_app.DATABASE.credentials, + "get_credential", + AsyncMock( + return_value=SimpleNamespace( + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id="credential-pin", + ) + ), + ) + monkeypatch.setattr(forge_app.DATABASE.credentials, "update_credential", update_credential) + monkeypatch.setattr( + forge_app.DATABASE.browser_sessions, + "get_browser_profile", + AsyncMock(return_value=SimpleNamespace(browser_profile_id="bp_existing", proxy_session_id="profile-pin")), + ) + monkeypatch.setattr(forge_app.DATABASE.browser_sessions, "update_browser_profile", update_browser_profile) + monkeypatch.setattr(forge_app.DATABASE.browser_sessions, "touch_browser_profile", touch_browser_profile) + + await credentials_routes._create_browser_profile_after_workflow( + credential_id="cred_123", + workflow_run_id="wr_123", + workflow_id="wf_123", + workflow_permanent_id="wp_123", + organization_id="org_123", + credential_name="test", + test_url="https://example.com/login", + existing_browser_profile_id="bp_existing", + ) + + assert events == ["store", "touch_profile", "update_credential"] + + +def test_credential_route_treats_null_advanced_key_as_no_explicit_identity() -> None: + update_kwargs: dict[str, object] = {} + + _apply_proxy_pin_update( + update_kwargs, + proxy_location_was_set=True, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id_was_set=True, + proxy_session_id=None, + ) + + assert update_kwargs == {"proxy_location": ProxyLocation.RESIDENTIAL_ISP} + + +def test_credential_route_passes_rotate_proxy_session_id_intent() -> None: + update_kwargs: dict[str, object] = {} + + _apply_proxy_pin_update( + update_kwargs, + proxy_location_was_set=True, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + proxy_session_id_was_set=False, + proxy_session_id=None, + rotate_proxy_session_id=True, + ) + + assert update_kwargs == { + "proxy_location": ProxyLocation.RESIDENTIAL_ISP, + "rotate_proxy_session_id": True, + } + + +def test_generate_proxy_session_id_rejects_empty_entity_id() -> None: + with pytest.raises(ValueError, match="empty entity id"): + generate_proxy_session_id(" ") + + +@pytest.mark.asyncio +async def test_repo_update_credential_clears_proxy_session_id_with_proxy_location() -> None: + mock_credential = MagicMock() + mock_credential.name = "test" + mock_credential.proxy_location = ProxyLocation.RESIDENTIAL_ISP.value + mock_credential.proxy_session_id = "abc1234567" + repo = _make_credential_repo(mock_credential) + + with patch("skyvern.forge.sdk.schemas.credentials.Credential.model_validate", return_value=MagicMock()): + await repo.update_credential( + credential_id="cred_123", + organization_id="org_123", + proxy_location=None, + ) + + assert mock_credential.proxy_location is None + assert mock_credential.proxy_session_id is None diff --git a/tests/unit/test_login_block_credential_proxy_pin.py b/tests/unit/test_login_block_credential_proxy_pin.py new file mode 100644 index 000000000..1dc177c73 --- /dev/null +++ b/tests/unit/test_login_block_credential_proxy_pin.py @@ -0,0 +1,200 @@ +from datetime import datetime, timezone +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest + +from skyvern.forge.sdk.workflow.models.parameter import CredentialParameter +from skyvern.forge.sdk.workflow.service import WorkflowService +from skyvern.schemas.runs import ProxyLocation + + +def _credential_parameter(credential_id: str = "cred_1") -> CredentialParameter: + now = datetime.now(timezone.utc) + return CredentialParameter( + key="login", + credential_parameter_id="cp_1", + workflow_id="wf_1", + credential_id=credential_id, + created_at=now, + modified_at=now, + ) + + +def _proxy_header_hook() -> SimpleNamespace: + return SimpleNamespace( + has_proxy_session_extra_http_headers=lambda headers: bool(headers and "dedicated-ip" in headers), + merge_proxy_session_extra_http_headers=lambda headers, proxy_session_id: { + **dict(headers or {}), + "dedicated-ip": proxy_session_id, + }, + ) + + +@pytest.mark.asyncio +async def test_login_block_credential_proxy_pin_applies_to_workflow_run() -> None: + service = WorkflowService() + workflow_run = SimpleNamespace(extra_http_headers={"X-Test": "1"}, proxy_location=None) + block = SimpleNamespace(parameters=[_credential_parameter()]) + credential = SimpleNamespace(proxy_session_id="credential-pin", proxy_location=ProxyLocation.RESIDENTIAL_ISP) + + with patch("skyvern.forge.sdk.workflow.service.app") as mock_app: + mock_app.AGENT_FUNCTION = _proxy_header_hook() + mock_app.DATABASE.credentials.get_credential = AsyncMock(return_value=credential) + mock_app.DATABASE.workflow_runs.update_workflow_run = AsyncMock() + + await service._apply_login_block_credential_proxy_pin( + block=block, # type: ignore[arg-type] + workflow_run=workflow_run, # type: ignore[arg-type] + workflow_run_id="wr_1", + organization_id="org_1", + ) + + assert workflow_run.extra_http_headers == {"X-Test": "1", "dedicated-ip": "credential-pin"} + assert workflow_run.proxy_location == ProxyLocation.RESIDENTIAL_ISP + mock_app.DATABASE.workflow_runs.update_workflow_run.assert_awaited_once_with( + workflow_run_id="wr_1", + extra_http_headers={"X-Test": "1", "dedicated-ip": "credential-pin"}, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + +@pytest.mark.asyncio +async def test_login_block_credential_proxy_pin_respects_existing_run_header() -> None: + service = WorkflowService() + workflow_run = SimpleNamespace(extra_http_headers={"dedicated-ip": "run-pin"}, proxy_location=None) + block = SimpleNamespace(parameters=[_credential_parameter()]) + + with patch("skyvern.forge.sdk.workflow.service.app") as mock_app: + mock_app.AGENT_FUNCTION = _proxy_header_hook() + mock_app.DATABASE.credentials.get_credential = AsyncMock() + mock_app.DATABASE.workflow_runs.update_workflow_run = AsyncMock() + + await service._apply_login_block_credential_proxy_pin( + block=block, # type: ignore[arg-type] + workflow_run=workflow_run, # type: ignore[arg-type] + workflow_run_id="wr_1", + organization_id="org_1", + ) + + mock_app.DATABASE.credentials.get_credential.assert_not_called() + mock_app.DATABASE.workflow_runs.update_workflow_run.assert_not_called() + assert workflow_run.extra_http_headers == {"dedicated-ip": "run-pin"} + + +@pytest.mark.asyncio +async def test_login_block_credential_proxy_pin_preserves_run_proxy_location() -> None: + service = WorkflowService() + workflow_run = SimpleNamespace(extra_http_headers=None, proxy_location=ProxyLocation.RESIDENTIAL) + block = SimpleNamespace(parameters=[_credential_parameter()]) + credential = SimpleNamespace(proxy_session_id="credential-pin", proxy_location=ProxyLocation.RESIDENTIAL_ISP) + + with patch("skyvern.forge.sdk.workflow.service.app") as mock_app: + mock_app.AGENT_FUNCTION = _proxy_header_hook() + mock_app.DATABASE.credentials.get_credential = AsyncMock(return_value=credential) + mock_app.DATABASE.workflow_runs.update_workflow_run = AsyncMock() + + await service._apply_login_block_credential_proxy_pin( + block=block, # type: ignore[arg-type] + workflow_run=workflow_run, # type: ignore[arg-type] + workflow_run_id="wr_1", + organization_id="org_1", + ) + + update_kwargs: dict[str, Any] = mock_app.DATABASE.workflow_runs.update_workflow_run.await_args.kwargs + assert update_kwargs == { + "workflow_run_id": "wr_1", + "extra_http_headers": {"dedicated-ip": "credential-pin"}, + } + assert workflow_run.proxy_location == ProxyLocation.RESIDENTIAL + + +@pytest.mark.asyncio +async def test_login_block_credential_proxy_pin_prefers_linked_profile_pin() -> None: + service = WorkflowService() + workflow_run = SimpleNamespace(extra_http_headers=None, proxy_location=None) + block = SimpleNamespace(parameters=[_credential_parameter()]) + credential = SimpleNamespace( + proxy_session_id="credential-pin", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + browser_profile_id="bp_1", + ) + profile = SimpleNamespace( + proxy_session_id="profile-pin", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + with patch("skyvern.forge.sdk.workflow.service.app") as mock_app: + mock_app.AGENT_FUNCTION = _proxy_header_hook() + mock_app.DATABASE.credentials.get_credential = AsyncMock(return_value=credential) + mock_app.DATABASE.browser_sessions.get_browser_profile = AsyncMock(return_value=profile) + mock_app.DATABASE.workflow_runs.update_workflow_run = AsyncMock() + + await service._apply_login_block_credential_proxy_pin( + block=block, # type: ignore[arg-type] + workflow_run=workflow_run, # type: ignore[arg-type] + workflow_run_id="wr_1", + organization_id="org_1", + ) + + assert workflow_run.extra_http_headers == {"dedicated-ip": "profile-pin"} + assert workflow_run.proxy_location == ProxyLocation.RESIDENTIAL_ISP + mock_app.DATABASE.workflow_runs.update_workflow_run.assert_awaited_once_with( + workflow_run_id="wr_1", + extra_http_headers={"dedicated-ip": "profile-pin"}, + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + ) + + +@pytest.mark.asyncio +async def test_login_block_credential_proxy_pin_skips_when_linked_profile_is_unpinned() -> None: + service = WorkflowService() + workflow_run = SimpleNamespace(extra_http_headers=None, proxy_location=None) + block = SimpleNamespace(parameters=[_credential_parameter()]) + credential = SimpleNamespace( + proxy_session_id="credential-pin", + proxy_location=ProxyLocation.RESIDENTIAL_ISP, + browser_profile_id="bp_1", + ) + profile = SimpleNamespace(proxy_session_id=None, proxy_location=None) + + with patch("skyvern.forge.sdk.workflow.service.app") as mock_app: + mock_app.AGENT_FUNCTION = _proxy_header_hook() + mock_app.DATABASE.credentials.get_credential = AsyncMock(return_value=credential) + mock_app.DATABASE.browser_sessions.get_browser_profile = AsyncMock(return_value=profile) + mock_app.DATABASE.workflow_runs.update_workflow_run = AsyncMock() + + await service._apply_login_block_credential_proxy_pin( + block=block, # type: ignore[arg-type] + workflow_run=workflow_run, # type: ignore[arg-type] + workflow_run_id="wr_1", + organization_id="org_1", + ) + + assert workflow_run.extra_http_headers is None + assert workflow_run.proxy_location is None + mock_app.DATABASE.workflow_runs.update_workflow_run.assert_not_called() + + +@pytest.mark.asyncio +async def test_login_block_credential_proxy_pin_does_not_mutate_run_when_db_update_fails() -> None: + service = WorkflowService() + workflow_run = SimpleNamespace(extra_http_headers=None, proxy_location=None) + block = SimpleNamespace(parameters=[_credential_parameter()]) + credential = SimpleNamespace(proxy_session_id="credential-pin", proxy_location=ProxyLocation.RESIDENTIAL_ISP) + + with patch("skyvern.forge.sdk.workflow.service.app") as mock_app: + mock_app.AGENT_FUNCTION = _proxy_header_hook() + mock_app.DATABASE.credentials.get_credential = AsyncMock(return_value=credential) + mock_app.DATABASE.workflow_runs.update_workflow_run = AsyncMock(side_effect=RuntimeError("db down")) + + await service._apply_login_block_credential_proxy_pin( + block=block, # type: ignore[arg-type] + workflow_run=workflow_run, # type: ignore[arg-type] + workflow_run_id="wr_1", + organization_id="org_1", + ) + + assert workflow_run.extra_http_headers is None + assert workflow_run.proxy_location is None diff --git a/tests/unit/test_proxy_url_custom.py b/tests/unit/test_proxy_url_custom.py index 50e16f8ab..51968f42c 100644 --- a/tests/unit/test_proxy_url_custom.py +++ b/tests/unit/test_proxy_url_custom.py @@ -1,7 +1,10 @@ import pytest +from pydantic import ValidationError from skyvern.config import settings from skyvern.forge.sdk.db.utils import deserialize_proxy_location, serialize_proxy_location +from skyvern.forge.sdk.schemas.browser_profiles import UpdateBrowserProfileRequest +from skyvern.forge.sdk.schemas.credentials import UpdateCredentialRequest from skyvern.schemas.runs import GeoTarget, ProxyLocation from skyvern.webeye.browser_factory import BrowserContextFactory, _redact_proxy_url, _redact_url_query @@ -79,6 +82,12 @@ def test_proxy_location_db_round_trip_custom_url() -> None: assert deserialize_proxy_location(serialized) == original +@pytest.mark.parametrize("request_model", [UpdateCredentialRequest, UpdateBrowserProfileRequest]) +def test_proxy_pin_requests_reject_custom_proxy_urls(request_model: type) -> None: + with pytest.raises(ValidationError, match="Custom proxy URLs are not supported"): + request_model(proxy_location={"url": "http://user:pass@proxy.example.com:8080"}) + + def test_proxy_location_db_round_trip_geo_target() -> None: original = GeoTarget(country="US", subdivision="CA", city="San Francisco") serialized = serialize_proxy_location(original) diff --git a/tests/unit/test_real_browser_manager.py b/tests/unit/test_real_browser_manager.py index 21590e58f..886cb31ef 100644 --- a/tests/unit/test_real_browser_manager.py +++ b/tests/unit/test_real_browser_manager.py @@ -154,12 +154,22 @@ def make_task( return task -def make_session(proxy_location: object = None) -> MagicMock: +def make_session(proxy_location: object = None, proxy_session_id: str | None = None) -> MagicMock: session = MagicMock() session.proxy_location = proxy_location + session.proxy_session_id = proxy_session_id return session +def _merge_cloud_proxy_session_headers( + extra_http_headers: dict[str, str] | None, + proxy_session_id: str, +) -> dict[str, str]: + headers = dict(extra_http_headers or {}) + headers.setdefault("dedicated-ip", proxy_session_id) + return headers + + @pytest.mark.asyncio async def test_task_browser_inherits_session_proxy_when_no_browser_state() -> None: """When a task has a browser_session_id and no in-memory browser state, the session's proxy_location is used.""" @@ -172,6 +182,7 @@ async def test_task_browser_inherits_session_proxy_when_no_browser_state() -> No session = make_session(proxy_location=session_proxy) with patch("skyvern.webeye.real_browser_manager.app") as mock_app: + mock_app.AGENT_FUNCTION.merge_proxy_session_extra_http_headers.side_effect = _merge_cloud_proxy_session_headers mock_app.PERSISTENT_SESSIONS_MANAGER.get_browser_state = AsyncMock(return_value=None) mock_app.PERSISTENT_SESSIONS_MANAGER.get_session = AsyncMock(return_value=session) mock_app.PERSISTENT_SESSIONS_MANAGER.set_browser_state = AsyncMock() @@ -186,6 +197,33 @@ async def test_task_browser_inherits_session_proxy_when_no_browser_state() -> No assert kwargs["proxy_location"] == session_proxy +@pytest.mark.asyncio +async def test_task_browser_inherits_session_proxy_pin_when_no_browser_state() -> None: + manager = RealBrowserManager() + task = make_task("tsk_1", proxy_location="RESIDENTIAL") + task.extra_http_headers = {"X-Test": "1"} + new_browser_state = MagicMock() + new_browser_state.get_or_create_page = AsyncMock() + + session = make_session(proxy_location="RESIDENTIAL_ISP", proxy_session_id="abc1234567") + + with patch("skyvern.webeye.real_browser_manager.app") as mock_app: + mock_app.AGENT_FUNCTION.merge_proxy_session_extra_http_headers.side_effect = _merge_cloud_proxy_session_headers + mock_app.PERSISTENT_SESSIONS_MANAGER.get_browser_state = AsyncMock(return_value=None) + mock_app.PERSISTENT_SESSIONS_MANAGER.get_session = AsyncMock(return_value=session) + mock_app.PERSISTENT_SESSIONS_MANAGER.set_browser_state = AsyncMock() + + with patch.object( + manager, "_create_browser_state", new=AsyncMock(return_value=new_browser_state) + ) as mock_create: + await manager.get_or_create_for_task(task=task, browser_session_id="pbs_123") + + expected_headers = {"X-Test": "1", "dedicated-ip": "abc1234567"} + assert mock_create.await_args.kwargs["extra_http_headers"] == expected_headers + assert new_browser_state.get_or_create_page.await_args.kwargs["extra_http_headers"] == expected_headers + assert task.extra_http_headers == {"X-Test": "1"} + + @pytest.mark.asyncio async def test_task_browser_uses_task_proxy_when_session_has_no_proxy() -> None: """When the session has no proxy_location, the task's proxy_location is used.""" @@ -244,6 +282,38 @@ async def test_workflow_run_browser_inherits_session_proxy_when_no_browser_state assert kwargs["proxy_location"] == session_proxy +@pytest.mark.asyncio +async def test_workflow_run_browser_inherits_session_proxy_pin_when_no_browser_state() -> None: + manager = RealBrowserManager() + workflow_run = make_workflow_run("wfr_1") + workflow_run.extra_http_headers = {"X-Test": "1"} + + new_browser_state = MagicMock() + new_browser_state.get_or_create_page = AsyncMock() + + session = make_session(proxy_location="RESIDENTIAL_ISP", proxy_session_id="abc1234567") + + with patch("skyvern.webeye.real_browser_manager.app") as mock_app: + mock_app.AGENT_FUNCTION.merge_proxy_session_extra_http_headers.side_effect = _merge_cloud_proxy_session_headers + mock_app.PERSISTENT_SESSIONS_MANAGER.get_browser_state = AsyncMock(return_value=None) + mock_app.PERSISTENT_SESSIONS_MANAGER.get_session = AsyncMock(return_value=session) + mock_app.PERSISTENT_SESSIONS_MANAGER.set_browser_state = AsyncMock() + + with patch.object( + manager, "_create_browser_state", new=AsyncMock(return_value=new_browser_state) + ) as mock_create: + await manager.get_or_create_for_workflow_run( + workflow_run=workflow_run, + url="https://example.com", + browser_session_id="pbs_456", + ) + + expected_headers = {"X-Test": "1", "dedicated-ip": "abc1234567"} + assert mock_create.await_args.kwargs["extra_http_headers"] == expected_headers + assert new_browser_state.get_or_create_page.await_args.kwargs["extra_http_headers"] == expected_headers + assert workflow_run.extra_http_headers == {"X-Test": "1"} + + @pytest.mark.asyncio async def test_workflow_run_browser_uses_workflow_proxy_when_session_has_no_proxy() -> None: """When the session has no proxy_location, the workflow run's proxy_location is used."""