Skip to content

Commit 7ea5b6e

Browse files
committed
Restrict CybORG player protocol
1 parent d77984c commit 7ea5b6e

7 files changed

Lines changed: 341 additions & 134 deletions

File tree

codeclash/arenas/cyborg/cyborg.py

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,36 @@
88
from codeclash.utils.environment import assert_zero_exit_code
99

1010
RESULTS_JSON = "cyborg_results.json"
11+
CRASH_SCORE = -1_000_000.0
1112

1213

1314
class 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
2625
Each 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:

codeclash/arenas/cyborg/runtime/README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22

33
Edit `cyborg_agent.py`.
44

5-
Your file must define `MyAgent`, a CybORG `BaseAgent` subclass. A safe starting point is:
5+
Your file must define `decide(observation, action_space)`. The trusted runtime calls it with:
66

7-
```python
8-
from CybORG.Agents import RandomAgent
7+
- `observation`: a plain list converted from the CybORG observation array
8+
- `action_space`: a dictionary such as `{"type": "discrete", "n": 11}`
99

10+
Return an integer action in `[0, action_space["n"])`:
1011

11-
class MyAgent(RandomAgent):
12-
pass
12+
```python
13+
def decide(observation, action_space):
14+
return 0
1315
```
1416

15-
The arena runs simulated CAGE Challenge 3 DroneSwarm episodes and scores agents by average reward.
17+
The trusted CodeClash runtime owns the CybORG environment and validates returned actions before
18+
stepping the simulation. Submitted code only receives observations and returns action intents.
19+
20+
The arena runs simulated CAGE Challenge 3 DroneSwarm episodes and scores policies by average reward.
Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
from CybORG.Agents import RandomAgent
2-
3-
4-
class MyAgent(RandomAgent):
5-
"""Baseline CybORG blue-team agent.
6-
7-
Improve this class to choose better defensive actions in the simulated DroneSwarm scenario.
8-
"""
9-
10-
pass
1+
def decide(observation, action_space):
2+
"""Return a discrete CybORG action for the trusted runtime to validate."""
3+
return 0

0 commit comments

Comments
 (0)