@@ -29,7 +29,8 @@ class PolicyBundle:
2929 etag : Optional [str ] = None # For analytics and caching
3030 tool_to_roles : Dict [str , Set [str ]] = field (default_factory = dict , repr = False )
3131 tool_conditions : Dict [str , List [Dict [str , Any ]]] = field (default_factory = dict , repr = False )
32- role_to_sequences : Dict [str , List [Dict [str , Any ]]] = field (default_factory = dict , repr = False )
32+ # Map role -> SequenceRules object (holds rules list + mode)
33+ role_to_sequences : Dict [str , Any ] = field (default_factory = dict , repr = False )
3334 # Data flow tracking: which labels each tool produces
3435 tool_labels : Dict [str , Set [str ]] = field (default_factory = dict , repr = False )
3536 # Data flow tracking: which labels block which tools
@@ -119,14 +120,31 @@ def __post_init__(self):
119120 )
120121
121122 # Parse sequence rules (apply to this specific role)
122- sequence_rules = policy .get ("sequence" , [])
123- if sequence_rules :
124- logger .debug ("Processing %d sequence rules for role '%s'" , len (sequence_rules ), role )
123+ sequence_raw = policy .get ("sequence" , [])
124+
125+ # Handle both legacy list format and new nested format
126+ if isinstance (sequence_raw , dict ):
127+ # New format: {"mode": "deny", "rules": [...]}
128+ mode = sequence_raw .get ("mode" )
129+ rules_list = sequence_raw .get ("rules" , [])
130+ else :
131+ # Legacy format: list of rules directly, default to allow mode
132+ mode = "allow"
133+ rules_list = sequence_raw if isinstance (sequence_raw , list ) else []
134+
135+ # Store sequence rules if they exist OR if mode is "deny" (empty deny = block all)
136+ if rules_list or (mode == "deny" ):
137+ logger .debug ("Processing %d sequence rules for role '%s' (mode=%s)" , len (rules_list ), role , mode )
125138 # Validate @group references without expanding (lazy expansion at runtime)
126139 tool_groups = self ._extract_tool_groups (policy_content )
127- validated_rules = self ._validate_sequence_rules (sequence_rules , tool_groups )
128- self .role_to_sequences [role ] = validated_rules
129- logger .debug ("Validated %d sequence rules for role '%s' (lazy @group expansion)" , len (validated_rules ), role )
140+ validated_rules = self ._validate_sequence_rules (rules_list , tool_groups )
141+
142+ # Store as simple dict to avoid pickling issues with dataclasses
143+ self .role_to_sequences [role ] = {
144+ "mode" : mode ,
145+ "rules" : validated_rules
146+ }
147+ logger .debug ("Validated %d sequence rules for role '%s'" , len (validated_rules ), role )
130148
131149 logger .debug ("Final tool_to_roles mapping: %s" , dict (self .tool_to_roles ))
132150 logger .debug ("Final role_to_sequences mapping: %s" , dict (self .role_to_sequences ))
@@ -140,16 +158,17 @@ def all_known_tools(self) -> Set[str]:
140158
141159 return set (self .tool_to_roles .keys ())
142160
143- def get_sequence_rules (self , role : str ) -> List [ Dict [str , Any ] ]:
144- """Get sequence rules for a specific role.
161+ def get_sequence_rules (self , role : str ) -> Dict [str , Any ]:
162+ """Get sequence configuration for a specific role.
145163
146164 Args:
147165 role: Role name
148166
149167 Returns:
150- List of sequence rules (may contain @group references for lazy expansion)
168+ Dict with 'mode' (str) and 'rules' (List[Dict]) keys.
169+ Returns None if no sequence rules defined for role.
151170 """
152- return self .role_to_sequences .get (role , [] )
171+ return self .role_to_sequences .get (role )
153172
154173 def get_tool_groups (self ) -> Dict [str , List [str ]]:
155174 """Get tool_groups from policy metadata for lazy sequence expansion.
0 commit comments