[eric] browser: RequestUserSignIn is a first-class tool; auth walls hand off to the user in place instead of killing the run (ENG-351)

This commit is contained in:
ciregenz
2026-08-18 19:06:38 -07:00
parent 201216d166
commit 123f71b8cb
2 changed files with 79 additions and 1 deletions
+59 -1
View File
@@ -1500,7 +1500,8 @@ async def run_browser_agent(
if not browser_delete_script.delete_tool_enabled():
p_cached_tools = [t for t in p_cached_tools if t["name"] != "BrowserDeleteItem"]
if not p_hitl_allowed:
p_cached_tools = [t for t in p_cached_tools if t["name"] != "RequestHumanIntervention"]
# Unattended runs (workflows) have no human to sign in either; both hand-off tools go together.
p_cached_tools = [t for t in p_cached_tools if t["name"] not in ("RequestHumanIntervention", "RequestUserSignIn")]
if p_cached_tools:
p_cached_tools[-1] = {**p_cached_tools[-1], "cache_control": {"type": "ephemeral"}}
@@ -2514,6 +2515,63 @@ async def run_browser_agent(
})
break
# RequestUserSignIn (ENG-351): the model hands a login wall to the user in
# place; same borrow-then-wait ladder as the automatic ENG-279 path, same
# one-pause-per-run cap, and the run never dies at the wall.
if tu.name == "RequestUserSignIn":
from urllib.parse import urlsplit as p_si_urlsplit
p_si_dom = (str(tu.input.get("domain") or "").strip()
or (p_si_urlsplit(last_seen_url).hostname or "this site"))
if await try_borrow_signin(p_si_dom, browser_id, tab_id, last_seen_url):
browser_login_handoff.record_login(p_si_dom)
p_si_text = (f"You are now signed in to {p_si_dom} (the user's existing "
"session was borrowed silently). Look at the page fresh and "
"continue the task.")
elif p_signin_wait_used or not ws_manager.global_connections:
p_si_text = ("Nobody is watching the card right now (or a sign-in wait "
"already ran this run), so the user cannot sign in for you. "
"Do what is possible without it and say plainly in Done what "
"still needs the sign-in.")
else:
p_signin_wait_used = True
p_si_msg = Message(role="assistant", content=(
f"⏸ {p_si_dom} needs you to sign in. Use the browser card directly; "
"I'll continue automatically once you're in (waiting up to 3 minutes)."))
session.messages.append(p_si_msg)
await ws_manager.send_to_session(session_id, "agent:message", {
"session_id": session_id,
"message": p_si_msg.model_dump(mode="json"),
})
async def p_si_probe() -> tuple:
p_gt = await execute_browser_tool("BrowserGetText", {}, browser_id, tab_id)
if not isinstance(p_gt, dict):
return ("", "")
return (str(p_gt.get("url") or ""), str(p_gt.get("text") or ""))
if await wait_for_user_signin(p_si_dom, p_si_probe, cancel_event):
browser_login_handoff.record_login(p_si_dom)
p_si_text = (f"The user just signed in to {p_si_dom}. The page has "
"changed; look at it fresh and continue the task.")
else:
p_si_text = (f"The user did not sign in to {p_si_dom} within the wait "
"window. Do what is possible without it, and say plainly "
"in Done what still needs the sign-in.")
tool_results.append({
"type": "tool_result", "tool_use_id": tu.id,
"content": [{"type": "text", "text": p_si_text}],
})
p_si_result = Message(
role="tool_result",
content={"text": p_si_text, "tool_name": tu.name, "elapsed_ms": 0},
)
session.messages.append(p_si_result)
await ws_manager.send_to_session(session_id, "agent:message", {
"session_id": session_id,
"message": p_si_result.model_dump(mode="json"),
})
continue
# Handle RequestHumanIntervention; pause and wait for user. Only the model can
# reach here, and only when the tool was actually offered this run.
if tu.name == "RequestHumanIntervention":
@@ -766,6 +766,26 @@ BROWSER_TOOLS_SCHEMA = [
"required": ["task"],
},
},
{
"name": "RequestUserSignIn",
"description": (
"Hand a login wall back to the user WITHOUT ending the run: the browser card stays "
"live, the user signs in on it directly, and you resume automatically the moment "
"the wall clears (waits up to 3 minutes). Prefer this over giving up or over "
"RequestHumanIntervention whenever the only blocker is authentication "
"(login page, SSO, MFA, session expired)."
),
"input_schema": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Site that needs the sign-in, e.g. 'notion.so'. Defaults to the current page's domain.",
},
},
"required": [],
},
},
{
"name": "RequestHumanIntervention",
"description": (