def rule_resolve(rules_dir, rule_name, visited=None): """Recursively resolve a rule with all its base rules merged""" if visited is None: visited = set() if rule_name in visited: raise ValueError(f"Circular dependency detected: {rule_name}") visited.add(rule_name) rule_file = os.path.join(rules_dir, f"{rule_name}.rule") with open(rule_file) as f: rule = json.load(f) # Start with empty merged rule merged = { 'allow_ips': [], 'allow_ports': [], 'block_ips': [], 'block_ports': [], 'dns_redirect': False } # Merge base rules first (left to right) for base_name in rule.get('extends', []): base = rule_resolve(rules_dir, base_name, visited.copy()) merged['allow_ips'] += base.get('allow_ips', []) merged['allow_ports'] += base.get('allow_ports', []) merged['block_ips'] += base.get('block_ips', []) merged['block_ports'] += base.get('block_ports', []) if base.get('dns_redirect'): merged['dns_redirect'] = True # Child rule overrides — deduplicate merged['allow_ips'] = list(dict.fromkeys( merged['allow_ips'] + rule.get('allow_ips', []))) merged['allow_ports'] = list(dict.fromkeys( merged['allow_ports'] + rule.get('allow_ports', []))) merged['block_ips'] = list(dict.fromkeys( merged['block_ips'] + rule.get('block_ips', []))) merged['block_ports'] = list(dict.fromkeys( merged['block_ports'] + rule.get('block_ports', []))) if rule.get('dns_redirect'): merged['dns_redirect'] = True merged['name'] = rule['name'] merged['desc'] = rule.get('desc', '') merged['extends'] = rule.get('extends', []) print(json.dumps(merged)) def find_rule_file(rules_dir, rule_name): """Find rule file in rules/ or rules/base/""" for path in [ os.path.join(rules_dir, f"{rule_name}.rule"), os.path.join(rules_dir, "base", f"{rule_name}.rule"), ]: if os.path.exists(path): return path raise FileNotFoundError(f"Rule not found: {rule_name}") 'rule_resolve': lambda args: rule_resolve(args[0], args[1], args[2]), 'find_rule_file': lambda args: find_rule_file(args[0], args[1]), Still need to add that to json.sh, also let's avoid inline Python, I saw you write it just now on rule::get()