@@ -61,6 +61,7 @@ class LeaseRequest(BaseModel):
6161 owner : str = "unknown"
6262 ttl_seconds : int = Field (default = 14400 , ge = 60 , le = 14400 )
6363 identity_id : str | None = None
64+ headed : bool = False
6465
6566
6667class LeaseIdRequest (BaseModel ):
@@ -152,7 +153,7 @@ class AuthRequest(BaseModel):
152153 reason : str = "login_required"
153154 identity_id : str | None = None
154155 profile : str | None = None
155- mode : str = "vnc "
156+ mode : str = "same_lease "
156157 ttl_seconds : int = Field (default = 900 , ge = 60 , le = 14400 )
157158 control_ttl_seconds : int = Field (default = 900 , ge = 60 , le = 3600 )
158159 wait_until : str = "domcontentloaded"
@@ -165,8 +166,8 @@ def normalize_legacy_profile(self) -> "AuthRequest":
165166 raise ValueError ("profile and identity_id must match when both are provided" )
166167 self .identity_id = self .profile
167168 normalized_mode = self .mode .strip ().lower ().replace ("-" , "_" )
168- if normalized_mode not in {"lease_control" , "vnc" }:
169- raise ValueError ("mode must be lease_control or vnc" )
169+ if normalized_mode not in {"lease_control" , "same_lease" , " vnc" }:
170+ raise ValueError ("mode must be lease_control, same_lease, or vnc" )
170171 self .mode = normalized_mode
171172 return self
172173
@@ -284,6 +285,76 @@ async def _legacy_vnc_auth_request(request: AuthRequest) -> dict[str, Any]:
284285 return result
285286
286287
288+ async def _same_lease_auth_request (request : AuthRequest ) -> dict [str , Any ]:
289+ if request .identity_id :
290+ require_identity (request .identity_id )
291+ active_lease_id = _active_identity_lease_id (request .identity_id ) if request .identity_id else None
292+ lease_obj : dict [str , Any ]
293+ created_for_auth = False
294+ navigation : dict [str , Any ] | None = None
295+ if active_lease_id :
296+ lease_data = require_lease (active_lease_id )
297+ if not lease_data .headed :
298+ raise HTTPException (
299+ status_code = 409 ,
300+ detail = (
301+ f"Identity is already leased in a non-visual slot: { request .identity_id } . "
302+ "Release that lease before starting a login handoff."
303+ ),
304+ )
305+ lease_obj = lease_data .__dict__
306+ else :
307+ lease_obj = await create_lease (
308+ LeaseRequest (
309+ owner = request .owner ,
310+ ttl_seconds = request .ttl_seconds ,
311+ identity_id = request .identity_id ,
312+ headed = True ,
313+ )
314+ )
315+ created_for_auth = True
316+ try :
317+ navigation = await browser_navigate (
318+ NavigateRequest (lease_id = str (lease_obj ["lease_id" ]), url = request .url , wait_until = request .wait_until )
319+ )
320+ except Exception :
321+ await release_lease (str (lease_obj ["lease_id" ]))
322+ raise
323+ auth_data = create_auth_request (
324+ request .owner ,
325+ request .url ,
326+ request .reason ,
327+ request .identity_id ,
328+ mode = "same_lease" ,
329+ lease_id = str (lease_obj ["lease_id" ]),
330+ slot = str (lease_obj .get ("name" ) or "" ),
331+ cdp = str (lease_obj .get ("cdp" ) or "" ),
332+ )
333+ auth_data ["lease_id" ] = str (lease_obj ["lease_id" ])
334+ auth_data ["lease" ] = lease_obj
335+ auth_data ["auth_url" ] = auth_data ["portal_url" ]
336+ auth_data ["handoff_url" ] = auth_data ["portal_url" ]
337+ auth_data ["created_lease" ] = created_for_auth
338+ if navigation :
339+ auth_data ["navigation" ] = navigation
340+ _safe_record_event (
341+ source = request .owner ,
342+ event_type = "auth" ,
343+ message = "Same-lease auth handoff request created" ,
344+ lease_id = str (lease_obj ["lease_id" ]),
345+ url = request .url ,
346+ tags = ["auth" , "same-lease" , request .reason ],
347+ data = {
348+ "token" : auth_data .get ("token" ),
349+ "status" : auth_data .get ("status" ),
350+ "identity_id" : request .identity_id ,
351+ "slot" : lease_obj .get ("name" ),
352+ "created_lease" : created_for_auth ,
353+ },
354+ )
355+ return auth_data
356+
357+
287358async def _active_identity_control_response (request : AuthRequest ) -> dict [str , Any ] | None :
288359 if not request .identity_id :
289360 return None
@@ -369,6 +440,11 @@ async def _active_identity_control_response(request: AuthRequest) -> dict[str, A
369440
370441
371442async def _open_auth_lease_control (request : AuthRequest ) -> dict [str , Any ]:
443+ if request .mode == "same_lease" :
444+ if not request .identity_id :
445+ legacy_request = request .model_copy (update = {"mode" : "vnc" })
446+ return await _legacy_vnc_auth_request (legacy_request )
447+ return await _same_lease_auth_request (request )
372448 if request .mode == "vnc" :
373449 return await _legacy_vnc_auth_request (request )
374450 active_response = await _active_identity_control_response (request )
@@ -636,6 +712,18 @@ async def _verify_live_auth_browser(cdp: str | None, target_url: str, host: str
636712 return {"ok" : False , "checked" : False , "host" : host , "url" : target_url , "error" : str (error )}
637713
638714
715+ async def _verify_live_lease_page_state (lease_id : str , target_url : str , host : str | None ) -> dict [str , Any ]:
716+ if not lease_id :
717+ return {"ok" : False , "checked" : False , "error" : "missing lease_id" }
718+ try :
719+ if target_url :
720+ await browser_navigate (NavigateRequest (lease_id = lease_id , url = target_url ))
721+ snapshot = await browser_snapshot (LeaseIdRequest (lease_id = lease_id ))
722+ return _assess_auth_page_state (snapshot , host )
723+ except Exception as error :
724+ return {"ok" : False , "checked" : False , "host" : host , "url" : target_url , "error" : str (error )}
725+
726+
639727def _record_browser_failure (request : LeaseIdRequest , action : str , error : Exception , data : dict [str , Any ] | None = None ) -> None :
640728 _safe_record_event (
641729 source = "broker-api" ,
@@ -2223,6 +2311,11 @@ async def openbrowser_docs(_auth: str = Depends(require_openbrowser_api_key)) ->
22232311 "dashboard" : "/openbrowser" ,
22242312 "base_url" : _openbrowser_base_url (),
22252313 "endpoints" : _openbrowser_endpoint_catalog (),
2314+ "agent_guidance" : {
2315+ "quickstart" : docs ("quickstart" ),
2316+ "auth" : docs ("auth" ),
2317+ "routing" : docs ("routing" ),
2318+ },
22262319 "identities" : {
22272320 "generic" : "omit identity_id for a neutral non-account browser" ,
22282321 "configured" : {
@@ -2467,7 +2560,7 @@ async def openbrowser_telemetry_summary(
24672560@app .post ("/lease" )
24682561async def create_lease (request : LeaseRequest ) -> dict [str , Any ]:
24692562 try :
2470- lease_obj = lease (request .owner , request .ttl_seconds , request .identity_id )
2563+ lease_obj = lease (request .owner , request .ttl_seconds , request .identity_id , headed = request . headed )
24712564 result = lease_obj .__dict__
24722565 _safe_record_event (
24732566 source = lease_obj .owner ,
@@ -4474,6 +4567,44 @@ async def auth_complete(token: str) -> dict[str, Any]:
44744567 pending_target_url = str (pending_request .get ("url" ) or "" )
44754568 pending_host = urllib .parse .urlsplit (pending_target_url ).hostname if pending_target_url else None
44764569 pending_vnc = pending_request .get ("vnc" ) or {}
4570+ if pending_request .get ("mode" ) == "same_lease" :
4571+ live_verification = await _verify_live_lease_page_state (
4572+ str (pending_request .get ("lease_id" ) or "" ),
4573+ pending_target_url ,
4574+ pending_host ,
4575+ )
4576+ if not live_verification .get ("ok" ):
4577+ response = dict (pending_request )
4578+ response ["completion_blocked" ] = True
4579+ response ["auth_verified" ] = False
4580+ response ["live_page_verification" ] = live_verification
4581+ _safe_record_event (
4582+ source = str (pending_request .get ("owner" , "unknown" )),
4583+ event_type = "auth" ,
4584+ message = "Same-lease auth completion blocked because live tab still appears signed out" ,
4585+ severity = "warning" ,
4586+ lease_id = str (pending_request .get ("lease_id" ) or "" ),
4587+ url = pending_target_url ,
4588+ tags = ["auth" , "complete" , "blocked" , "same-lease" , "live-page-signed-out" ],
4589+ data = {"token" : token , "identity_id" : pending_identity_id , "verification" : live_verification },
4590+ )
4591+ return response
4592+ request = complete_auth_request (token )
4593+ request ["vnc_stop" ] = stop_auth_vnc (token , missing_ok = True )
4594+ request ["auth_verified" ] = True
4595+ request ["live_page_verification" ] = live_verification
4596+ request ["lease_id" ] = str (pending_request .get ("lease_id" ) or "" )
4597+ request ["handoff_url" ] = request .get ("portal_url" )
4598+ _safe_record_event (
4599+ source = str (request .get ("owner" , "unknown" )),
4600+ event_type = "auth" ,
4601+ message = "Same-lease auth request completed" ,
4602+ lease_id = str (request .get ("lease_id" ) or "" ),
4603+ url = str (request .get ("url" ) or "" ),
4604+ tags = ["auth" , "complete" , "same-lease" ],
4605+ data = {"token" : token , "status" : request .get ("status" )},
4606+ )
4607+ return request
44774608 if pending_identity_id and pending_vnc .get ("cdp" ):
44784609 live_verification = await _verify_live_auth_browser (
44794610 str (pending_vnc .get ("cdp" )),
0 commit comments