From f9f1e8e9691371cd775c608b76e86572af152179 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 14:33:23 +0200 Subject: [PATCH 01/31] Add check for all_action being None --- AIDojoCoordinator/worlds/WhiteBoxNSGCoordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDojoCoordinator/worlds/WhiteBoxNSGCoordinator.py b/AIDojoCoordinator/worlds/WhiteBoxNSGCoordinator.py index 88b5e1b7..d9ac3285 100644 --- a/AIDojoCoordinator/worlds/WhiteBoxNSGCoordinator.py +++ b/AIDojoCoordinator/worlds/WhiteBoxNSGCoordinator.py @@ -29,7 +29,7 @@ def _initialize(self): self._generate_all_actions() self._registration_info = { "all_actions": json.dumps([v.as_dict for v in self._all_actions]), - } + } if self._all_actions is not None else {} def _generate_all_actions(self)-> list: From b73d99b6ba0db47a1408f4b4422ee34e00b8568f Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 14:54:01 +0200 Subject: [PATCH 02/31] Fix correct return value --- AIDojoCoordinator/utils/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AIDojoCoordinator/utils/utils.py b/AIDojoCoordinator/utils/utils.py index a1e5510e..db78a381 100644 --- a/AIDojoCoordinator/utils/utils.py +++ b/AIDojoCoordinator/utils/utils.py @@ -238,7 +238,7 @@ def read_agents_known_services(self, type_agent: str, type_data: str) -> dict: known_services = {} return known_services - def read_agents_known_networks(self, type_agent: str, type_data: str) -> dict: + def read_agents_known_networks(self, type_agent: str, type_data: str) -> set: """ Generic function to read the known networks for any agent and goal of position """ @@ -251,7 +251,7 @@ def read_agents_known_networks(self, type_agent: str, type_data: str) -> dict: host_part, net_part = net.split('/') known_networks.add(Network(host_part, int(net_part))) except (ValueError, TypeError, netaddr.AddrFormatError): - self.logger('Configuration problem with the known networks') + self.logger.error('Configuration problem with the known networks') return known_networks def read_agents_known_hosts(self, type_agent: str, type_data: str) -> dict: From 7c7e508f756646197ee7cae2e7e4fc6ca37cc410 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 14:57:14 +0200 Subject: [PATCH 03/31] Fix return type --- AIDojoCoordinator/utils/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AIDojoCoordinator/utils/utils.py b/AIDojoCoordinator/utils/utils.py index db78a381..d4482a6f 100644 --- a/AIDojoCoordinator/utils/utils.py +++ b/AIDojoCoordinator/utils/utils.py @@ -154,7 +154,7 @@ def read_config_file(self, conf_file_name:str): self.logger.error(f'Error loading the configuration file{e}') pass - def read_env_action_data(self, action_name: str) -> dict: + def read_env_action_data(self, action_name: str) -> float: """ Generic function to read the known data for any agent and goal of position """ @@ -254,7 +254,7 @@ def read_agents_known_networks(self, type_agent: str, type_data: str) -> set: self.logger.error('Configuration problem with the known networks') return known_networks - def read_agents_known_hosts(self, type_agent: str, type_data: str) -> dict: + def read_agents_known_hosts(self, type_agent: str, type_data: str) -> set: """ Generic function to read the known hosts for any agent and goal of position """ @@ -274,7 +274,7 @@ def read_agents_known_hosts(self, type_agent: str, type_data: str) -> dict: self.logger.error(f'Configuration problem with the known hosts: {e}') return known_hosts - def read_agents_controlled_hosts(self, type_agent: str, type_data: str) -> dict: + def read_agents_controlled_hosts(self, type_agent: str, type_data: str) -> set: """ Generic function to read the controlled hosts for any agent and goal of position """ From 946561ea0c2be501df654cf033ec6a9dee1f178e Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 14:59:35 +0200 Subject: [PATCH 04/31] Add optional type --- AIDojoCoordinator/utils/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AIDojoCoordinator/utils/utils.py b/AIDojoCoordinator/utils/utils.py index d4482a6f..8962712d 100644 --- a/AIDojoCoordinator/utils/utils.py +++ b/AIDojoCoordinator/utils/utils.py @@ -13,6 +13,7 @@ import json import hashlib from cyst.api.configuration.network.node import NodeConfig +from typing import Optional def get_file_hash(filepath, hash_func='sha256', chunk_size=4096): """ @@ -395,7 +396,7 @@ def get_win_conditions(self, agent_role): case _: raise ValueError(f"Unsupported agent role: {agent_role}") - def get_max_steps(self, role=str)->int: + def get_max_steps(self, role=str)->Optional[int]: """ Get the max steps based on agent's role """ From 867cb5e0fdb8752240cded6219bc20ad15b80af7 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 15:03:24 +0200 Subject: [PATCH 05/31] Add typing --- AIDojoCoordinator/utils/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/AIDojoCoordinator/utils/utils.py b/AIDojoCoordinator/utils/utils.py index 8962712d..be30933f 100644 --- a/AIDojoCoordinator/utils/utils.py +++ b/AIDojoCoordinator/utils/utils.py @@ -12,8 +12,8 @@ from random import randint import json import hashlib -from cyst.api.configuration.network.node import NodeConfig -from typing import Optional +from cyst.api.configuration.network.node import NodeConfig, ActiveServiceConfig, InterfaceConfig +from typing import Optional, cast def get_file_hash(filepath, hash_func='sha256', chunk_size=4096): """ @@ -410,7 +410,7 @@ def get_max_steps(self, role=str)->Optional[int]: self.logger.warning(f"Unsupported value in 'coordinator.agents.{role}.max_steps': {e}. Setting value to default=None (no step limit)") return max_steps - def get_goal_description(self, agent_role)->dict: + def get_goal_description(self, agent_role)->str: """ Get goal description per role """ @@ -554,11 +554,13 @@ def get_starting_position_from_cyst_config(cyst_objects): for obj in cyst_objects: if isinstance(obj, NodeConfig): for active_service in obj.active_services: + active_service = cast(ActiveServiceConfig, active_service) if active_service.type == "netsecenv_agent": - print(f"startig processing {obj.id}.{active_service.name}") + print(f"starting processing {obj.id}.{active_service.name}") hosts = set() networks = set() for interface in obj.interfaces: + interface = cast(InterfaceConfig, interface) hosts.add(IP(str(interface.ip))) net_ip, net_mask = str(interface.net).split("/") networks.add(Network(net_ip,int(net_mask))) From 9d90a3597c41b2c83980777b6c56491a4393b67a Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 15:04:37 +0200 Subject: [PATCH 06/31] Add optional typing --- AIDojoCoordinator/utils/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AIDojoCoordinator/utils/utils.py b/AIDojoCoordinator/utils/utils.py index be30933f..5f0d523c 100644 --- a/AIDojoCoordinator/utils/utils.py +++ b/AIDojoCoordinator/utils/utils.py @@ -112,7 +112,7 @@ def observation_as_dict(observation:Observation)->dict: } return observation_dict -def parse_log_content(log_content:str)->list: +def parse_log_content(log_content:str)->Optional[list]: try: logs = [] data = json.loads(log_content) @@ -554,13 +554,11 @@ def get_starting_position_from_cyst_config(cyst_objects): for obj in cyst_objects: if isinstance(obj, NodeConfig): for active_service in obj.active_services: - active_service = cast(ActiveServiceConfig, active_service) if active_service.type == "netsecenv_agent": print(f"starting processing {obj.id}.{active_service.name}") hosts = set() networks = set() for interface in obj.interfaces: - interface = cast(InterfaceConfig, interface) hosts.add(IP(str(interface.ip))) net_ip, net_mask = str(interface.net).split("/") networks.add(Network(net_ip,int(net_mask))) From 15dedf7ee46f55138a5d891bf03dfd2238e5dd14 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 15:06:03 +0200 Subject: [PATCH 07/31] Add default option --- AIDojoCoordinator/game_components.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AIDojoCoordinator/game_components.py b/AIDojoCoordinator/game_components.py index 98881f13..f91b090a 100755 --- a/AIDojoCoordinator/game_components.py +++ b/AIDojoCoordinator/game_components.py @@ -738,6 +738,8 @@ def from_string(cls, string:str)->"GameStatus": return GameStatus.FORBIDDEN case "GameStatus.RESET_DONE": return GameStatus.RESET_DONE + case _: + raise ValueError(f"Invalid GameStatus string: {string}") def __repr__(self) -> str: """ Return the string representation of the GameStatus. From 627a6dc5efc31d675ef402062eb8820a13f76dc2 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 15:07:10 +0200 Subject: [PATCH 08/31] Initialize empty array --- AIDojoCoordinator/game_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDojoCoordinator/game_components.py b/AIDojoCoordinator/game_components.py index f91b090a..5ec6a505 100755 --- a/AIDojoCoordinator/game_components.py +++ b/AIDojoCoordinator/game_components.py @@ -551,8 +551,8 @@ def as_graph(self)->tuple: graph_nodes = {} node_features = [] controlled = [] + edges = [] try: - edges = [] #add known nets for net in self.known_networks: graph_nodes[net] = len(graph_nodes) From 1883a0c1a85dcf75b6cc73b76369088642b6a478 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 16:11:57 +0200 Subject: [PATCH 09/31] Add return value --- AIDojoCoordinator/coordinator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index 92d07034..bcbe8d21 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -816,7 +816,7 @@ async def _remove_agent_from_game(self, agent_addr): async def step(self, agent_id:tuple, agent_state:GameState, action:Action): raise NotImplementedError - async def reset(self): + async def reset(self)->bool: return NotImplemented def _initialize(self): @@ -847,6 +847,7 @@ def goal_dict_satistfied(goal_dict:dict, known_dict: dict)-> bool: return False self.logger.debug(f"Checking goal for agent {agent_addr}.") goal_conditions = self._win_conditions_per_role[self.agents[agent_addr][1]] + self.logger.debug(f"\tGoal conditions for {agent_addr}: {goal_conditions}.") state = self._agent_states[agent_addr] # For each part of the state of the game, check if the conditions are met goal_reached = {} From 44b0be75f393fb55f11f884e9c428e0ab1375e3c Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 16:55:00 +0200 Subject: [PATCH 10/31] Refacord view processing to be more reusable --- AIDojoCoordinator/worlds/NSEGameCoordinator.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index b845497c..a5632350 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -67,8 +67,8 @@ def _initialize(self) -> None: self._data_content_original = copy.deepcopy(self._data_content) self._firewall_original = copy.deepcopy(self._firewall) self.logger.info("Environment initialization finished") - - def _get_controlled_hosts_from_view(self, view_controlled_hosts:Iterable)->set: + + def _get_controlled_hosts_from_view(self, view_controlled_hosts:Iterable, hosts_to_start=None)->set: """ Parses view and translates all keywords. Produces set of controlled host (IP) """ @@ -81,8 +81,11 @@ def _get_controlled_hosts_from_view(self, view_controlled_hosts:Iterable)->set: elif host == 'random': # Random start self.logger.debug('\tAdding random starting position of agent') - self.logger.debug(f'\t\tChoosing from {self.hosts_to_start}') - selected = random.choice(self.hosts_to_start) + if hosts_to_start is not None: + self.logger.debug(f'\t\tChoosing from {self.hosts_to_start}') + selected = random.choice(self.hosts_to_start) + else: + selected = random.choice(list(self._ip_to_hostname.keys())) controlled_hosts.add(selected) self.logger.debug(f'\t\tMaking agent start in {selected}') elif host == "all_local": @@ -159,7 +162,7 @@ def _create_state_from_view(self, view:dict, add_neighboring_nets:bool=True)->Ga # re-map all networks based on current mapping in self._network_mapping known_networks = set([self._network_mapping[net] for net in view["known_networks"]]) # parse controlled hosts - controlled_hosts = self._get_controlled_hosts_from_view(view["controlled_hosts"]) + controlled_hosts = self._get_controlled_hosts_from_view(view_controlled_hosts=view["controlled_hosts"], hosts_to_start=self.hosts_to_start) known_hosts = set([self._ip_mapping[ip] for ip in view["known_hosts"]]) # Add all controlled hosts to known_hosts known_hosts = known_hosts.union(controlled_hosts) From 53349f1e2dca6dfdd892a47aa31fa281040a8c3e Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 16:55:46 +0200 Subject: [PATCH 11/31] Split IP mapping generation --- .../worlds/NSEGameCoordinator.py | 131 ++++++++++-------- 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index a5632350..5aa8406e 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -380,66 +380,18 @@ def process_firewall()->dict: self.logger.info(f"\tintitial self._ip_mapping: {self._ip_mapping}") self.logger.info("CYST configuration processed successfully") - def _create_new_network_mapping(self)->tuple: - """ Method that generates random IP and Network addreses - while following the topology loaded in the environment. - All internal data structures are updated with the newly generated addresses.""" - fake = self._faker_object - mapping_nets = {} - mapping_ips = {} - # generate mapping for networks - private_nets = [] - for net in self._networks.keys(): - if netaddr.IPNetwork(str(net)).ip.is_private(): - private_nets.append(net) - else: - mapping_nets[net] = Network(fake.ipv4_public(), net.mask) - - # for private networks, we want to keep the distances among them - private_nets_sorted = sorted(private_nets) - valid_valid_network_mapping = False - counter_iter = 0 - while not valid_valid_network_mapping: - try: - # find the new lowest networks - new_base = netaddr.IPNetwork(f"{fake.ipv4_private()}/{private_nets_sorted[0].mask}") - # store its new mapping - mapping_nets[private_nets[0]] = Network(str(new_base.network), private_nets_sorted[0].mask) - base = netaddr.IPNetwork(str(private_nets_sorted[0])) - is_private_net_checks = [] - for i in range(1,len(private_nets_sorted)): - current = netaddr.IPNetwork(str(private_nets_sorted[i])) - # find the distance before mapping - diff_ip = current.ip - base.ip - # find the new mapping - new_net_addr = netaddr.IPNetwork(str(mapping_nets[private_nets_sorted[0]])).ip + diff_ip - # evaluate if its still a private network - is_private_net_checks.append(new_net_addr.is_private()) - # store the new mapping - mapping_nets[private_nets_sorted[i]] = Network(str(new_net_addr), private_nets_sorted[i].mask) - if False not in is_private_net_checks: # verify that ALL new networks are still in the private ranges - valid_valid_network_mapping = True - except IndexError as e: - self.logger.info(f"Dynamic address sampling failed, re-trying. {e}") - counter_iter +=1 - if counter_iter > 10: - self.logger.error("Dynamic address failed more than 10 times - stopping.") - exit(-1) - # Invalid IP address boundary - self.logger.info(f"New network mapping:{mapping_nets}") + + + def _dynamic_ip_change(self, max_attempts:int=10)-> None: + """ + Changes the IP and network addresses in the environment + """ + self.logger.info("Changing IP and Network addresses in the environment") + # find a new IP and network mapping - # genereate mapping for ips: - for net,ips in self._networks.items(): - ip_list = list(netaddr.IPNetwork(str(mapping_nets[net])))[1:] - # remove broadcast and network ip from the list - random.shuffle(ip_list) - for i,ip in enumerate(ips): - mapping_ips[ip] = IP(str(ip_list[i])) - # Always add random, in case random is selected for ips - mapping_ips['random'] = 'random' - self.logger.info(f"Mapping IPs done:{mapping_ips}") + mapping_nets, mapping_ips = self._create_new_network_mapping(max_attempts) - # update ALL data structure in the environment with the new mappings + # update ALL data structure in the environment with the new mappings # self._networks new_self_networks = {} for net, ips in self._networks.items(): @@ -530,6 +482,67 @@ def repl(match): for ip, mapping in self._ip_mapping.items(): self._ip_mapping[ip] = mapping_ips[mapping] self.logger.debug(f"self._ip_mapping: {self._ip_mapping}") + + + def _create_new_network_mapping(self, max_attempts:int=10)->tuple: + """ Method that generates random IP and Network addreses + while following the topology loaded in the environment. + All internal data structures are updated with the newly generated addresses.""" + fake = self._faker_object + mapping_nets = {} + mapping_ips = {} + # generate mapping for networks + private_nets = [] + for net in self._networks.keys(): + if netaddr.IPNetwork(str(net)).ip.is_private(): + private_nets.append(net) + else: + mapping_nets[net] = Network(fake.ipv4_public(), net.mask) + + # for private networks, we want to keep the distances among them + private_nets_sorted = sorted(private_nets) + valid_valid_network_mapping = False + counter_iter = 0 + while not valid_valid_network_mapping: + try: + # find the new lowest networks + new_base = netaddr.IPNetwork(f"{fake.ipv4_private()}/{private_nets_sorted[0].mask}") + # store its new mapping + mapping_nets[private_nets[0]] = Network(str(new_base.network), private_nets_sorted[0].mask) + base = netaddr.IPNetwork(str(private_nets_sorted[0])) + is_private_net_checks = [] + for i in range(1,len(private_nets_sorted)): + current = netaddr.IPNetwork(str(private_nets_sorted[i])) + # find the distance before mapping + diff_ip = current.ip - base.ip + # find the new mapping + new_net_addr = netaddr.IPNetwork(str(mapping_nets[private_nets_sorted[0]])).ip + diff_ip + # evaluate if its still a private network + is_private_net_checks.append(new_net_addr.is_private()) + # store the new mapping + mapping_nets[private_nets_sorted[i]] = Network(str(new_net_addr), private_nets_sorted[i].mask) + if False not in is_private_net_checks: # verify that ALL new networks are still in the private ranges + valid_valid_network_mapping = True + except IndexError as e: + self.logger.info(f"Dynamic address sampling failed, re-trying. {e}") + counter_iter +=1 + if counter_iter > max_attempts: + self.logger.error(f"Dynamic address failed more than {max_attempts} times - stopping.") + exit(-1) + # Invalid IP address boundary + self.logger.info(f"New network mapping:{mapping_nets}") + + # genereate mapping for ips: + for net,ips in self._networks.items(): + ip_list = list(netaddr.IPNetwork(str(mapping_nets[net])))[1:] + # remove broadcast and network ip from the list + random.shuffle(ip_list) + for i,ip in enumerate(ips): + mapping_ips[ip] = IP(str(ip_list[i])) + # Always add random, in case random is selected for ips + mapping_ips['random'] = 'random' + self.logger.info(f"Mapping IPs done:{mapping_ips}") + return mapping_nets, mapping_ips def _get_services_from_host(self, host_ip:str, controlled_hosts:set)-> set: """ @@ -954,7 +967,7 @@ async def reset(self)->bool: if self.task_config.get_use_dynamic_addresses(): if all(self._randomize_topology_requests.values()): self.logger.info("All agents requested reset with randomized topology.") - self._create_new_network_mapping() + self._dynamic_ip_change() else: self.logger.info("Not all agents requested a topology randomization. Keeping the current one.") # reset self._data to orignal state From cdad6e38628f1c7fa01cdb6d467345e3a2760c1e Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 17:34:46 +0200 Subject: [PATCH 12/31] Update mapping of the start view and win conditions --- .../worlds/NSEGameCoordinator.py | 177 ++++++++++++------ 1 file changed, 122 insertions(+), 55 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 5aa8406e..14f60293 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -10,6 +10,7 @@ from faker import Faker from pathlib import Path from typing import Iterable +from collections import defaultdict from AIDojoCoordinator.game_components import GameState, Action, ActionType, IP, Network, Data, Service from AIDojoCoordinator.coordinator import GameCoordinator @@ -388,10 +389,10 @@ def _dynamic_ip_change(self, max_attempts:int=10)-> None: """ self.logger.info("Changing IP and Network addresses in the environment") # find a new IP and network mapping - mapping_nets, mapping_ips = self._create_new_network_mapping(max_attempts) - # update ALL data structure in the environment with the new mappings + # update ALL data structure in the environment with the new mappings + # self._networks new_self_networks = {} for net, ips in self._networks.items(): @@ -409,7 +410,7 @@ def _dynamic_ip_change(self, max_attempts:int=10)-> None: self.logger.debug(f"New FW: {new_self_firewall_original}") self._firewall_original = new_self_firewall_original - #self._ip_to_hostname + # self._ip_to_hostname new_self_ip_to_hostname = {} for ip, hostname in self._ip_to_hostname.items(): new_self_ip_to_hostname[mapping_ips[ip]] = hostname @@ -420,60 +421,123 @@ def _dynamic_ip_change(self, max_attempts:int=10)-> None: for ip in self.hosts_to_start: new_self_host_to_start.append(mapping_ips[ip]) self.hosts_to_start = new_self_host_to_start + + def apply_mapping(d: dict, mapping: dict) -> dict: + """ + Apply a mapping to a dictionary. + - Keys are remapped with mapping if present. + - Values: + * If iterable (set/list/tuple), each element is remapped. + * If string (or non-iterable), attempt direct remap. + """ + out = defaultdict(set) + for k, vals in d.items(): + nk = mapping.get(k, k) + + if isinstance(vals, str) or not isinstance(vals, Iterable): + # treat as a single atomic value + nv = {mapping.get(vals, vals)} + else: + nv = {mapping.get(v, v) for v in vals} + + out[nk].update(nv) + + return dict(out) + + # start_position per role + for role, start_position in self._starting_positions_per_role.items(): + # {'role': {'controlled_hosts': [...], 'known_hosts': [...], 'known_data': {...}, 'known_services': {...}, known_networks: [...], known_blocks: [...]}} + new_start_position = {} + new_start_position['known_networks'] = [mapping_nets.get(net, net) for net in start_position['known_networks']] + new_start_position['controlled_hosts'] = [mapping_ips.get(ip, ip) for ip in start_position['controlled_hosts']] + new_start_position['known_hosts'] = [mapping_ips.get(ip, ip) for ip in start_position['known_hosts']] + new_start_position['known_services'] = {mapping_ips.get(ip, ip): services for ip, services in start_position['known_services'].items()} + new_start_position["known_data"] = {mapping_ips.get(ip, ip): data for ip, data in start_position['known_data'].items()} + # known_blocks {IP:set(IP)} + new_start_position["known_blocks"] = apply_mapping(start_position.get("known_blocks", {}), mapping_ips) + self._starting_positions_per_role[role] = new_start_position + self.logger.debug(f"Updated starting position for role {role}: {self._starting_positions_per_role[role]}") + + # win_conditions_per_role + for role, win_condition in self._win_conditions_per_role.items(): + new_win_condition = {} + new_win_condition['known_networks'] = [mapping_nets.get(net, net) for net in win_condition['known_networks']] + new_win_condition['controlled_hosts'] = [mapping_ips.get(ip, ip) for ip in win_condition['controlled_hosts']] + new_win_condition['known_hosts'] = [mapping_ips.get(ip, ip) for ip in win_condition['known_hosts']] + new_win_condition['known_services'] = {mapping_ips.get(ip, ip): services for ip, services in win_condition['known_services'].items()} + new_win_condition["known_data"] = {mapping_ips.get(ip, ip): data for ip, data in win_condition['known_data'].items()} + new_win_condition["known_blocks"] = apply_mapping(win_condition.get("known_blocks", {}), mapping_ips) + self._win_conditions_per_role[role] = new_win_condition + self.logger.debug(f"Updated win condition for role {role}: {self._win_conditions_per_role[role]}") - # map IPs and networks stored in the taskconfig file - # This is a quick fix, we should find some other solution - agents = self.task_config.config['coordinator']['agents'] - # Fields that are dictionaries with IP keys - dict_keys = ['known_data', 'blocked_ips', 'known_blocks'] - # Fields that are lists of IP strings - list_keys = ['known_hosts', 'controlled_hosts'] + # goal_description_per_role ip_regex = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b') + def repl(match): + ip_str = match.group(0) + try: + new_ip = str(mapping_ips[IP(ip_str)]) + return new_ip + except (ValueError, KeyError): + return ip_str + + new_goal_description = {role:repl(description) for role, description in self._goal_description_per_role.items()} + self._goal_description_per_role = new_goal_description + self.logger.debug(f"Updated goal description per role: {self._goal_description_per_role}") + + + # # map IPs and networks stored in the taskconfig file + # # This is a quick fix, we should find some other solution + # agents = self.task_config.config['coordinator']['agents'] + # # Fields that are dictionaries with IP keys + # dict_keys = ['known_data', 'blocked_ips', 'known_blocks'] + # # Fields that are lists of IP strings + # list_keys = ['known_hosts', 'controlled_hosts'] + # ip_regex = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b') - for agent in agents.values(): - for section_key in ['goal', 'start_position']: - section = agent.get(section_key, {}) - - # Remap IP addresses in the description field of the goal section - if section_key == 'goal' and 'description' in section: - description = section['description'] - def repl(match): - ip_str = match.group(0) - try: - new_ip = str(mapping_ips[IP(ip_str)]) - return new_ip - except (ValueError, KeyError): - return ip_str - section['description'] = ip_regex.sub(repl, description) - - # Remap dictionary keys - for key in dict_keys: - if key in section: - current_dict = section[key] - for ip in list(current_dict.keys()): - try: - # Convert the ip string to an IP object - new_ip = str(mapping_ips[IP(ip)]) - except (ValueError, KeyError): - # Skip if the IP is invalid or not found in mapping_ips - continue - current_dict[new_ip] = current_dict.pop(ip) - - # Remap list items - for key in list_keys: - if key in section: - new_list = [] - for ip in section[key]: - try: - new_ip = str(mapping_ips[IP(ip)]) - except (ValueError, KeyError): - # Keep the original if invalid or not in mapping_ips - new_ip = ip - new_list.append(new_ip) - section[key] = new_list - # update win conditions with the new IPs - self._win_conditions_per_role = self._get_win_condition_per_role() - self._goal_description_per_role = self._get_goal_description_per_role() + # for agent in agents.values(): + # for section_key in ['goal', 'start_position']: + # section = agent.get(section_key, {}) + + # # Remap IP addresses in the description field of the goal section + # if section_key == 'goal' and 'description' in section: + # description = section['description'] + # def repl(match): + # ip_str = match.group(0) + # try: + # new_ip = str(mapping_ips[IP(ip_str)]) + # return new_ip + # except (ValueError, KeyError): + # return ip_str + # section['description'] = ip_regex.sub(repl, description) + + # # Remap dictionary keys + # for key in dict_keys: + # if key in section: + # current_dict = section[key] + # for ip in list(current_dict.keys()): + # try: + # # Convert the ip string to an IP object + # new_ip = str(mapping_ips[IP(ip)]) + # except (ValueError, KeyError): + # # Skip if the IP is invalid or not found in mapping_ips + # continue + # current_dict[new_ip] = current_dict.pop(ip) + + # # Remap list items + # for key in list_keys: + # if key in section: + # new_list = [] + # for ip in section[key]: + # try: + # new_ip = str(mapping_ips[IP(ip)]) + # except (ValueError, KeyError): + # # Keep the original if invalid or not in mapping_ips + # new_ip = ip + # new_list.append(new_ip) + # section[key] = new_list + # # update win conditions with the new IPs + # self._win_conditions_per_role = self._get_win_condition_per_role() + # self._goal_description_per_role = self._get_goal_description_per_role() #update mappings stored in the environment for net, mapping in self._network_mapping.items(): @@ -539,8 +603,11 @@ def _create_new_network_mapping(self, max_attempts:int=10)->tuple: random.shuffle(ip_list) for i,ip in enumerate(ips): mapping_ips[ip] = IP(str(ip_list[i])) - # Always add random, in case random is selected for ips + # Always add keywords 'random' and 'all_local' 'all_attackers' to the mapping mapping_ips['random'] = 'random' + mapping_ips['all_local'] = 'all_local' + mapping_ips['all_attackers'] = 'all_attackers' + self.logger.info(f"Mapping IPs done:{mapping_ips}") return mapping_nets, mapping_ips From 4c461dc6c8ac51c21c53045ae1153e39a73d956e Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 17:47:52 +0200 Subject: [PATCH 13/31] fix goal description mapping --- .../worlds/NSEGameCoordinator.py | 90 ++++++------------- 1 file changed, 25 insertions(+), 65 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 14f60293..3e63a27f 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -471,75 +471,35 @@ def apply_mapping(d: dict, mapping: dict) -> dict: self.logger.debug(f"Updated win condition for role {role}: {self._win_conditions_per_role[role]}") # goal_description_per_role - ip_regex = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b') - def repl(match): - ip_str = match.group(0) - try: - new_ip = str(mapping_ips[IP(ip_str)]) - return new_ip - except (ValueError, KeyError): - return ip_str + def replace_ips_in_text(text: str, ip_mapping: dict, net_mapping:dict) -> str: + """ + Replace IPs/CIDRs in text according to mapping {IP: IP}. + """ + # regex: matches IPv4 like 1.2.3.4 or 1.2.3.4/24 + ip_pattern = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}(?:/\d{1,2})?\b") + + def replacer(match): + token = match.group(0) + if "/" in token: + try: + net_obj = Network(*token.split("/")) + return str(net_mapping.get(net_obj, token)) + except ValueError: + return token + else: + try: + net_obj = IP(token) + return str(ip_mapping.get(net_obj, token)) + except ValueError: + return token - new_goal_description = {role:repl(description) for role, description in self._goal_description_per_role.items()} + return ip_pattern.sub(replacer, text) + + new_goal_description = {role:replace_ips_in_text(description, mapping_ips, mapping_nets) for role, description in self._goal_description_per_role.items()} self._goal_description_per_role = new_goal_description self.logger.debug(f"Updated goal description per role: {self._goal_description_per_role}") - - - # # map IPs and networks stored in the taskconfig file - # # This is a quick fix, we should find some other solution - # agents = self.task_config.config['coordinator']['agents'] - # # Fields that are dictionaries with IP keys - # dict_keys = ['known_data', 'blocked_ips', 'known_blocks'] - # # Fields that are lists of IP strings - # list_keys = ['known_hosts', 'controlled_hosts'] - # ip_regex = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b') - - # for agent in agents.values(): - # for section_key in ['goal', 'start_position']: - # section = agent.get(section_key, {}) - - # # Remap IP addresses in the description field of the goal section - # if section_key == 'goal' and 'description' in section: - # description = section['description'] - # def repl(match): - # ip_str = match.group(0) - # try: - # new_ip = str(mapping_ips[IP(ip_str)]) - # return new_ip - # except (ValueError, KeyError): - # return ip_str - # section['description'] = ip_regex.sub(repl, description) - - # # Remap dictionary keys - # for key in dict_keys: - # if key in section: - # current_dict = section[key] - # for ip in list(current_dict.keys()): - # try: - # # Convert the ip string to an IP object - # new_ip = str(mapping_ips[IP(ip)]) - # except (ValueError, KeyError): - # # Skip if the IP is invalid or not found in mapping_ips - # continue - # current_dict[new_ip] = current_dict.pop(ip) - - # # Remap list items - # for key in list_keys: - # if key in section: - # new_list = [] - # for ip in section[key]: - # try: - # new_ip = str(mapping_ips[IP(ip)]) - # except (ValueError, KeyError): - # # Keep the original if invalid or not in mapping_ips - # new_ip = ip - # new_list.append(new_ip) - # section[key] = new_list - # # update win conditions with the new IPs - # self._win_conditions_per_role = self._get_win_condition_per_role() - # self._goal_description_per_role = self._get_goal_description_per_role() - #update mappings stored in the environment + # update mappings stored in the environment for net, mapping in self._network_mapping.items(): self._network_mapping[net] = mapping_nets[mapping] self.logger.debug(f"self._network_mapping: {self._network_mapping}") From f222d3dabaea0448a7cd222c17ddfd3348616f9b Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 18:45:45 +0200 Subject: [PATCH 14/31] add generation of win_state to registration method --- AIDojoCoordinator/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index bcbe8d21..f85b0d45 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -764,7 +764,7 @@ def _initialize_new_player(self, agent_addr:tuple, agent_current_state:GameState # create initial observation return Observation(self._agent_states[agent_addr], 0, False, {}) - async def register_agent(self, agent_id:tuple, agent_role:str, agent_initial_view:dict)->GameState: + async def register_agent(self, agent_id:tuple, agent_role:str, agent_initial_view:dict, agent_win_condition_view:dict)->tuple[GameState, GameState]: """ Domain specific method of the environment. Creates the initial state of the agent. """ From 4a56a7fb0c6253eaceeadebe61b7d842e1c44c99 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 18:46:01 +0200 Subject: [PATCH 15/31] Add generation of win_state --- .../worlds/NSEGameCoordinator.py | 109 +++++++++++++----- 1 file changed, 83 insertions(+), 26 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 3e63a27f..79b6eab8 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -45,7 +45,7 @@ def __init__(self, game_host, game_port, task_config:str, allowed_roles=["Attack self._seed = seed self.logger.info(f'Setting env seed to {seed}') - def _initialize(self) -> None: + def _initialize(self): """ Initializes the NetSecGame environment. @@ -69,38 +69,44 @@ def _initialize(self) -> None: self._firewall_original = copy.deepcopy(self._firewall) self.logger.info("Environment initialization finished") - def _get_controlled_hosts_from_view(self, view_controlled_hosts:Iterable, hosts_to_start=None)->set: + def _get_hosts_from_view(self, view_hosts:Iterable, allowed_hosts=None)->set[IP]: """ Parses view and translates all keywords. Produces set of controlled host (IP) + Args: + view_hosts (Iterable): The view containing host information. + allowed_hosts (list, optional): A list of host to start from if 'random' is specified. Defaults to None. + Returns: + set: A set of controlled hosts. """ - controlled_hosts = set() + hosts = set() + self.logger.debug(f'\tParsing from view: {view_hosts}') # controlled_hosts - for host in view_controlled_hosts: + for host in view_hosts: if isinstance(host, IP): - controlled_hosts.add(self._ip_mapping[host]) - self.logger.debug(f'\tThe attacker has control of host {self._ip_mapping[host]}.') + hosts.add(self._ip_mapping[host]) + self.logger.debug(f'\tAdding {self._ip_mapping[host]}.') elif host == 'random': # Random start - self.logger.debug('\tAdding random starting position of agent') - if hosts_to_start is not None: - self.logger.debug(f'\t\tChoosing from {self.hosts_to_start}') - selected = random.choice(self.hosts_to_start) + if allowed_hosts is not None: + self.logger.debug(f'\tChoosing randomly from {allowed_hosts}') + selected = random.choice(allowed_hosts) else: + self.logger.debug(f'\tChoosing randomly from all available hosts {list(self._ip_to_hostname.keys())}') selected = random.choice(list(self._ip_to_hostname.keys())) - controlled_hosts.add(selected) - self.logger.debug(f'\t\tMaking agent start in {selected}') + hosts.add(selected) + self.logger.debug(f'\t\tAdding {selected}.') elif host == "all_local": # all local ips - self.logger.debug('\t\tAdding all local hosts to agent') - controlled_hosts = controlled_hosts.union(self._get_all_local_ips()) + self.logger.debug(f'\tAdding all local hosts') + hosts = hosts.union(self._get_all_local_ips()) else: - self.logger.error(f"Unsupported value encountered in start_position['controlled_hosts']: {host}") - return controlled_hosts + self.logger.error(f"Unsupported value encountered in view_hosts: {host}") + return hosts def _get_services_from_view(self, view_known_services:dict)->dict: """ Parses view and translates all keywords. Produces dict of known services {IP: set(Service)} - + Args: view_known_services (dict): The view containing known services information. @@ -152,6 +158,59 @@ def _get_data_from_view(self, view_known_data:dict)->dict: self.logger.warning("\tNo available data. Skipping") return known_data + def _get_networks_from_view(self, view_known_networks:Iterable)->set[Network]: + """ + Parses view and translates all keywords. Produces set of known networks (Network). + Args: + view_known_networks (Iterable): The view containing known networks information. + Returns: + set: A set of known networks. + """ + known_networks = set() + for net in view_known_networks: + if isinstance(net, Network): + known_networks.add(self._network_mapping[net]) + self.logger.debug(f'\tAdding network {self._network_mapping[net]}.') + elif net == 'random': + # Randomly select a network from the available ones + selected = random.choice(list(self._networks.keys())) + known_networks.add(self._network_mapping[selected]) + self.logger.debug(f'\tAdding randomly selected network: {self._network_mapping[selected]}.') + elif net == "all_local": + # all local networks + self.logger.debug('\t\tAdding all local private networks') + for n in self._networks.keys(): + if not n.is_private(): + known_networks.add(self._network_mapping[n]) + else: + self.logger.error(f"Unsupported value encountered in start_position['known_networks']: {net}") + return known_networks + + + def _create_goal_state_from_view(self, view:dict, allowed_hosts=None)->GameState: + """ + Builds a GameState from given view (dict). All keywords are replaced by valid options. + Args: + view (dict): The view containing goal state information. + allowed_hosts (set, optional): A set of allowed hosts for random selection. Defaults to None. + Returns: + GameState: The generated goal state. + """ + self.logger.info(f'Generating goal state from view:{view}') + # process known networks + known_networks = self._get_networks_from_view(view_known_networks=view["known_networks"]) + # parse controlled hosts, replacing keywords if present + controlled_hosts = self._get_hosts_from_view(view_hosts=view["controlled_hosts"], allowed_hosts=allowed_hosts) + # parse known hosts + known_hosts = self._get_hosts_from_view(view_hosts=view["known_hosts"]) + # parse known services + known_services = self._get_services_from_view(view["known_services"]) + # parse known data + known_data = self._get_data_from_view(view["known_data"]) + goal_state = GameState(controlled_hosts, known_hosts, known_services, known_data, known_networks) + self.logger.info(f"Generated Goal GameState:{goal_state}") + return goal_state + def _create_state_from_view(self, view:dict, add_neighboring_nets:bool=True)->GameState: """ Builds a GameState from given view. @@ -161,10 +220,10 @@ def _create_state_from_view(self, view:dict, add_neighboring_nets:bool=True)->Ga """ self.logger.info(f'Generating state from view:{view}') # re-map all networks based on current mapping in self._network_mapping - known_networks = set([self._network_mapping[net] for net in view["known_networks"]]) + known_networks = self._get_networks_from_view(view_known_networks=view["known_networks"]) # parse controlled hosts - controlled_hosts = self._get_controlled_hosts_from_view(view_controlled_hosts=view["controlled_hosts"], hosts_to_start=self.hosts_to_start) - known_hosts = set([self._ip_mapping[ip] for ip in view["known_hosts"]]) + controlled_hosts = self._get_hosts_from_view(view_hosts=view["controlled_hosts"], allowed_hosts=self.hosts_to_start) + known_hosts = self._get_hosts_from_view(view_hosts=view["known_hosts"], allowed_hosts=self.hosts_to_start) # Add all controlled hosts to known_hosts known_hosts = known_hosts.union(controlled_hosts) if add_neighboring_nets: @@ -381,8 +440,6 @@ def process_firewall()->dict: self.logger.info(f"\tintitial self._ip_mapping: {self._ip_mapping}") self.logger.info("CYST configuration processed successfully") - - def _dynamic_ip_change(self, max_attempts:int=10)-> None: """ Changes the IP and network addresses in the environment @@ -507,7 +564,6 @@ def replacer(match): self._ip_mapping[ip] = mapping_ips[mapping] self.logger.debug(f"self._ip_mapping: {self._ip_mapping}") - def _create_new_network_mapping(self, max_attempts:int=10)->tuple: """ Method that generates random IP and Network addreses while following the topology loaded in the environment. @@ -967,10 +1023,11 @@ def update_log_file(self, known_data:set, action, target_host:IP): new_content = json.dumps(new_content) self._data[hostaname].add(Data(owner="system", id="logfile", type="log", size=len(new_content) , content= new_content)) - async def register_agent(self, agent_id, agent_role, agent_initial_view)->GameState: + async def register_agent(self, agent_id, agent_role, agent_initial_view)->tuple[GameState, GameState]: game_state = self._create_state_from_view(agent_initial_view) - return game_state - + goal_state = self._create_state_from_view(self._goal_description_per_role[agent_role]) + return game_state, goal_state + async def remove_agent(self, agent_id, agent_state)->bool: # No action is required return True From a8931a2b239c408e3cd59db025f26f523e102d5a Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 18:55:10 +0200 Subject: [PATCH 16/31] process return tuple --- AIDojoCoordinator/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index f85b0d45..1b526d80 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -462,7 +462,7 @@ async def _process_join_game_action(self, agent_addr: tuple, action: Action)->No agent_role = action.parameters["agent_info"].role if agent_role in self.ALLOWED_ROLES: # add agent to the world - new_agent_game_state = await self.register_agent(agent_addr, agent_role, self._starting_positions_per_role[agent_role]) + new_agent_game_state, new_agent_goal_state = await self.register_agent(agent_addr, agent_role, self._starting_positions_per_role[agent_role], self._win_conditions_per_role[agent_role]) if new_agent_game_state: # successful registration async with self._agents_lock: self.agents[agent_addr] = (agent_name, agent_role) From cac9eaddfb287106fef23e14618e57df9a809644 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 18:55:41 +0200 Subject: [PATCH 17/31] Generate goal state at registration --- AIDojoCoordinator/worlds/NSEGameCoordinator.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 79b6eab8..a3c62bdd 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -153,9 +153,11 @@ def _get_data_from_view(self, view_known_data:dict)->dict: data_candidates = [d for d in self._data[self._ip_to_hostname[ip]] if d not in known_data[self._ip_mapping[ip]]] if len(data_candidates) > 0: # randomly select from candidates - known_data[self._ip_mapping[ip]].add(random.choice(data_candidates)) + selected = random.choice(data_candidates) + self.logger.info(f"\t\tAdding: {selected}") + known_data[self._ip_mapping[ip]].add(selected) else: - self.logger.warning("\tNo available data. Skipping") + self.logger.warning("\t\tNo available data. Skipping") return known_data def _get_networks_from_view(self, view_known_networks:Iterable)->set[Network]: @@ -1023,10 +1025,10 @@ def update_log_file(self, known_data:set, action, target_host:IP): new_content = json.dumps(new_content) self._data[hostaname].add(Data(owner="system", id="logfile", type="log", size=len(new_content) , content= new_content)) - async def register_agent(self, agent_id, agent_role, agent_initial_view)->tuple[GameState, GameState]: - game_state = self._create_state_from_view(agent_initial_view) - goal_state = self._create_state_from_view(self._goal_description_per_role[agent_role]) - return game_state, goal_state + async def register_agent(self, agent_id, agent_role, agent_initial_view:dict, agent_win_condition_view:dict)->tuple[GameState, GameState]: + start_game_state = self._create_state_from_view(agent_initial_view) + goal_state = self._create_goal_state_from_view(agent_win_condition_view) + return start_game_state, goal_state async def remove_agent(self, agent_id, agent_state)->bool: # No action is required From 168a70fbd716d770bf61b3846411eeb4eebcd049 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 19:00:13 +0200 Subject: [PATCH 18/31] register the goal state --- AIDojoCoordinator/coordinator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index 1b526d80..d0b57a76 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -180,6 +180,8 @@ def __init__(self, game_host: str, game_port: int, service_host:str, service_por self._agent_starting_position = {} # current state per agent_addr (GameState) self._agent_states = {} + # goal state per agent_addr (GameState) + self._agent_goal_states = {} # last action played by agent (Action) self._agent_last_action = {} # False positives per agent (due to added blocks) @@ -741,7 +743,7 @@ async def _reset_game(self): self._reset_done_condition.notify_all() self.logger.info("\tReset game task stopped.") - def _initialize_new_player(self, agent_addr:tuple, agent_current_state:GameState) -> Observation: + def _initialize_new_player(self, agent_addr:tuple, agent_current_state:GameState, agent_current_goal_state:GameState) -> Observation: """ Method to initialize new player upon joining the game. Returns initial observation for the agent based on the agent's role @@ -753,6 +755,8 @@ def _initialize_new_player(self, agent_addr:tuple, agent_current_state:GameState self._episode_ends[agent_addr] = False self._agent_starting_position[agent_addr] = self._starting_positions_per_role[agent_role] self._agent_states[agent_addr] = agent_current_state + self._agent_goal_states[agent_addr] = agent_current_goal_state + self._agent_last_action[agent_addr] = None self._agent_rewards[agent_addr] = 0 self._agent_false_positives[agent_addr] = 0 if agent_role.lower() == "attacker": From b95fdd0df965c577304700cc16732f8e7a3ca02b Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 19:00:21 +0200 Subject: [PATCH 19/31] Add note --- AIDojoCoordinator/worlds/NSEGameCoordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index a3c62bdd..7ffe531e 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -139,6 +139,7 @@ def _get_data_from_view(self, view_known_data:dict)->dict: Returns: dict: A dictionary mapping IP addresses to sets of known data. """ + # TODO Should we omit certain data types (e.g., logs)? known_data = {} for ip, data_list in view_known_data.items(): if self._ip_mapping[ip] not in known_data: From 39c55671f1fce479fb6dbf30c29c1b5411d6abfe Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 19:01:50 +0200 Subject: [PATCH 20/31] Add goal state to agent reset method --- AIDojoCoordinator/coordinator.py | 4 ++-- AIDojoCoordinator/worlds/NSEGameCoordinator.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index d0b57a76..c803a880 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -779,8 +779,8 @@ async def remove_agent(self, agent_id:tuple, agent_state:GameState)->bool: Domain specific method of the environment. Creates the initial state of the agent. """ raise NotImplementedError - - async def reset_agent(self, agent_id:tuple, agent_role:str, agent_initial_view:dict)->GameState: + + async def reset_agent(self, agent_id:tuple, agent_role:str, agent_initial_view:dict, agent_win_condition_view:dict)->tuple[GameState, GameState]: raise NotImplementedError async def _remove_agent_from_game(self, agent_addr): diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 7ffe531e..e2de4703 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -1037,10 +1037,11 @@ async def remove_agent(self, agent_id, agent_state)->bool: async def step(self, agent_id, agent_state, action)->GameState: return self._execute_action(agent_state, action, agent_id) - - async def reset_agent(self, agent_id, agent_role, agent_initial_view)->GameState: + + async def reset_agent(self, agent_id, agent_role, agent_initial_view:dict, agent_win_condition_view:dict)->tuple[GameState, GameState]: game_state = self._create_state_from_view(agent_initial_view) - return game_state + goal_state = self._create_goal_state_from_view(agent_win_condition_view) + return game_state, goal_state async def reset(self)->bool: """ From f69ba5f3d66646beab09a995c954beaf2227331d Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 19:03:09 +0200 Subject: [PATCH 21/31] Add processing of the goal state per reset --- AIDojoCoordinator/coordinator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index c803a880..29c6b2e6 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -722,10 +722,11 @@ async def _reset_game(self): async with self._agents_lock: self._store_trajectory_to_file(agent) self.logger.debug(f"Resetting agent {agent}") - new_state = await self.reset_agent(agent, self.agents[agent][1], self._agent_starting_position[agent]) + new_state, new_goal_state = await self.reset_agent(agent, self.agents[agent][1], self._agent_starting_position[agent], self._win_conditions_per_role[self.agents[agent][1]]) new_observation = Observation(new_state, 0, False, {}) async with self._agents_lock: self._agent_states[agent] = new_state + self._agent_goal_states[agent] = new_goal_state self._agent_observations[agent] = new_observation self._episode_ends[agent] = False self._reset_requests[agent] = False From f24dd00ca2bf78c0631ddb3b072f94b4f236410d Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 19:03:55 +0200 Subject: [PATCH 22/31] remove goal_state when removing the agent from the game --- AIDojoCoordinator/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index 29c6b2e6..ee0a1007 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -793,6 +793,7 @@ async def _remove_agent_from_game(self, agent_addr): async with self._agents_lock: if agent_addr in self.agents: agent_info["state"] = self._agent_states.pop(agent_addr) + agent_info["goal_state"] = self._agent_goal_states.pop(agent_addr) agent_info["num_steps"] = self._agent_steps.pop(agent_addr) agent_info["agent_status"] = self._agent_status.pop(agent_addr) agent_info["false_positives"] = self._agent_false_positives.pop(agent_addr) From f1953c20a780d7571f758fd74844d634ee36d71d Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 19:07:24 +0200 Subject: [PATCH 23/31] forward the created goal state --- AIDojoCoordinator/coordinator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index ee0a1007..5e892087 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -468,7 +468,7 @@ async def _process_join_game_action(self, agent_addr: tuple, action: Action)->No if new_agent_game_state: # successful registration async with self._agents_lock: self.agents[agent_addr] = (agent_name, agent_role) - observation = self._initialize_new_player(agent_addr, new_agent_game_state) + observation = self._initialize_new_player(agent_addr, new_agent_game_state, new_agent_goal_state) self._agent_observations[agent_addr] = observation #if len(self.agents) == self._min_required_players: if sum(1 for v in self._agent_status.values() if v == AgentStatus.PlayingWithTimeout) >= self._min_required_players: @@ -722,7 +722,9 @@ async def _reset_game(self): async with self._agents_lock: self._store_trajectory_to_file(agent) self.logger.debug(f"Resetting agent {agent}") - new_state, new_goal_state = await self.reset_agent(agent, self.agents[agent][1], self._agent_starting_position[agent], self._win_conditions_per_role[self.agents[agent][1]]) + agent_role = self.agents[agent][1] + # reset the agent in the world + new_state, new_goal_state = await self.reset_agent(agent, agent_role, self._starting_positions_per_role[agent_role], self._win_conditions_per_role[agent_role]) new_observation = Observation(new_state, 0, False, {}) async with self._agents_lock: self._agent_states[agent] = new_state From e5ad8dd20459888e36796749f163c9ebe7d0ec6d Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 21:09:18 +0200 Subject: [PATCH 24/31] fix parsing hosts, data, and services from view --- .../worlds/NSEGameCoordinator.py | 69 ++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index e2de4703..57289f5b 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -79,12 +79,12 @@ def _get_hosts_from_view(self, view_hosts:Iterable, allowed_hosts=None)->set[IP] set: A set of controlled hosts. """ hosts = set() - self.logger.debug(f'\tParsing from view: {view_hosts}') + self.logger.debug(f'\tParsing hosts from view: {view_hosts}') # controlled_hosts for host in view_hosts: if isinstance(host, IP): - hosts.add(self._ip_mapping[host]) - self.logger.debug(f'\tAdding {self._ip_mapping[host]}.') + hosts.add(host) + self.logger.debug(f'\tAdding {host}.') elif host == 'random': # Random start if allowed_hosts is not None: @@ -115,18 +115,30 @@ def _get_services_from_view(self, view_known_services:dict)->dict: """ known_services = {} for ip, service_list in view_known_services.items(): - if self._ip_mapping[ip] not in known_services: - known_services[self._ip_mapping[ip]] = set() - for s in service_list: - if isinstance(s, Service): - known_services[self._ip_mapping[ip]].add(s) - elif isinstance(s, str): - if s == "random": # randomly select the service - self.logger.info(f"\tSelecting service randomly in {self._ip_mapping[ip]}") + self.logger.debug(f'\tParsing services from {ip}: {service_list}') + known_services[ip] = set() + for service in service_list: + if isinstance(service, Service): + known_services[ip].add(service) + self.logger.debug(f'\tAdding {service}.') + elif isinstance(service, str): + if service == "random": # randomly select the service + self.logger.info(f"\tSelecting service randomly in {ip}") # select candidates that are not explicitly listed - service_candidates = [s for s in self._services[self._ip_to_hostname[ip]] if s not in known_services[self._ip_mapping[ip]]] - # randomly select from candidates - known_services[self._ip_mapping[ip]].add(random.choice(service_candidates)) + service_candidates = [s for s in self._services[self._ip_to_hostname[ip]] if s not in known_services[ip]] + if len(service_candidates) == 0: + self.logger.warning("\t\tNo available services. Skipping") + else: + # randomly select from candidates + selected = random.choice(service_candidates) + self.logger.debug(f"\t\tAdding: {selected}") + known_services[ip].add(selected) + elif service == "all": + self.logger.info(f"\tSelecting all services in {ip}") + known_services[ip].update(self._services[self._ip_to_hostname[ip]]) + else: + self.logger.error(f"Unsupported value encountered in view_known_services: {service}") + # re-map all IPs based on current mapping in self._ip_mapping return known_services def _get_data_from_view(self, view_known_data:dict)->dict: @@ -142,23 +154,30 @@ def _get_data_from_view(self, view_known_data:dict)->dict: # TODO Should we omit certain data types (e.g., logs)? known_data = {} for ip, data_list in view_known_data.items(): - if self._ip_mapping[ip] not in known_data: - known_data[self._ip_mapping[ip]] = set() + self.logger.debug(f'\tParsing data from {ip}: {data_list}') + known_data[ip] = set() for datum in data_list: if isinstance(datum, Data): - known_data[self._ip_mapping[ip]].add(datum) + known_data[ip].add(datum) + self.logger.debug(f'\tAdding {datum}.') elif isinstance(datum, str): - if datum == "random": # randomly select the data - self.logger.info(f"\tSelecting data randomly in {self._ip_mapping[ip]}") + if datum == "random": # randomly select the service + self.logger.info(f"\tSelecting data randomly in {ip}") # select candidates that are not explicitly listed - data_candidates = [d for d in self._data[self._ip_to_hostname[ip]] if d not in known_data[self._ip_mapping[ip]]] - if len(data_candidates) > 0: + data_candidates = [d for d in self._data[self._ip_to_hostname[ip]] if d not in known_data[ip]] + if len(data_candidates) == 0: + self.logger.warning("\t\tNo available data. Skipping") + else: # randomly select from candidates selected = random.choice(data_candidates) - self.logger.info(f"\t\tAdding: {selected}") - known_data[self._ip_mapping[ip]].add(selected) - else: - self.logger.warning("\t\tNo available data. Skipping") + self.logger.debug(f"\t\tAdding: {selected}") + known_data[ip].add(selected) + elif datum == "all": + self.logger.info(f"\tSelecting all data in {ip}") + known_data[ip].update(self._data[self._ip_to_hostname[ip]]) + else: + self.logger.error(f"Unsupported value encountered in view_known_data: {datum}") + # re-map all IPs based on current mapping in self._ip_mapping return known_data def _get_networks_from_view(self, view_known_networks:Iterable)->set[Network]: From 56c9eab5cda6c72fef0a7d430072185269d9231e Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 21:26:24 +0200 Subject: [PATCH 25/31] fix data selection from view --- .../worlds/NSEGameCoordinator.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 57289f5b..011a576d 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -141,7 +141,7 @@ def _get_services_from_view(self, view_known_services:dict)->dict: # re-map all IPs based on current mapping in self._ip_mapping return known_services - def _get_data_from_view(self, view_known_data:dict)->dict: + def _get_data_from_view(self, view_known_data:dict, exclude_types=["log"])->dict: """ Parses view and translates all keywords. Produces dict of known data {IP: set(Data)} @@ -161,20 +161,26 @@ def _get_data_from_view(self, view_known_data:dict)->dict: known_data[ip].add(datum) self.logger.debug(f'\tAdding {datum}.') elif isinstance(datum, str): + # select candidates that are not explicitly listed + data_candidates = set() + for datapoints in self._data.values(): + for d in datapoints: + if d.type not in exclude_types and d not in known_data[ip]: + data_candidates.add(d) if datum == "random": # randomly select the service - self.logger.info(f"\tSelecting data randomly in {ip}") - # select candidates that are not explicitly listed - data_candidates = [d for d in self._data[self._ip_to_hostname[ip]] if d not in known_data[ip]] + self.logger.info("\tSelecting data randomly") if len(data_candidates) == 0: self.logger.warning("\t\tNo available data. Skipping") else: # randomly select from candidates - selected = random.choice(data_candidates) + selected = random.choice(list(data_candidates)) self.logger.debug(f"\t\tAdding: {selected}") known_data[ip].add(selected) - elif datum == "all": - self.logger.info(f"\tSelecting all data in {ip}") - known_data[ip].update(self._data[self._ip_to_hostname[ip]]) + elif datum == "all": + self.logger.info(f"\tSelecting all data in {ip}") + known_data[ip].update(data_candidates) + else: + self.logger.error(f"Unsupported value encountered in view_known_data: {datum}") else: self.logger.error(f"Unsupported value encountered in view_known_data: {datum}") # re-map all IPs based on current mapping in self._ip_mapping From 2a6cecdc7dc29fa61700bbe986e4f46791d93787 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 21:35:04 +0200 Subject: [PATCH 26/31] Add keyword scope to enable better definition in get_data_from_view --- AIDojoCoordinator/worlds/NSEGameCoordinator.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 011a576d..516972c3 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -141,7 +141,7 @@ def _get_services_from_view(self, view_known_services:dict)->dict: # re-map all IPs based on current mapping in self._ip_mapping return known_services - def _get_data_from_view(self, view_known_data:dict, exclude_types=["log"])->dict: + def _get_data_from_view(self, view_known_data:dict, keyword_scope:str="host", exclude_types=["log"])->dict: """ Parses view and translates all keywords. Produces dict of known data {IP: set(Data)} @@ -163,10 +163,16 @@ def _get_data_from_view(self, view_known_data:dict, exclude_types=["log"])->dict elif isinstance(datum, str): # select candidates that are not explicitly listed data_candidates = set() - for datapoints in self._data.values(): - for d in datapoints: + if keyword_scope == "host": # scope of the keyword is the host only + for d in self._data[self._ip_to_hostname[ip]]: if d.type not in exclude_types and d not in known_data[ip]: data_candidates.add(d) + else: + # scope of the keyword is all hosts + for datapoints in self._data.values(): + for d in datapoints: + if d.type not in exclude_types and d not in known_data[ip]: + data_candidates.add(d) if datum == "random": # randomly select the service self.logger.info("\tSelecting data randomly") if len(data_candidates) == 0: @@ -214,7 +220,6 @@ def _get_networks_from_view(self, view_known_networks:Iterable)->set[Network]: self.logger.error(f"Unsupported value encountered in start_position['known_networks']: {net}") return known_networks - def _create_goal_state_from_view(self, view:dict, allowed_hosts=None)->GameState: """ Builds a GameState from given view (dict). All keywords are replaced by valid options. @@ -234,7 +239,7 @@ def _create_goal_state_from_view(self, view:dict, allowed_hosts=None)->GameState # parse known services known_services = self._get_services_from_view(view["known_services"]) # parse known data - known_data = self._get_data_from_view(view["known_data"]) + known_data = self._get_data_from_view(view["known_data"], keyword_scope="global", exclude_types=["logs"]) goal_state = GameState(controlled_hosts, known_hosts, known_services, known_data, known_networks) self.logger.info(f"Generated Goal GameState:{goal_state}") return goal_state From 1294c2ae6b76f21d261986e72e76bcacdfdb47f7 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 21:37:00 +0200 Subject: [PATCH 27/31] Fix docstring --- AIDojoCoordinator/worlds/NSEGameCoordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 516972c3..35efed34 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -147,11 +147,11 @@ def _get_data_from_view(self, view_known_data:dict, keyword_scope:str="host", ex Args: view_known_data (dict): The view containing known data information. - + keyword_scope (str, optional): Scope of keywords like 'random' or 'all'. Defaults to "host" (i.e., only data from the specified host are considered). + exclude_types (list, optional): List of data types to exclude when selecting data. Defaults to ["log"]. Returns: dict: A dictionary mapping IP addresses to sets of known data. """ - # TODO Should we omit certain data types (e.g., logs)? known_data = {} for ip, data_list in view_known_data.items(): self.logger.debug(f'\tParsing data from {ip}: {data_list}') From d36086f45c548c5ec9bbaa2fab8f63d7e0f9cb12 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 21:41:46 +0200 Subject: [PATCH 28/31] add note --- AIDojoCoordinator/worlds/NSEGameCoordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/AIDojoCoordinator/worlds/NSEGameCoordinator.py b/AIDojoCoordinator/worlds/NSEGameCoordinator.py index 35efed34..76222313 100644 --- a/AIDojoCoordinator/worlds/NSEGameCoordinator.py +++ b/AIDojoCoordinator/worlds/NSEGameCoordinator.py @@ -113,6 +113,7 @@ def _get_services_from_view(self, view_known_services:dict)->dict: Returns: dict: A dictionary mapping IP addresses to sets of known services. """ + # TODO: Add keyword scope parameter (like in _get_data_from_view) known_services = {} for ip, service_list in view_known_services.items(): self.logger.debug(f'\tParsing services from {ip}: {service_list}') From 2ee09f2ac967e7d1256b524675e5ecab494157e5 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Wed, 1 Oct 2025 22:02:16 +0200 Subject: [PATCH 29/31] Use target goal state for goal check --- AIDojoCoordinator/coordinator.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AIDojoCoordinator/coordinator.py b/AIDojoCoordinator/coordinator.py index 5e892087..9f0a2a8e 100644 --- a/AIDojoCoordinator/coordinator.py +++ b/AIDojoCoordinator/coordinator.py @@ -854,17 +854,17 @@ def goal_dict_satistfied(goal_dict:dict, known_dict: dict)-> bool: return False return False self.logger.debug(f"Checking goal for agent {agent_addr}.") - goal_conditions = self._win_conditions_per_role[self.agents[agent_addr][1]] - self.logger.debug(f"\tGoal conditions for {agent_addr}: {goal_conditions}.") state = self._agent_states[agent_addr] # For each part of the state of the game, check if the conditions are met + target_goal_state = self._agent_goal_states[agent_addr] + self.logger.debug(f"\tGoal conditions: {target_goal_state}.") goal_reached = {} - goal_reached["networks"] = set(goal_conditions["known_networks"]) <= set(state.known_networks) - goal_reached["known_hosts"] = set(goal_conditions["known_hosts"]) <= set(state.known_hosts) - goal_reached["controlled_hosts"] = set(goal_conditions["controlled_hosts"]) <= set(state.controlled_hosts) - goal_reached["services"] = goal_dict_satistfied(goal_conditions["known_services"], state.known_services) - goal_reached["data"] = goal_dict_satistfied(goal_conditions["known_data"], state.known_data) - goal_reached["known_blocks"] = goal_dict_satistfied(goal_conditions["known_blocks"], state.known_blocks) + goal_reached["networks"] = target_goal_state.known_networks <= state.known_networks + goal_reached["known_hosts"] = target_goal_state.known_hosts <= state.known_hosts + goal_reached["controlled_hosts"] = target_goal_state.controlled_hosts <= state.controlled_hosts + goal_reached["services"] = goal_dict_satistfied(target_goal_state.known_services, state.known_services) + goal_reached["data"] = goal_dict_satistfied(target_goal_state.known_data, state.known_data) + goal_reached["known_blocks"] = goal_dict_satistfied(target_goal_state.known_blocks, state.known_blocks) self.logger.debug(f"\t{goal_reached}") return all(goal_reached.values()) From 93b164239be726e0dd57a0b5f7b5f8f46a736ee6 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Mon, 20 Oct 2025 10:24:19 +0200 Subject: [PATCH 30/31] add missing Moc value --- tests/coordinator/test_coordinator_core.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/coordinator/test_coordinator_core.py b/tests/coordinator/test_coordinator_core.py index 7d435bb5..0853693a 100644 --- a/tests/coordinator/test_coordinator_core.py +++ b/tests/coordinator/test_coordinator_core.py @@ -244,6 +244,7 @@ async def test_process_join_game_action_success(initialized_coordinator): # Minimal working state initialized_coordinator._starting_positions_per_role = {"Attacker": MagicMock()} initialized_coordinator._goal_description_per_role = {"Attacker": "Goal"} + initialized_coordinator._win_conditions_per_role = {"Attacker": MagicMock()} initialized_coordinator._steps_limit_per_role = {"Attacker": 10} initialized_coordinator._CONFIG_FILE_HASH = "abc123" initialized_coordinator._min_required_players = 1 @@ -251,7 +252,10 @@ async def test_process_join_game_action_success(initialized_coordinator): initialized_coordinator._episode_start_event.set() # Prevent wait action = MagicMock() - action.parameters = {"agent_info": MagicMock(name="AgentX", role="Attacker")} + agent_info = MagicMock() + agent_info.name = "AgentX" + agent_info.role = "Attacker" + action.parameters = {"agent_info": agent_info} observation = SimpleNamespace( state=SimpleNamespace(as_dict={}), # empty dict works here reward=0, @@ -259,7 +263,7 @@ async def test_process_join_game_action_success(initialized_coordinator): info={} ) - with patch.object(initialized_coordinator, "register_agent", new_callable=AsyncMock, return_value=MagicMock()), \ + with patch.object(initialized_coordinator, "register_agent", new_callable=AsyncMock, return_value=(MagicMock(),MagicMock())), \ patch.object(initialized_coordinator, "_initialize_new_player", return_value=observation), \ patch.object(initialized_coordinator.logger, "info"), \ patch.object(initialized_coordinator.logger, "debug"): From fdfcc653456c21e97568b78b5f91db2d24b45b73 Mon Sep 17 00:00:00 2001 From: Ondrej Lukas Date: Mon, 20 Oct 2025 10:27:07 +0200 Subject: [PATCH 31/31] remove unused imports --- AIDojoCoordinator/utils/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AIDojoCoordinator/utils/utils.py b/AIDojoCoordinator/utils/utils.py index 5f0d523c..c92b030c 100644 --- a/AIDojoCoordinator/utils/utils.py +++ b/AIDojoCoordinator/utils/utils.py @@ -12,8 +12,8 @@ from random import randint import json import hashlib -from cyst.api.configuration.network.node import NodeConfig, ActiveServiceConfig, InterfaceConfig -from typing import Optional, cast +from cyst.api.configuration.network.node import NodeConfig +from typing import Optional def get_file_hash(filepath, hash_func='sha256', chunk_size=4096): """