@@ -62,6 +62,12 @@ class ActOnceResult:
6262 used_vision : bool
6363
6464
65+ @dataclass (frozen = True )
66+ class PreActionAuthorityDecision :
67+ allowed : bool
68+ reason : str | None = None
69+
70+
6571class RuntimeAgent :
6672 """
6773 A thin orchestration layer over AgentRuntime:
@@ -79,12 +85,22 @@ def __init__(
7985 vision_executor : LLMProvider | None = None ,
8086 vision_verifier : LLMProvider | None = None ,
8187 short_circuit_canvas : bool = True ,
88+ pre_action_authorizer : Callable [[Any ], Any ] | None = None ,
89+ authority_principal_id : str | None = None ,
90+ authority_tenant_id : str | None = None ,
91+ authority_session_id : str | None = None ,
92+ authority_fail_closed : bool = True ,
8293 ) -> None :
8394 self .runtime = runtime
8495 self .executor = executor
8596 self .vision_executor = vision_executor
8697 self .vision_verifier = vision_verifier
8798 self .short_circuit_canvas = short_circuit_canvas
99+ self .pre_action_authorizer = pre_action_authorizer
100+ self .authority_principal_id = authority_principal_id
101+ self .authority_tenant_id = authority_tenant_id
102+ self .authority_session_id = authority_session_id
103+ self .authority_fail_closed = authority_fail_closed
88104
89105 self ._structured_llm = LLMInteractionHandler (executor )
90106
@@ -120,7 +136,7 @@ async def run_step(
120136
121137 # 1) Structured executor attempt.
122138 action = self ._propose_structured_action (task_goal = task_goal , step = step , snap = snap )
123- await self ._execute_action (action = action , snap = snap )
139+ await self ._execute_action (action = action , snap = snap , step_goal = step . goal )
124140 ok = await self ._apply_verifications (step = step )
125141 if ok :
126142 outcome = "ok"
@@ -268,7 +284,7 @@ async def act_once_result(
268284 temperature = 0.0 ,
269285 )
270286 action = self ._extract_action_from_text (resp .content )
271- await self ._execute_action (action = action , snap = snap )
287+ await self ._execute_action (action = action , snap = snap , step_goal = step . goal )
272288 return ActOnceResult (action = action , snap = snap , used_vision = True )
273289
274290 # Structured snapshot-first proposal.
@@ -290,7 +306,7 @@ async def act_once_result(
290306 resp = self ._structured_llm .query_llm (dom_context , combined_goal )
291307 action = self ._structured_llm .extract_action (resp .content )
292308
293- await self ._execute_action (action = action , snap = snap )
309+ await self ._execute_action (action = action , snap = snap , step_goal = step . goal )
294310 return ActOnceResult (action = action , snap = snap , used_vision = False )
295311
296312 async def _run_hook (
@@ -367,7 +383,7 @@ async def _vision_executor_attempt(
367383 )
368384
369385 action = self ._extract_action_from_text (resp .content )
370- await self ._execute_action (action = action , snap = snap )
386+ await self ._execute_action (action = action , snap = snap , step_goal = step . goal )
371387 # Important: vision executor fallback is a *retry* of the same step.
372388 # Clear prior step assertions so required_assertions_passed reflects the final attempt.
373389 self .runtime .flush_assertions ()
@@ -397,21 +413,28 @@ async def _apply_verifications(self, *, step: RuntimeStep) -> bool:
397413 # Respect required verifications semantics.
398414 return self .runtime .required_assertions_passed () and all_ok
399415
400- async def _execute_action (self , * , action : str , snap : Snapshot | None ) -> None :
416+ async def _execute_action (self , * , action : str , snap : Snapshot | None , step_goal : str | None ) -> None :
401417 url = None
402418 try :
403419 url = await self .runtime .get_url ()
404420 except Exception :
405421 url = getattr (snap , "url" , None )
406422
407- await self .runtime .record_action (action , url = url )
408-
409423 # Coordinate-backed execution (by snapshot id or explicit coordinates).
410424 kind , payload = self ._parse_action (action )
411425
412426 if kind == "finish" :
427+ await self .runtime .record_action (action , url = url )
413428 return
414429
430+ await self ._authorize_pre_action_or_raise (
431+ action = action ,
432+ kind = kind ,
433+ url = url ,
434+ step_goal = step_goal ,
435+ )
436+ await self .runtime .record_action (action , url = url )
437+
415438 if kind == "press" :
416439 await self ._press_key_best_effort (payload ["key" ])
417440 await self ._stabilize_best_effort ()
@@ -449,6 +472,65 @@ async def _execute_action(self, *, action: str, snap: Snapshot | None) -> None:
449472
450473 raise ValueError (f"Unknown action kind: { kind } " )
451474
475+ async def _authorize_pre_action_or_raise (
476+ self ,
477+ * ,
478+ action : str ,
479+ kind : str ,
480+ url : str | None ,
481+ step_goal : str | None ,
482+ ) -> None :
483+ if self .pre_action_authorizer is None :
484+ return
485+ principal_id = self .authority_principal_id or "agent:sdk-python"
486+ action_name = self ._authority_action_name (kind )
487+ resource = url or "about:blank"
488+ intent = step_goal or action
489+
490+ try :
491+ request = self .runtime .build_authority_action_request (
492+ principal_id = principal_id ,
493+ action = action_name ,
494+ resource = resource ,
495+ intent = intent ,
496+ tenant_id = self .authority_tenant_id ,
497+ session_id = self .authority_session_id ,
498+ )
499+ decision_raw = self .pre_action_authorizer (request )
500+ if inspect .isawaitable (decision_raw ):
501+ decision_raw = await decision_raw
502+ decision = self ._normalize_authority_decision (decision_raw )
503+ if decision .allowed :
504+ return
505+ raise RuntimeError (
506+ f"pre_action_authority_denied: { decision .reason or 'denied_by_authority' } "
507+ )
508+ except Exception :
509+ if self .authority_fail_closed :
510+ raise
511+ return
512+
513+ def _normalize_authority_decision (self , value : Any ) -> PreActionAuthorityDecision :
514+ if isinstance (value , PreActionAuthorityDecision ):
515+ return value
516+ allowed_attr = getattr (value , "allowed" , None )
517+ if isinstance (allowed_attr , bool ):
518+ reason_attr = getattr (value , "reason" , None )
519+ reason = str (reason_attr ) if isinstance (reason_attr , str ) and reason_attr else None
520+ return PreActionAuthorityDecision (allowed = allowed_attr , reason = reason )
521+ if isinstance (value , bool ):
522+ return PreActionAuthorityDecision (allowed = value )
523+ raise RuntimeError ("invalid_pre_action_authority_decision" )
524+
525+ def _authority_action_name (self , kind : str ) -> str :
526+ if kind in {"click" , "click_xy" , "click_rect" }:
527+ return "browser.click"
528+ if kind == "type" :
529+ return "browser.type"
530+ if kind == "press" :
531+ return "browser.press"
532+ return "browser.unknown"
533+
452534 async def _stabilize_best_effort (self ) -> None :
453535 try :
454536 await self .runtime .backend .wait_ready_state (state = "interactive" , timeout_ms = 15000 )
0 commit comments