Ajayyy00 commited on
Commit
292f6a5
Β·
1 Parent(s): 7211e63

Add alternating self-play training scaffolding.

Browse files

Wire configurable frozen-opponent server startup and add training utilities for archive, eval harness, data collection, and freeze-alternate orchestration.

Made-with: Cursor

server/app.py CHANGED
@@ -22,6 +22,9 @@ Usage:
22
  uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
23
  """
24
 
 
 
 
25
  try:
26
  from openenv.core.env_server.http_server import create_app
27
  except Exception as e: # pragma: no cover
@@ -37,9 +40,38 @@ except (ImportError, ModuleNotFoundError):
37
  from server.play_environment import CyberSOCEnvironment
38
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  # Create the app with the CyberSOCEnv environment
41
  app = create_app(
42
- CyberSOCEnvironment,
43
  SOCActionWrapper,
44
  SOCObservation,
45
  env_name="cybersocenv",
 
22
  uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
23
  """
24
 
25
+ import os
26
+ import random
27
+
28
  try:
29
  from openenv.core.env_server.http_server import create_app
30
  except Exception as e: # pragma: no cover
 
40
  from server.play_environment import CyberSOCEnvironment
41
 
42
 
43
+ class FrozenCheckpointRedPolicy:
44
+ """Lightweight frozen red policy shim keyed by checkpoint identity."""
45
+
46
+ def __init__(self, checkpoint: str):
47
+ self.checkpoint = checkpoint
48
+ self._rng = random.Random(hash(checkpoint))
49
+
50
+ def act(self, red_observation):
51
+ blue_action = red_observation.get("blue_action_type", "")
52
+ # Deterministic-ish heuristic controlled by checkpoint hash and episode step.
53
+ trigger = blue_action in {"kill_process", "isolate_segment"}
54
+ if trigger and self._rng.random() < 0.5:
55
+ return {
56
+ "action_type": "lateral_pivot",
57
+ "source_host": red_observation.get("blue_action_target", ""),
58
+ }
59
+ return {"action_type": "noop"}
60
+
61
+
62
+ _frozen_checkpoint = os.environ.get("CYBERSOC_FROZEN_RED_CHECKPOINT", "").strip()
63
+ _adaptive = os.environ.get("CYBERSOC_ADAPTIVE", "1").strip() not in {"0", "false", "False"}
64
+ _red_policy = FrozenCheckpointRedPolicy(_frozen_checkpoint) if _frozen_checkpoint else None
65
+
66
+
67
+ class ConfiguredCyberSOCEnvironment(CyberSOCEnvironment):
68
+ def __init__(self):
69
+ super().__init__(adaptive=_adaptive, neural_red_policy=_red_policy)
70
+
71
+
72
  # Create the app with the CyberSOCEnv environment
