@@ -152,6 +152,8 @@ def _history_message_to_updates(msg: Message) -> list[Any]:
152152_OPTION_ALLOW_ALWAYS = "allow_always"
153153_OPTION_REJECT_ONCE = "reject_once"
154154_OPTION_REJECT_ALWAYS = "reject_always"
155+ _PREFIX_ALLOW_RULE = "allow_rule:"
156+ _PREFIX_DENY_RULE = "deny_rule:"
155157
156158
157159class ACPSession :
@@ -373,6 +375,30 @@ async def cancel(self) -> None:
373375 logger .info ("Session %s cancel requested" , self .id )
374376 self ._current_task .cancel ()
375377
378+ def _get_permission_context (self ):
379+ """Read the agent_loop's mutable permission context."""
380+ return getattr (self .agent_loop , "_permission_context" , None )
381+
382+ def _set_permission_context (self , perm_ctx ) -> None :
383+ """Write back the updated permission context to agent_loop."""
384+ if hasattr (self .agent_loop , "_permission_context" ):
385+ self .agent_loop ._permission_context = perm_ctx
386+
387+ def _apply_rule (self , tool_name : str , rules_str : str , behavior : str ) -> None :
388+ """Apply rule-level permission to the session's permission_context."""
389+ from iac_code .services .permissions .storage import apply_session_rule
390+ from iac_code .types .permissions import PermissionRuleValue
391+
392+ perm_ctx = self ._get_permission_context ()
393+ if perm_ctx is None :
394+ return
395+ for rule_content in rules_str .split ("," ):
396+ rule_content = rule_content .strip ()
397+ if rule_content :
398+ rule_value = PermissionRuleValue (tool_name = tool_name , rule_content = rule_content )
399+ perm_ctx = apply_session_rule (perm_ctx , behavior , rule_value )
400+ self ._set_permission_context (perm_ctx )
401+
376402 async def _request_permission (self , event : PermissionRequestEvent ) -> bool :
377403 tool_name = event .tool_name
378404
@@ -385,59 +411,113 @@ async def _request_permission(self, event: PermissionRequestEvent) -> bool:
385411 logger .debug ("Permission auto-denied for tool %s (cached)" , tool_name )
386412 return False
387413
388- response = await self ._conn .request_permission (
389- [
414+ # Extract suggestions from permission_result for rule-level options.
415+ suggestions = []
416+ if (
417+ event .permission_result is not None
418+ and hasattr (event .permission_result , "suggestions" )
419+ and event .permission_result .suggestions
420+ ):
421+ suggestions = event .permission_result .suggestions
422+
423+ # Build dynamic option list aligned with local REPL behavior.
424+ options : list [acp .schema .PermissionOption ] = [
425+ acp .schema .PermissionOption (
426+ option_id = _OPTION_ALLOW_ONCE ,
427+ name = "Allow once" ,
428+ kind = "allow_once" ,
429+ ),
430+ ]
431+
432+ if suggestions :
433+ rules_display = "," .join (s .rule_content for s in suggestions )
434+ options .append (
390435 acp .schema .PermissionOption (
391- option_id = _OPTION_ALLOW_ONCE ,
392- name = "Allow once" ,
393- kind = "allow_once" ,
394- ),
436+ option_id = _PREFIX_ALLOW_RULE + rules_display ,
437+ name = 'Always allow "{}" (this session)' .format (rules_display ),
438+ kind = "allow_always" ,
439+ )
440+ )
441+ else :
442+ options .append (
395443 acp .schema .PermissionOption (
396444 option_id = _OPTION_ALLOW_ALWAYS ,
397- name = "Always allow" ,
445+ name = "Always allow this tool " ,
398446 kind = "allow_always" ,
399- ),
400- acp .schema .PermissionOption (
401- option_id = _OPTION_REJECT_ONCE ,
402- name = "Reject once" ,
403- kind = "reject_once" ,
404- ),
447+ )
448+ )
449+
450+ options .append (
451+ acp .schema .PermissionOption (
452+ option_id = _OPTION_REJECT_ONCE ,
453+ name = "Reject once" ,
454+ kind = "reject_once" ,
455+ )
456+ )
457+
458+ if suggestions :
459+ rules_display = "," .join (s .rule_content for s in suggestions )
460+ options .append (
405461 acp .schema .PermissionOption (
406- option_id = _OPTION_REJECT_ALWAYS ,
407- name = " Always reject" ,
462+ option_id = _PREFIX_DENY_RULE + rules_display ,
463+ name = ' Always deny "{}" (this session)' . format ( rules_display ) ,
408464 kind = "reject_always" ,
409- ),
410- ],
465+ )
466+ )
467+
468+ options .append (
469+ acp .schema .PermissionOption (
470+ option_id = _OPTION_REJECT_ALWAYS ,
471+ name = "Always reject this tool" ,
472+ kind = "reject_always" ,
473+ ),
474+ )
475+
476+ # Build content with command details and suggested rule.
477+ content_text = "Approve tool call: {}\n Input: {}" .format (tool_name , event .tool_input )
478+ if suggestions :
479+ content_text += "\n Suggested rule: {}" .format ("," .join (s .rule_content for s in suggestions ))
480+
481+ response = await self ._conn .request_permission (
482+ options ,
411483 self .id ,
412484 acp .schema .ToolCallUpdate (
413- tool_call_id = f "permission/{ event .tool_use_id } " ,
485+ tool_call_id = "permission/{}" . format ( event .tool_use_id ) ,
414486 title = event .tool_name ,
415487 content = [
416488 acp .schema .ContentToolCallContent (
417489 type = "content" ,
418490 content = acp .schema .TextContentBlock (
419491 type = "text" ,
420- text = f"Approve tool call { event . tool_name } with input: { event . tool_input } " ,
492+ text = content_text ,
421493 ),
422494 )
423495 ],
424496 ),
425497 )
426498
427- # Interpret the outcome and update the permission cache
499+ # Interpret the outcome and update permission state.
428500 if isinstance (response .outcome , acp .schema .AllowedOutcome ):
429501 option_id = response .outcome .option_id
430502 if option_id == _OPTION_ALLOW_ALWAYS :
431503 self ._cache_permission (tool_name , "always_allow" )
504+ elif option_id and option_id .startswith (_PREFIX_ALLOW_RULE ):
505+ rules_str = option_id [len (_PREFIX_ALLOW_RULE ) :]
506+ self ._apply_rule (tool_name , rules_str , "allow" )
432507 return True
433508
434- # DeniedOutcome — the ACP SDK DeniedOutcome has no option_id field,
435- # so clients that want to signal "reject_always" should set
436- # _meta={"option_id": "reject_always"} on the *response* envelope.
509+ # DeniedOutcome — parse option_id from meta or direct field.
437510 if isinstance (response .outcome , acp .schema .DeniedOutcome ):
438- resp_meta = getattr (response , "field_meta" , None ) or {}
439- if resp_meta .get ("option_id" ) == _OPTION_REJECT_ALWAYS :
511+ option_id = getattr (response .outcome , "option_id" , None )
512+ if option_id is None :
513+ resp_meta = getattr (response , "field_meta" , None ) or {}
514+ option_id = resp_meta .get ("option_id" )
515+
516+ if option_id == _OPTION_REJECT_ALWAYS :
440517 self ._cache_permission (tool_name , "always_deny" )
518+ elif option_id and option_id .startswith (_PREFIX_DENY_RULE ):
519+ rules_str = option_id [len (_PREFIX_DENY_RULE ) :]
520+ self ._apply_rule (tool_name , rules_str , "deny" )
441521
442522 return False
443523
0 commit comments