88from codeclash .utils .environment import assert_zero_exit_code
99
1010RESULTS_JSON = "cyborg_results.json"
11+ CRASH_SCORE = - 1_000_000.0
1112
1213
1314class CybORGArena (CodeArena ):
1415 name : str = "CybORG"
1516 submission : str = "cyborg_agent.py"
1617 description : str = """CybORG is a simulated cyber-defense arena based on the CAGE Challenge 3 DroneSwarm scenario.
1718
18- Your bot is a Python file named `cyborg_agent.py` that defines a class named `MyAgent `.
19- `MyAgent` should inherit from a CybORG BaseAgent-compatible class, for example :
19+ Your bot is a Python file named `cyborg_agent.py` that defines a function named `decide `.
20+ The function receives a plain observation list and action-space dictionary, then returns an action :
2021
21- from CybORG.Agents import RandomAgent
22-
23- class MyAgent(RandomAgent):
24- ...
22+ def decide(observation, action_space):
23+ return 0
2524
2625Each round evaluates every submitted agent independently on the same seeded DroneSwarm episodes.
27- Your agent controls the blue-team drone agents through CybORG's simulated PettingZoo interface.
28- The objective is to maximize average episode reward. This arena uses CybORG simulation only and does
29- not run real exploit tools or interact with external networks.
30- """
26+ The trusted runtime owns the CybORG environment and action validation. Submitted code only receives
27+ plain observations and returns action intents. The objective is to maximize average episode reward.
28+ This arena uses CybORG simulation only and does not run real exploit tools or interact with external
29+ networks.
30+ """
3131 default_args : dict = {
3232 "steps_per_episode" : 30 ,
3333 "num_drones" : 18 ,
34+ "decision_timeout" : 3.0 ,
35+ "validation_timeout" : 10 ,
3436 "timeout" : 240 ,
3537 }
3638
3739 def _game_arg (self , key : str ):
38- return self . game_config .get ("args" , {}).get (key , self .default_args [key ])
40+ return getattr ( self , " game_config" , {}) .get ("args" , {}).get (key , self .default_args [key ])
3941
4042 def _episodes_per_round (self ) -> int :
4143 return int (self .game_config .get ("args" , {}).get ("episodes_per_round" , self .game_config ["sims_per_round" ]))
@@ -54,33 +56,25 @@ def validate_code(self, agent: Player) -> tuple[bool, str | None]:
5456 if syntax_check ["returncode" ] != 0 :
5557 return False , f"Python syntax error in `{ self .submission } `:\n { syntax_check ['output' ]} "
5658
57- import_check = agent .environment .execute (
58- "python - <<'PY'\n "
59- "import importlib.util\n "
60- f"spec = importlib.util.spec_from_file_location('submission_agent', { self .submission !r} )\n "
61- "module = importlib.util.module_from_spec(spec)\n "
62- "spec.loader.exec_module(module)\n "
63- "assert hasattr(module, 'MyAgent'), 'MyAgent class not found'\n "
64- "from CybORG.Agents import BaseAgent\n "
65- "assert issubclass(module.MyAgent, BaseAgent), 'MyAgent must inherit from a CybORG BaseAgent class'\n "
66- "def make_agent(agent_class, agent_name):\n "
67- " try:\n "
68- " return agent_class(name=agent_name)\n "
69- " except TypeError:\n "
70- " try:\n "
71- " return agent_class(agent_name)\n "
72- " except TypeError:\n "
73- " try:\n "
74- " return agent_class()\n "
75- " except TypeError as final_error:\n "
76- " raise TypeError(\n "
77- " 'MyAgent could not be constructed with the CybORG runtime constructor fallbacks'\n "
78- " ) from final_error\n "
79- "make_agent(module.MyAgent, 'validation-agent')\n "
80- "PY"
81- )
59+ validation_timeout = int (self ._game_arg ("validation_timeout" ))
60+ try :
61+ import_check = agent .environment .execute (
62+ "python - <<'PY'\n "
63+ "import importlib.util\n "
64+ f"spec = importlib.util.spec_from_file_location('submission_agent', { self .submission !r} )\n "
65+ "module = importlib.util.module_from_spec(spec)\n "
66+ "spec.loader.exec_module(module)\n "
67+ "assert hasattr(module, 'decide'), 'decide function not found'\n "
68+ "assert callable(module.decide), 'decide must be callable'\n "
69+ "result = module.decide([0, 1, 0], {'type': 'discrete', 'n': 11})\n "
70+ "assert result is None or isinstance(result, int), 'decide must return an integer action or None'\n "
71+ "PY" ,
72+ timeout = validation_timeout ,
73+ )
74+ except subprocess .TimeoutExpired :
75+ return False , f"`decide` validation exceeded { validation_timeout } s timeout"
8276 if import_check ["returncode" ] != 0 :
83- return False , f"Could not import `MyAgent ` from `{ self .submission } `:\n { import_check ['output' ]} "
77+ return False , f"Could not import or call `decide ` from `{ self .submission } `:\n { import_check ['output' ]} "
8478
8579 return True , None
8680
@@ -98,6 +92,8 @@ def execute_round(self, agents: list[Player]) -> None:
9892 str (self ._game_arg ("steps_per_episode" )),
9993 "--drones" ,
10094 str (self ._game_arg ("num_drones" )),
95+ "--decision-timeout" ,
96+ str (self ._game_arg ("decision_timeout" )),
10197 "--output" ,
10298 str (self .log_env / RESULTS_JSON ),
10399 * agent_args ,
@@ -116,8 +112,19 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
116112 self .logger .error (f"Missing result file: { result_file } " )
117113 stats .winner = RESULT_TIE
118114 for agent in agents :
119- stats .scores [agent .name ] = 0.0
120- stats .player_stats [agent .name ].score = 0.0
115+ stats .scores [agent .name ] = CRASH_SCORE
116+ stats .player_stats [agent .name ].score = CRASH_SCORE
117+ stats .details .append (
118+ json .dumps (
119+ {
120+ "player" : agent .name ,
121+ "score" : CRASH_SCORE ,
122+ "status" : "error" ,
123+ "error" : f"missing CybORG result file: { result_file } " ,
124+ },
125+ sort_keys = True ,
126+ )
127+ )
121128 return
122129
123130 with open (result_file ) as f :
0 commit comments