73
  app = create_app(
74
+ ConfiguredCyberSOCEnvironment,
75
  SOCActionWrapper,
76
  SOCObservation,
77
  env_name="cybersocenv",
training/agent_archive.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Historical archive utilities for FSP/PFSP training."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import random
7
+ from dataclasses import dataclass, asdict
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional
10
+
11
+
12
+ @dataclass
13
+ class ArchivedAgent:
14
+ role: str
15
+ version: str
16
+ checkpoint_path: str
17
+ iteration: int
18
+ metadata: Dict[str, float]
19
+
20
+
21
+ class AgentArchive:
22
+ """Stores historical checkpoints and win-rate metadata in a JSON index."""
23
+
24
+ def __init__(self, index_path: str = "training/archive/index.json"):
25
+ self.index_path = Path(index_path)
26
+ self.index_path.parent.mkdir(parents=True, exist_ok=True)
27
+ self._items: List[ArchivedAgent] = self._load()
28
+
29
+ def _load(self) -> List[ArchivedAgent]:
30
+ if not self.index_path.exists():
31
+ return []
32
+ raw = json.loads(self.index_path.read_text(encoding="utf-8"))
33
+ return [ArchivedAgent(**item) for item in raw]
34
+
35
+ def save(self) -> None:
36
+ payload = [asdict(item) for item in self._items]
37
+ self.index_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
38
+
39
+ def add(
40
+ self,
41
+ role: str,
42
+ version: str,
43
+ checkpoint_path: str,
44
+ iteration: int,
45
+ metadata: Optional[Dict[str, float]] = None,
46
+ ) -> None:
47
+ self._items.append(
48
+ ArchivedAgent(
49
+ role=role,
50
+ version=version,
51
+ checkpoint_path=checkpoint_path,
52
+ iteration=iteration,
53
+ metadata=metadata or {},
54
+ )
55
+ )
56
+ self.save()
57
+
58
+ def list_role(self, role: str) -> List[ArchivedAgent]:
59
+ return [item for item in self._items if item.role == role]
60
+
61
+ def latest(self, role: str) -> Optional[ArchivedAgent]:
62
+ role_items = self.list_role(role)
63
+ if not role_items:
64
+ return None
65
+ return max(role_items, key=lambda x: x.iteration)
66
+
67
+ def sample_fsp(self, role: str) -> Optional[ArchivedAgent]:
68
+ items = self.list_role(role)
69
+ if not items:
70
+ return None
71
+ return random.choice(items)
72
+
73
+ def sample_pfsp(self, role: str, temperature: float = 1.0) -> Optional[ArchivedAgent]:
74
+ """Prioritize opponents where blue has lower win-rate."""
75
+ items = self.list_role(role)
76
+ if not items:
77
+ return None
78
+
79
+ weights: List[float] = []
80
+ for item in items:
81
+ blue_win_rate = float(item.metadata.get("blue_win_rate", 0.5))
82
+ difficulty = max(0.0, min(1.0, 1.0 - blue_win_rate))
83
+ weights.append(max(1e-6, difficulty**temperature))
84
+
85
+ return random.choices(items, weights=weights, k=1)[0]
86
+
87
+ def must_beat_all(self, threshold: float = 0.55) -> bool:
88
+ """Return True when every archived red has blue_win_rate >= threshold."""
89
+ red_items = self.list_role("red")
90
+ if not red_items:
91
+ return True
92
+ return all(float(item.metadata.get("blue_win_rate", 0.0)) >= threshold for item in red_items)
training/collect_sft.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Collect imitation data from deterministic red-team decisions.
3
+
4
+ This script runs generated scenarios and stores red decision tuples as JSONL:
5
+ {"observation": {...}, "action": {...}}
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List
14
+
15
+ from models import SOCActionWrapper
16
+ from server.play_environment import CyberSOCEnvironment
17
+ from server.tasks import get_task
18
+
19
+
20
+ def _scripted_blue_actions(task_id: str) -> List[Dict[str, Any]]:
21
+ """Simple deterministic blue rollout to trigger red dynamics each step."""
22
+ task_def = get_task(task_id)
23
+ reqs = task_def.get("containment_requirements", {}) or {}
24
+ actions: List[Dict[str, Any]] = []
25
+
26
+ for host in reqs.get("must_forensics", []):
27
+ actions.append({"type": "run_forensics", "hostname": host})
28
+
29
+ for proc in reqs.get("must_kill", []):
30
+ actions.append(
31
+ {
32
+ "type": "kill_process",
33
+ "hostname": proc["hostname"],
34
+ "process_name": proc["process"],
35
+ }
36
+ )
37
+
38
+ for ioc in reqs.get("must_block_iocs", []):
39
+ ioc_type = "hash" if len(ioc) >= 32 and "." not in ioc else ("ip" if ioc.count(".") == 3 else "domain")
40
+ actions.append({"type": "block_ioc", "ioc_type": ioc_type, "ioc_value": ioc})
41
+
42
+ actions.append(
43
+ {
44
+ "type": "submit_containment_plan",
45
+ "plan": [
46
+ {
47
+ "threat_id": t.get("threat_id", "UNKNOWN"),
48
+ "actions_taken": ["run_forensics", "kill_process", "block_ioc"],
49
+ "root_cause": t.get("threat_type", "unknown"),
50
+ "confidence": 0.8,
51
+ }
52
+ for t in task_def.get("attack_chain", [])
53
+ ],
54
+ "executive_summary": "Automated containment sequence completed.",
55
+ }
56
+ )
57
+ return actions
58
+
59
+
60
+ def collect_red_imitation_dataset(
61
+ output_path: Path,
62
+ num_tasks: int = 1000,
63
+ task_prefix: str = "gen_",
64
+ ) -> int:
65
+ output_path.parent.mkdir(parents=True, exist_ok=True)
66
+ records: List[Dict[str, Any]] = []
67
+
68
+ def _logger(record: Dict[str, Any]) -> None:
69
+ records.append(record)
70
+
71
+ env = CyberSOCEnvironment(adaptive=True, red_team_logger=_logger)
72
+
73
+ for idx in range(1, num_tasks + 1):
74
+ task_id = f"{task_prefix}{idx:04d}"
75
+ env.reset(task_id=task_id)
76
+ for action in _scripted_blue_actions(task_id):
77
+ obs = env.step(SOCActionWrapper(**action))
78
+ if obs.done:
79
+ break
80
+
81
+ with output_path.open("w", encoding="utf-8") as f:
82
+ for record in records:
83
+ f.write(json.dumps(record) + "\n")
84
+
85
+ return len(records)
86
+
87
+
88
+ def main() -> None:
89
+ parser = argparse.ArgumentParser(description="Collect deterministic red-team SFT data")
90
+ parser.add_argument(
91
+ "--output",
92
+ default="training/data/red_imitation.jsonl",
93
+ help="Output JSONL path",
94
+ )
95
+ parser.add_argument("--num-tasks", type=int, default=1000, help="Number of generated scenarios")
96
+ parser.add_argument("--task-prefix", default="gen_", help="Task ID prefix")
97
+ args = parser.parse_args()
98
+
99
+ total = collect_red_imitation_dataset(
100
+ output_path=Path(args.output),
101
+ num_tasks=args.num_tasks,
102
+ task_prefix=args.task_prefix,
103
+ )
104
+ print(f"Saved {total} red decision examples to {args.output}")
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
training/eval_harness.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluation harness for Blue-vs-Red checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable, Dict, List
7
+
8
+ from models import SOCActionWrapper
9
+ from server.play_environment import CyberSOCEnvironment
10
+
11
+
12
+ PolicyFn = Callable[[Dict[str, Any]], Dict[str, Any]]
13
+
14
+
15
+ @dataclass
16
+ class EvalResult:
17
+ episodes: int
18
+ blue_win_rate: float
19
+ avg_blue_score: float
20
+ per_episode_scores: List[float]
21
+
22
+
23
+ def run_head_to_head_eval(
24
+ blue_policy: PolicyFn,
25
+ red_policy: Any,
26
+ task_ids: List[str],
27
+ episodes_per_task: int = 50,
28
+ ) -> EvalResult:
29
+ """Evaluate blue against a frozen red policy across task IDs."""
30
+ scores: List[float] = []
31
+ wins = 0
32
+ total = 0
33
+
34
+ for task_id in task_ids:
35
+ for _ in range(episodes_per_task):
36
+ env = CyberSOCEnvironment(adaptive=True, neural_red_policy=red_policy)
37
+ obs = env.reset(task_id=task_id)
38
+
39
+ while not obs.done:
40
+ action_dict = blue_policy(obs.model_dump())
41
+ obs = env.step(SOCActionWrapper(**action_dict))
42
+
43
+ score = float(obs.final_score or 0.0)
44
+ scores.append(score)
45
+ wins += int(score >= 0.5)
46
+ total += 1
47
+
48
+ avg_score = sum(scores) / max(1, len(scores))
49
+ return EvalResult(
50
+ episodes=total,
51
+ blue_win_rate=wins / max(1, total),
52
+ avg_blue_score=avg_score,
53
+ per_episode_scores=scores,
54
+ )
55
+
56
+
57
+ def must_beat_all_archive(win_rates: Dict[str, float], threshold: float = 0.55) -> bool:
58
+ return all(rate >= threshold for rate in win_rates.values())
training/freeze_alternate.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Freeze-alternate orchestration for Blue/Red GRPO training."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Dict, Optional
10
+
11
+ try:
12
+ from .agent_archive import AgentArchive
13
+ from .pfsp_scheduler import temperature_for_iteration
14
+ except ImportError:
15
+ from agent_archive import AgentArchive
16
+ from pfsp_scheduler import temperature_for_iteration
17
+
18
+
19
+ def _run_train_command(command: str) -> None:
20
+ completed = subprocess.run(command, shell=True, check=False)
21
+ if completed.returncode != 0:
22
+ raise RuntimeError(f"Training command failed: {command}")
23
+
24
+
25
+ def _format_cmd(
26
+ base_cmd: str,
27
+ role: str,
28
+ output_dir: str,
29
+ frozen_opponent: Optional[str],
30
+ episodes: int,
31
+ ) -> str:
32
+ cmd = [
33
+ base_cmd,
34
+ f"--train-role {role}",
35
+ f"--output-dir {output_dir}",
36
+ f"--episodes {episodes}",
37
+ ]
38
+ if frozen_opponent:
39
+ cmd.append(f"--frozen-opponent {frozen_opponent}")
40
+ return " ".join(cmd)
41
+
42
+
43
+ def run_freeze_alternate(
44
+ iterations: int,
45
+ train_blue_episodes: int,
46
+ train_red_episodes: int,
47
+ base_train_cmd: str,
48
+ archive_path: str,
49
+ ) -> Dict[str, str]:
50
+ archive = AgentArchive(archive_path)
51
+ latest_blue = archive.latest("blue")
52
+ latest_red = archive.latest("red")
53
+
54
+ for it in range(1, iterations + 1):
55
+ blue_version = f"blue_v{it}"
56
+ blue_ckpt = f"checkpoints/{blue_version}"
57
+ cmd_blue = _format_cmd(
58
+ base_cmd=base_train_cmd,
59
+ role="blue",
60
+ output_dir=blue_ckpt,
61
+ frozen_opponent=latest_red.checkpoint_path if latest_red else None,
62
+ episodes=train_blue_episodes,
63
+ )
64
+ _run_train_command(cmd_blue)
65
+ archive.add("blue", blue_version, blue_ckpt, iteration=it, metadata={})
66
+ latest_blue = archive.latest("blue")
67
+
68
+ red_version = f"red_v{it}"
69
+ red_ckpt = f"checkpoints/{red_version}"
70
+ cmd_red = _format_cmd(
71
+ base_cmd=base_train_cmd,
72
+ role="red",
73
+ output_dir=red_ckpt,
74
+ frozen_opponent=latest_blue.checkpoint_path if latest_blue else None,
75
+ episodes=train_red_episodes,
76
+ )
77
+ _run_train_command(cmd_red)
78
+ archive.add("red", red_version, red_ckpt, iteration=it, metadata={})
79
+ latest_red = archive.latest("red")
80
+
81
+ return {
82
+ "latest_blue": latest_blue.checkpoint_path if latest_blue else "",
83
+ "latest_red": latest_red.checkpoint_path if latest_red else "",
84
+ }
85
+
86
+
87
+ def main() -> None:
88
+ parser = argparse.ArgumentParser(description="Freeze-alternate Blue/Red training orchestrator")
89
+ parser.add_argument("--iterations", type=int, default=2)
90
+ parser.add_argument("--blue-episodes", type=int, default=500)
91
+ parser.add_argument("--red-episodes", type=int, default=300)
92
+ parser.add_argument("--train-cmd", default="python -m training.train_grpo")
93
+ parser.add_argument("--archive-path", default="training/archive/index.json")
94
+ parser.add_argument("--show-temp-for", type=int, default=0, help="Print PFSP temp schedule for N iterations")
95
+ args = parser.parse_args()
96
+
97
+ if args.show_temp_for > 0:
98
+ schedule = {
99
+ f"iter_{i + 1}": temperature_for_iteration(i, args.show_temp_for)
100
+ for i in range(args.show_temp_for)
101
+ }
102
+ print(json.dumps(schedule, indent=2))
103
+ return
104
+
105
+ outputs = run_freeze_alternate(
106
+ iterations=args.iterations,
107
+ train_blue_episodes=args.blue_episodes,
108
+ train_red_episodes=args.red_episodes,
109
+ base_train_cmd=args.train_cmd,
110
+ archive_path=args.archive_path,
111
+ )
112
+ print(json.dumps(outputs, indent=2))
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
training/pfsp_scheduler.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PFSP temperature scheduling and weighted opponent sampling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, Iterable, Tuple
6
+
7
+
8
+ def temperature_for_iteration(
9
+ iteration: int,
10
+ total_iterations: int,
11
+ start_temp: float = 0.5,
12
+ end_temp: float = 2.0,
13
+ ) -> float:
14
+ if total_iterations <= 1:
15
+ return end_temp
16
+ ratio = max(0.0, min(1.0, iteration / float(total_iterations - 1)))
17
+ return start_temp + (end_temp - start_temp) * ratio
18
+
19
+
20
+ def pfsp_weights(
21
+ win_rates: Dict[str, float],
22
+ temperature: float,
23
+ ) -> Dict[str, float]:
24
+ """Compute PFSP weights = (1 - blue_win_rate) ** temperature."""
25
+ weights: Dict[str, float] = {}
26
+ for name, win_rate in win_rates.items():
27
+ clipped = max(0.0, min(1.0, float(win_rate)))
28
+ weights[name] = max(1e-6, (1.0 - clipped) ** temperature)
29
+ return weights
30
+
31
+
32
+ def normalize_weights(raw_weights: Dict[str, float]) -> Dict[str, float]:
33
+ total = sum(raw_weights.values())
34
+ if total <= 0:
35
+ n = max(1, len(raw_weights))
36
+ return {k: 1.0 / n for k in raw_weights}
37
+ return {k: v / total for k, v in raw_weights.items()}
38
+
39
+
40
+ def rank_hard_opponents(win_rates: Dict[str, float]) -> Iterable[Tuple[str, float]]:
41
+ """Yield opponents ordered from hardest (lowest win-rate) to easiest."""
42
+ return sorted(win_rates.items(), key=lambda kv: kv[1])
training/train_grpo.py CHANGED
@@ -49,19 +49,29 @@ except ImportError:
49
  class EnvServer:
50
  """Starts and monitors the CyberSOCEnv FastAPI server as a subprocess."""
51
 
52
- def __init__(self, cfg: TrainingConfig):
53
  self.cfg = cfg
 
54
  self._proc: Optional[subprocess.Popen] = None
55
 
56
  def start(self) -> None:
57
  cmd = [
58
  sys.executable, "-m", "uvicorn",
59
- "server:app",
60
  "--host", self.cfg.env_host,
61
  "--port", str(self.cfg.env_port),
62
  "--log-level", "warning",
63
  ]
64
- self._proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
 
 
 
 
 
 
 
 
 
65
  print(f"[server] PID {self._proc.pid} starting on :{self.cfg.env_port} ...")
66
  self._wait_healthy()
67
 
@@ -313,6 +323,20 @@ def make_all_reward_fns(env_url: str) -> List:
313
  return fns
314
 
315
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  # ═══════════════════════════════════════════════════════════════════════════
317
  # 5. Main
318
  # ═══════════════════════════════════════════════════════════════════════════
@@ -321,8 +345,12 @@ def parse_args() -> argparse.Namespace:
321
  p = argparse.ArgumentParser(description="CyberSOC GRPO Training")
322
  p.add_argument("--model", default="", help="HuggingFace model ID (overrides config)")
323
  p.add_argument("--steps", type=int, default=0, help="Max training steps (overrides config)")
 
324
  p.add_argument("--tasks", default="", help="Comma-separated task IDs, e.g. easy,medium,hard")
325
  p.add_argument("--hub-id", default="", dest="hub_id", help="HF Hub model ID for checkpoint push")
 
 
 
326
  p.add_argument("--no-server", action="store_true", help="Skip starting env server (already running)")
327
  return p.parse_args()
328
 
@@ -333,8 +361,10 @@ def main() -> None:
333
 
334
  if args.model: cfg.model_name = args.model
335
  if args.steps: cfg.max_steps = args.steps
 
336
  if args.tasks: cfg.task_ids = args.tasks.split(",")
337
  if args.hub_id: cfg.hub_model_id = args.hub_id
 
338
 
339
  hf_token = os.environ.get("HF_TOKEN", "")
340
 
@@ -343,11 +373,14 @@ def main() -> None:
343
  print(f" Model : {cfg.model_name}")
344
  print(f" Tasks : {cfg.task_ids}")
345
  print(f" Steps : {cfg.max_steps}")
 
 
 
346
  print(f" Hub : {cfg.hub_model_id or '(local only)'}")
347
  print("=" * 60)
348
 
349
  # 1. Start env server ────────────────────────────────────────────────────
350
- server = EnvServer(cfg)
351
  if not args.no_server:
352
  server.start()
353
 
@@ -382,6 +415,9 @@ def main() -> None:
382
  # 4. Build reward functions ───────────────────────────────────────────
383
  print("\n[rewards] wiring 11 reward functions (10 dims + weighted total) ...")
384
  reward_fns = make_all_reward_fns(cfg.env_url)
 
 
 
385
  print(f"[rewards] βœ“ {len(reward_fns)} functions registered")
386
 
387
  # 5. GRPO config ──────────────────────────────────────────────────────
 
49
  class EnvServer:
50
  """Starts and monitors the CyberSOCEnv FastAPI server as a subprocess."""
51
 
52
+ def __init__(self, cfg: TrainingConfig, frozen_opponent: str = ""):
53
  self.cfg = cfg
54
+ self.frozen_opponent = frozen_opponent
55
  self._proc: Optional[subprocess.Popen] = None
56
 
57
  def start(self) -> None:
58
  cmd = [
59
  sys.executable, "-m", "uvicorn",
60
+ "server.app:app",
61
  "--host", self.cfg.env_host,
62
  "--port", str(self.cfg.env_port),
63
  "--log-level", "warning",
64
  ]
65
+ env = os.environ.copy()
66
+ if self.frozen_opponent:
67
+ env["CYBERSOC_FROZEN_RED_CHECKPOINT"] = self.frozen_opponent
68
+ env.setdefault("CYBERSOC_ADAPTIVE", "1")
69
+ self._proc = subprocess.Popen(
70
+ cmd,
71
+ stdout=subprocess.DEVNULL,
72
+ stderr=subprocess.PIPE,
73
+ env=env,
74
+ )
75
  print(f"[server] PID {self._proc.pid} starting on :{self.cfg.env_port} ...")
76
  self._wait_healthy()
77
 
 
323
  return fns
324
 
325
 
326
+ def invert_reward_fns(reward_fns: List) -> List:
327
+ """Invert blue-centric rewards for red training."""
328
+ inverted = []
329
+ for fn in reward_fns:
330
+ def _wrap(base_fn):
331
+ def _inv(completions: List[str], **kwargs) -> List[float]:
332
+ values = base_fn(completions, **kwargs)
333
+ return [-float(v) for v in values]
334
+ _inv.__name__ = f"inv_{getattr(base_fn, '__name__', 'reward')}"
335
+ return _inv
336
+ inverted.append(_wrap(fn))
337
+ return inverted
338
+
339
+
340
  # ═══════════════════════════════════════════════════════════════════════════
341
  # 5. Main
342
  # ═══════════════════════════════════════════════════════════════════════════
 
345
  p = argparse.ArgumentParser(description="CyberSOC GRPO Training")
346
  p.add_argument("--model", default="", help="HuggingFace model ID (overrides config)")
347
  p.add_argument("--steps", type=int, default=0, help="Max training steps (overrides config)")
348
+ p.add_argument("--episodes", type=int, default=0, help="Alias for steps in freeze-alternate mode")
349
  p.add_argument("--tasks", default="", help="Comma-separated task IDs, e.g. easy,medium,hard")
350
  p.add_argument("--hub-id", default="", dest="hub_id", help="HF Hub model ID for checkpoint push")
351
+ p.add_argument("--output-dir", default="", help="Override output directory")
352
+ p.add_argument("--train-role", choices=["blue", "red"], default="blue", help="Policy role being optimized")
353
+ p.add_argument("--frozen-opponent", default="", help="Path/ID for frozen opponent checkpoint")
354
  p.add_argument("--no-server", action="store_true", help="Skip starting env server (already running)")
355
  return p.parse_args()
356
 
 
361
 
362
  if args.model: cfg.model_name = args.model
363
  if args.steps: cfg.max_steps = args.steps
364
+ if args.episodes: cfg.max_steps = args.episodes
365
  if args.tasks: cfg.task_ids = args.tasks.split(",")
366
  if args.hub_id: cfg.hub_model_id = args.hub_id
367
+ if args.output_dir: cfg.output_dir = args.output_dir
368
 
369
  hf_token = os.environ.get("HF_TOKEN", "")
370
 
 
373
  print(f" Model : {cfg.model_name}")
374
  print(f" Tasks : {cfg.task_ids}")
375
  print(f" Steps : {cfg.max_steps}")
376
+ print(f" Role : {args.train_role}")
377
+ if args.frozen_opponent:
378
+ print(f" Frozen : {args.frozen_opponent}")
379
  print(f" Hub : {cfg.hub_model_id or '(local only)'}")
380
  print("=" * 60)
381
 
382
  # 1. Start env server ────────────────────────────────────────────────────
383
+ server = EnvServer(cfg, frozen_opponent=args.frozen_opponent)
384
  if not args.no_server:
385
  server.start()
386
 
 
415
  # 4. Build reward functions ───────────────────────────────────────────
416
  print("\n[rewards] wiring 11 reward functions (10 dims + weighted total) ...")
417
  reward_fns = make_all_reward_fns(cfg.env_url)
418
+ if args.train_role == "red":
419
+ reward_fns = invert_reward_fns(reward_fns)
420
+ print("[rewards] role=red -> inverted blue-centric rewards")
421
  print(f"[rewards] βœ“ {len(reward_fns)} functions registered")
422
 
423
  # 5. GRPO config ──────────────────────────────────────────────────────