A-HK commited on
Commit
cd43a41
·
verified ·
1 Parent(s): 60f4610

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +35 -0
  2. inference.py +29 -25
  3. models.py +9 -1
  4. server/agentic_security_lab_environment.py +126 -6
  5. server/app.py +20 -3
README.md CHANGED
@@ -95,6 +95,12 @@ Each step returns:
95
  - `active_malicious_packages`: malicious packages not yet quarantined
96
  - `error`: optional action error reason
97
 
 
 
 
 
 
 
98
  ## Reward Design
99
 
100
  Dense reward shaping provides trajectory-level signal, not just terminal binary success.
@@ -119,6 +125,35 @@ Episode-end bonuses (on `conclude`/terminal):
119
  - contained before exfiltration: `+0.10`
120
  - attacker succeeds: `-0.20`
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  ## Grader Definition (Deterministic)
123
 
124
  Task-level grading is deterministic and normalized to `[0, 1]` using explicit component completion ratios from final `state`.
 
95
  - `active_malicious_packages`: malicious packages not yet quarantined
96
  - `error`: optional action error reason
97
 
98
+ The `data` field includes evaluator-facing telemetry:
99
+ - `reward_type`: always `training_step_reward`
100
+ - `benchmark_score`: deterministic task score computed from current state
101
+ - `score_breakdown`: component ratios (`quarantine`, `rotate`, `notify`, `contain`)
102
+ - `evaluator_metrics`: audit-friendly counts (invalid actions, fallback use, false positives, deadline status)
103
+
104
  ## Reward Design
105
 
106
  Dense reward shaping provides trajectory-level signal, not just terminal binary success.
 
125
  - contained before exfiltration: `+0.10`
126
  - attacker succeeds: `-0.20`
127
 
128
+ ## Mode and Fallback Architecture
129
+
130
+ The environment supports explicit execution modes and safe fallbacks:
131
+
132
+ - `mode=benchmark` (default): deterministic evaluation behavior for leaderboard runs.
133
+ - `mode=training`: same state transitions with evaluator telemetry preserved for training analysis.
134
+ - Unsupported mode values safely fallback to `benchmark`, and `mode_fallback_used=true` is exposed in `state` and `data.evaluator_metrics`.
135
+
136
+ Command fallback can be enabled for robustness:
137
+ - aliases like `inspect`, `deps`, `dependents`, `rotate` map to canonical commands.
138
+ - fallback usage is counted in `command_fallback_used_count`.
139
+
140
+ You can set defaults via environment variables:
141
+ - `AGENTIC_SECURITY_LAB_MODE=benchmark|training`
142
+ - `AGENTIC_SECURITY_LAB_COMMAND_FALLBACK=true|false`
143
+
144
+ Or override at runtime via `POST /reset` body:
145
+ - `mode`
146
+ - `command_fallback_enabled`
147
+
148
+ ## Training Reward vs Benchmark Score
149
+
150
+ The environment now separates learning signal from benchmark evaluation:
151
+
152
+ - `reward` (per-step): **training reward** used for RL trajectories.
153
+ - `benchmark_score` (in `observation.data`): deterministic score for evaluator reporting.
154
+
155
+ This makes it clear that reward shaping drives behavior during episodes, while benchmark score reflects objective completion quality.
156
+
157
  ## Grader Definition (Deterministic)
158
 
159
  Task-level grading is deterministic and normalized to `[0, 1]` using explicit component completion ratios from final `state`.
inference.py CHANGED
@@ -20,8 +20,10 @@ from typing import Optional
20
 
21
  import httpx
22
  from openai import OpenAI
23
- from graders import grade_easy, grade_hard, grade_medium
24
 
 
 
 
25
  API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
26
  MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct")
27
  API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or ""
@@ -30,7 +32,6 @@ ENV_BASE_URL = os.getenv("ENV_BASE_URL", "http://localhost:8000")
30
  ALL_TASKS = ["easy", "medium", "hard"]
31
  MAX_STEPS = 30
32
  BENCHMARK = "agentic-security-lab"
33
- SCORE_EPS = 1e-6
34
 
35
  SYSTEM_PROMPT = textwrap.dedent("""
36
  You are an expert security engineer responding to a supply-chain incident.
@@ -58,6 +59,8 @@ SYSTEM_PROMPT = textwrap.dedent("""
58
  """).strip()
59
 
60
 
 
 
61
  def log_start(task: str, model: str) -> None:
62
  print(f"[START] task={task} env={BENCHMARK} model={model}", flush=True)
63
 
@@ -77,11 +80,13 @@ def log_end(success: bool, steps: int, score: float,
77
  rewards_str = ",".join(f"{r:.2f}" for r in rewards)
78
  print(
79
  f"[END] success={str(success).lower()} steps={steps} "
80
- f"score={score:.6f} rewards={rewards_str}",
81
  flush=True,
82
  )
83
 
84
 
 
 
85
  def env_reset(http: httpx.Client, task_name: str) -> dict:
86
  r = http.post("/reset", json={"task_name": task_name})
87
  r.raise_for_status()
@@ -94,11 +99,7 @@ def env_step(http: httpx.Client, command: str, parameters: dict) -> dict:
94
  return r.json()
95
 
96
 
97
- def env_state(http: httpx.Client) -> dict:
98
- r = http.get("/state")
99
- r.raise_for_status()
100
- return r.json()
101
-
102
 
103
  def parse_action(raw: str) -> tuple:
104
  text = raw.strip()
@@ -112,6 +113,8 @@ def parse_action(raw: str) -> tuple:
112
  return "conclude", {}
113
 
114
 
 
 
115
  def run_task(llm: OpenAI, http: httpx.Client, task_name: str) -> float:
116
  rewards = []
117
  step_n = 0
@@ -130,6 +133,7 @@ def run_task(llm: OpenAI, http: httpx.Client, task_name: str) -> float:
130
 
131
  for step_n in range(1, MAX_STEPS + 1):
132
 
 
133
  try:
134
  resp = llm.chat.completions.create(
135
  model = MODEL_NAME,
@@ -139,12 +143,14 @@ def run_task(llm: OpenAI, http: httpx.Client, task_name: str) -> float:
139
  )
140
  raw = resp.choices[0].message.content or ""
141
  except Exception as llm_err:
 
142
  raw = '{"command": "conclude", "parameters": {}}'
143
- _ = llm_err
144
 
145
  command, params = parse_action(raw)
146
  action_str = json.dumps({"command": command, "parameters": params})
147
 
 
148
  obs = env_step(http, command, params)
149
  reward = float(obs.get("reward", 0.0))
150
  done = bool(obs.get("done", False))
@@ -160,27 +166,20 @@ def run_task(llm: OpenAI, http: httpx.Client, task_name: str) -> float:
160
  if done:
161
  break
162
 
163
- final_state = env_state(http)
164
- graders = {
165
- "easy": grade_easy,
166
- "medium": grade_medium,
167
- "hard": grade_hard,
168
- }
169
- score = graders[task_name](final_state)
170
- score = max(SCORE_EPS, min(1.0 - SCORE_EPS, score))
171
- success = (
172
- score >= 1.0 - SCORE_EPS
173
- and not bool(final_state.get("attacker_succeeded", False))
174
- )
175
-
176
- except Exception:
177
  log_end(success=False, steps=step_n, score=0.0, rewards=rewards)
178
- return 0.0
179
 
180
  log_end(success=success, steps=step_n, score=score, rewards=rewards)
181
  return score
182
 
183
 
 
 
184
  def main() -> None:
185
  llm = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
186
  http = httpx.Client(base_url=ENV_BASE_URL, timeout=60)
@@ -188,12 +187,17 @@ def main() -> None:
188
  task_env = os.getenv("TASK_NAME", "")
189
  tasks = [task_env] if task_env in ALL_TASKS else ALL_TASKS
190
 
 
191
  try:
192
  for task in tasks:
193
- run_task(llm, http, task)
194
  finally:
195
  http.close()
196
 
 
 
 
 
197
 
198
  if __name__ == "__main__":
199
  main()
 
20
 
21
  import httpx
22
  from openai import OpenAI
 
23
 
24
+ # ── Configuration ──────────────────────────────────────────────────────────────
25
+ # Defaults match what the hackathon validator injects.
26
+ # HF_TOKEN is set automatically by the validator — do not hardcode it.
27
  API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
28
  MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct")
29
  API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or ""
 
32
  ALL_TASKS = ["easy", "medium", "hard"]
33
  MAX_STEPS = 30
34
  BENCHMARK = "agentic-security-lab"
 
35
 
36
  SYSTEM_PROMPT = textwrap.dedent("""
37
  You are an expert security engineer responding to a supply-chain incident.
 
59
  """).strip()
60
 
61
 
62
+ # ── Logging helpers ────────────────────────────────────────────────────────────
63
+
64
  def log_start(task: str, model: str) -> None:
65
  print(f"[START] task={task} env={BENCHMARK} model={model}", flush=True)
66
 
 
80
  rewards_str = ",".join(f"{r:.2f}" for r in rewards)
81
  print(
82
  f"[END] success={str(success).lower()} steps={steps} "
83
+ f"score={score:.2f} rewards={rewards_str}",
84
  flush=True,
85
  )
86
 
87
 
88
+ # ── Environment helpers ────────────────────────────────────────────────────────
89
+
90
  def env_reset(http: httpx.Client, task_name: str) -> dict:
91
  r = http.post("/reset", json={"task_name": task_name})
92
  r.raise_for_status()
 
99
  return r.json()
100
 
101
 
102
+ # ── Action parsing ─────────────────────────────────────────────────────────────
 
 
 
 
103
 
104
  def parse_action(raw: str) -> tuple:
105
  text = raw.strip()
 
113
  return "conclude", {}
114
 
115
 
116
+ # ── Episode runner ─────────────────────────────────────────────────────────────
117
+
118
  def run_task(llm: OpenAI, http: httpx.Client, task_name: str) -> float:
119
  rewards = []
120
  step_n = 0
 
133
 
134
  for step_n in range(1, MAX_STEPS + 1):
135
 
136
+ # ── LLM call — wrapped so a transient API error doesn't crash ──
137
  try:
138
  resp = llm.chat.completions.create(
139
  model = MODEL_NAME,
 
143
  )
144
  raw = resp.choices[0].message.content or ""
145
  except Exception as llm_err:
146
+ # Treat LLM failure as a no-op conclude to end gracefully
147
  raw = '{"command": "conclude", "parameters": {}}'
148
+ print(f"[DEBUG] LLM error at step {step_n}: {llm_err}", flush=True)
149
 
150
  command, params = parse_action(raw)
151
  action_str = json.dumps({"command": command, "parameters": params})
152
 
153
+ # ── Environment step ───────────────────────────────────────────
154
  obs = env_step(http, command, params)
155
  reward = float(obs.get("reward", 0.0))
156
  done = bool(obs.get("done", False))
 
166
  if done:
167
  break
168
 
169
+ score = min(1.0, max(0.0, sum(rewards)))
170
+ success = score > 0.0
171
+
172
+ except Exception as exc:
173
+ # Always emit [END] — even on unexpected failure
 
 
 
 
 
 
 
 
 
174
  log_end(success=False, steps=step_n, score=0.0, rewards=rewards)
175
+ raise
176
 
177
  log_end(success=success, steps=step_n, score=score, rewards=rewards)
178
  return score
179
 
180
 
181
+ # ── Entry point ────────────────────────────────────────────────────────────────
182
+
183
  def main() -> None:
184
  llm = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
185
  http = httpx.Client(base_url=ENV_BASE_URL, timeout=60)
 
187
  task_env = os.getenv("TASK_NAME", "")
188
  tasks = [task_env] if task_env in ALL_TASKS else ALL_TASKS
189
 
190
+ scores = {}
191
  try:
192
  for task in tasks:
193
+ scores[task] = run_task(llm, http, task)
194
  finally:
195
  http.close()
196
 
197
+ print("\n=== Baseline Results ===", flush=True)
198
+ for task, s in scores.items():
199
+ print(f" {task:8s} score={s:.2f}", flush=True)
200
+
201
 
202
  if __name__ == "__main__":
203
  main()
models.py CHANGED
@@ -57,6 +57,10 @@ class AgenticSecurityLabState(BaseModel):
57
  step_count: int = 0
58
 
59
  task_name: str = ""
 
 
 
 
60
 
61
  packages: dict[str, Any] = {} # name@version -> metadata
62
  dependents: dict[str, list[str]] = {} # package -> consumers
@@ -74,4 +78,8 @@ class AgenticSecurityLabState(BaseModel):
74
 
75
  total_reward: float = 0.0
76
  incident_contained: bool = False
77
- attacker_succeeded: bool = False
 
 
 
 
 
57
  step_count: int = 0
58
 
59
  task_name: str = ""
60
+ mode: str = "benchmark"
61
+ mode_fallback_used: bool = False
62
+ command_fallback_enabled: bool = True
63
+ command_fallback_used_count: int = 0
64
 
65
  packages: dict[str, Any] = {} # name@version -> metadata
66
  dependents: dict[str, list[str]] = {} # package -> consumers
 
78
 
79
  total_reward: float = 0.0
80
  incident_contained: bool = False
81
+ attacker_succeeded: bool = False
82
+ invalid_action_count: int = 0
83
+ duplicate_action_count: int = 0
84
+ false_positive_quarantines: int = 0
85
+ critical_rotated_before_deadline: bool = False
server/agentic_security_lab_environment.py CHANGED
@@ -33,6 +33,14 @@ from models import (
33
  )
34
  from scenarios import get_scenario
35
 
 
 
 
 
 
 
 
 
36
  VALID_COMMANDS = {
37
  "inspect_package",
38
  "check_dependents",
@@ -47,22 +55,50 @@ VALID_COMMANDS = {
47
  class AgenticSecurityLabEnvironment:
48
  """Supply chain incident response RL environment."""
49
 
50
- def __init__(self, task_name: str = "easy"):
 
 
 
 
 
51
  self._task_name = task_name
52
  self._state = AgenticSecurityLabState()
53
  self._scenario: dict[str, Any] = {}
54
-
55
- def reset(self, task_name: str | None = None) -> AgenticSecurityLabObservation:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  if task_name:
57
  self._task_name = task_name
 
 
 
 
58
 
59
  sc = get_scenario(self._task_name)
60
  self._scenario = sc
 
61
 
62
  self._state = AgenticSecurityLabState(
63
  episode_id = str(uuid.uuid4()),
64
  step_count = 0,
65
  task_name = self._task_name,
 
 
 
66
  packages = sc["packages"],
67
  dependents = sc["dependents"],
68
  secrets = sc["secrets"],
@@ -80,13 +116,22 @@ class AgenticSecurityLabEnvironment:
80
  result = (
81
  f"[INCIDENT ALERT] Task: {self._task_name.upper()}\n"
82
  f"{sc['description']}\n\n"
 
 
 
83
  f"Packages in scope : {list(sc['packages'].keys())}\n"
84
  f"Exfiltration in : {sc['exfiltration_step']} steps "
85
  f"(budget: {sc['max_steps']} steps total)\n\n"
86
  f"Commands available: inspect_package, check_dependents, "
87
  f"rotate_secret, quarantine, notify, scan_logs, conclude"
88
  ),
89
- data = {"packages_in_scope": list(sc["packages"].keys())},
 
 
 
 
 
 
90
  incident_summary = f"Incident open. {len(malicious)} malicious package(s) suspected.",
91
  steps_remaining = sc["exfiltration_step"],
92
  exposed_secrets = exposed,
@@ -112,10 +157,14 @@ class AgenticSecurityLabEnvironment:
112
 
113
  cmd = action.command.strip().lower()
114
  params = action.parameters
 
 
 
115
 
116
  if cmd not in VALID_COMMANDS:
117
  reward = -0.01
118
  s.total_reward += reward
 
119
  return self._build_obs(
120
  reward = reward,
121
  success = False,
@@ -200,6 +249,7 @@ class AgenticSecurityLabEnvironment:
200
  if s.secrets[secret]["rotated"]:
201
  reward = -0.02
202
  s.total_reward += reward
 
203
  return self._build_obs(reward, False,
204
  f"Secret '{secret}' was already rotated.", steps_left)
205
 
@@ -208,6 +258,8 @@ class AgenticSecurityLabEnvironment:
208
  is_critical = s.secrets[secret]["critical"]
209
  reward = 0.12 if is_critical else 0.06
210
  s.total_reward += reward
 
 
211
 
212
  return self._build_obs(
213
  reward = reward,
@@ -232,12 +284,14 @@ class AgenticSecurityLabEnvironment:
232
  error="Package not found")
233
 
234
  if pkg in s.quarantined:
 
235
  return self._build_obs(-0.02, False,
236
  f"'{pkg}' already quarantined.", steps_left)
237
 
238
  if not s.packages[pkg]["malicious"]:
239
  reward = -0.05
240
  s.total_reward += reward
 
241
  return self._build_obs(reward, False,
242
  f"⚠ False positive: '{pkg}' is NOT malicious. "
243
  f"Unnecessary quarantine harms users.", steps_left)
@@ -270,6 +324,7 @@ class AgenticSecurityLabEnvironment:
270
  error="Unknown team")
271
 
272
  if team in s.notified_teams:
 
273
  return self._build_obs(0.0, True,
274
  f"Team '{team}' already notified.", steps_left)
275
 
@@ -364,7 +419,13 @@ class AgenticSecurityLabEnvironment:
364
  done = done,
365
  reward = reward,
366
  result = result,
367
- data = data or {},
 
 
 
 
 
 
368
  incident_summary = (
369
  f"Step {s.step_count}/{s.max_steps} | "
370
  f"Quarantined: {len(s.quarantined)} | "
@@ -386,6 +447,13 @@ class AgenticSecurityLabEnvironment:
386
  done = True,
387
  reward = reward,
388
  result = result or "Episode ended.",
 
 
 
 
 
 
 
389
  incident_summary = (
390
  f"CLOSED | Steps: {s.step_count} | "
391
  f"Total reward: {s.total_reward:.3f}"
@@ -393,4 +461,56 @@ class AgenticSecurityLabEnvironment:
393
  steps_remaining = 0,
394
  exposed_secrets = [k for k, v in s.secrets.items() if not v["rotated"]],
395
  active_malicious_packages = [],
396
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  )
34
  from scenarios import get_scenario
35
 
36
+ VALID_MODES = {"benchmark", "training"}
37
+ COMMAND_ALIASES = {
38
+ "inspect": "inspect_package",
39
+ "dependents": "check_dependents",
40
+ "deps": "check_dependents",
41
+ "rotate": "rotate_secret",
42
+ }
43
+
44
  VALID_COMMANDS = {
45
  "inspect_package",
46
  "check_dependents",
 
55
  class AgenticSecurityLabEnvironment:
56
  """Supply chain incident response RL environment."""
57
 
58
+ def __init__(
59
+ self,
60
+ task_name: str = "easy",
61
+ mode: str = "benchmark",
62
+ command_fallback_enabled: bool = True,
63
+ ):
64
  self._task_name = task_name
65
  self._state = AgenticSecurityLabState()
66
  self._scenario: dict[str, Any] = {}
67
+ self._requested_mode = mode
68
+ self._command_fallback_enabled = command_fallback_enabled
69
+ self._mode_fallback_used = False
70
+
71
+ def _resolve_mode(self, mode: str) -> str:
72
+ if mode in VALID_MODES:
73
+ self._mode_fallback_used = False
74
+ return mode
75
+ self._mode_fallback_used = True
76
+ return "benchmark"
77
+
78
+ def reset(
79
+ self,
80
+ task_name: str | None = None,
81
+ mode: str | None = None,
82
+ command_fallback_enabled: bool | None = None,
83
+ ) -> AgenticSecurityLabObservation:
84
  if task_name:
85
  self._task_name = task_name
86
+ if mode is not None:
87
+ self._requested_mode = mode
88
+ if command_fallback_enabled is not None:
89
+ self._command_fallback_enabled = command_fallback_enabled
90
 
91
  sc = get_scenario(self._task_name)
92
  self._scenario = sc
93
+ resolved_mode = self._resolve_mode(self._requested_mode)
94
 
95
  self._state = AgenticSecurityLabState(
96
  episode_id = str(uuid.uuid4()),
97
  step_count = 0,
98
  task_name = self._task_name,
99
+ mode = resolved_mode,
100
+ mode_fallback_used = self._mode_fallback_used,
101
+ command_fallback_enabled = self._command_fallback_enabled,
102
  packages = sc["packages"],
103
  dependents = sc["dependents"],
104
  secrets = sc["secrets"],
 
116
  result = (
117
  f"[INCIDENT ALERT] Task: {self._task_name.upper()}\n"
118
  f"{sc['description']}\n\n"
119
+ f"Execution mode : {resolved_mode}"
120
+ + (" (fallback from unsupported mode)" if self._mode_fallback_used else "")
121
+ + "\n"
122
  f"Packages in scope : {list(sc['packages'].keys())}\n"
123
  f"Exfiltration in : {sc['exfiltration_step']} steps "
124
  f"(budget: {sc['max_steps']} steps total)\n\n"
125
  f"Commands available: inspect_package, check_dependents, "
126
  f"rotate_secret, quarantine, notify, scan_logs, conclude"
127
  ),
128
+ data = {
129
+ "packages_in_scope": list(sc["packages"].keys()),
130
+ "reward_type": "training_step_reward",
131
+ "benchmark_score": self._benchmark_score(),
132
+ "score_breakdown": self._benchmark_score_components(),
133
+ "evaluator_metrics": self._evaluator_metrics(),
134
+ },
135
  incident_summary = f"Incident open. {len(malicious)} malicious package(s) suspected.",
136
  steps_remaining = sc["exfiltration_step"],
137
  exposed_secrets = exposed,
 
157
 
158
  cmd = action.command.strip().lower()
159
  params = action.parameters
160
+ if self._state.command_fallback_enabled and cmd in COMMAND_ALIASES:
161
+ cmd = COMMAND_ALIASES[cmd]
162
+ self._state.command_fallback_used_count += 1
163
 
164
  if cmd not in VALID_COMMANDS:
165
  reward = -0.01
166
  s.total_reward += reward
167
+ s.invalid_action_count += 1
168
  return self._build_obs(
169
  reward = reward,
170
  success = False,
 
249
  if s.secrets[secret]["rotated"]:
250
  reward = -0.02
251
  s.total_reward += reward
252
+ s.duplicate_action_count += 1
253
  return self._build_obs(reward, False,
254
  f"Secret '{secret}' was already rotated.", steps_left)
255
 
 
258
  is_critical = s.secrets[secret]["critical"]
259
  reward = 0.12 if is_critical else 0.06
260
  s.total_reward += reward
261
+ if is_critical and s.step_count < s.exfiltration_step:
262
+ s.critical_rotated_before_deadline = True
263
 
264
  return self._build_obs(
265
  reward = reward,
 
284
  error="Package not found")
285
 
286
  if pkg in s.quarantined:
287
+ s.duplicate_action_count += 1
288
  return self._build_obs(-0.02, False,
289
  f"'{pkg}' already quarantined.", steps_left)
290
 
291
  if not s.packages[pkg]["malicious"]:
292
  reward = -0.05
293
  s.total_reward += reward
294
+ s.false_positive_quarantines += 1
295
  return self._build_obs(reward, False,
296
  f"⚠ False positive: '{pkg}' is NOT malicious. "
297
  f"Unnecessary quarantine harms users.", steps_left)
 
324
  error="Unknown team")
325
 
326
  if team in s.notified_teams:
327
+ s.duplicate_action_count += 1
328
  return self._build_obs(0.0, True,
329
  f"Team '{team}' already notified.", steps_left)
330
 
 
419
  done = done,
420
  reward = reward,
421
  result = result,
422
+ data = {
423
+ **(data or {}),
424
+ "reward_type": "training_step_reward",
425
+ "benchmark_score": self._benchmark_score(),
426
+ "score_breakdown": self._benchmark_score_components(),
427
+ "evaluator_metrics": self._evaluator_metrics(),
428
+ },
429
  incident_summary = (
430
  f"Step {s.step_count}/{s.max_steps} | "
431
  f"Quarantined: {len(s.quarantined)} | "
 
447
  done = True,
448
  reward = reward,
449
  result = result or "Episode ended.",
450
+ data = {
451
+ "reward_type": "training_step_reward",
452
+ "benchmark_score": self._benchmark_score(),
453
+ "score_breakdown": self._benchmark_score_components(),
454
+ "evaluator_metrics": self._evaluator_metrics(),
455
+ "total_training_reward": s.total_reward,
456
+ },
457
  incident_summary = (
458
  f"CLOSED | Steps: {s.step_count} | "
459
  f"Total reward: {s.total_reward:.3f}"
 
461
  steps_remaining = 0,
462
  exposed_secrets = [k for k, v in s.secrets.items() if not v["rotated"]],
463
  active_malicious_packages = [],
464
+ )
465
+
466
+ def _benchmark_score_components(self) -> dict[str, float]:
467
+ s = self._state
468
+ req = self._scenario.get("required_actions", {})
469
+ q_required = req.get("quarantine", [])
470
+ r_required = req.get("rotate_secret", [])
471
+ n_required = req.get("notify", [])
472
+
473
+ def ratio(done: list[str], required: list[str]) -> float:
474
+ if not required:
475
+ return 1.0
476
+ return len(set(done) & set(required)) / float(len(required))
477
+
478
+ quarantine_ratio = ratio(s.quarantined, q_required)
479
+ rotate_ratio = ratio(s.rotated_secrets, r_required)
480
+ notify_ratio = ratio(s.notified_teams, n_required)
481
+ contain_ratio = 1.0 if not s.attacker_succeeded else 0.0
482
+ return {
483
+ "quarantine_ratio": quarantine_ratio,
484
+ "rotate_ratio": rotate_ratio,
485
+ "notify_ratio": notify_ratio,
486
+ "contain_ratio": contain_ratio,
487
+ }
488
+
489
+ def _benchmark_score(self) -> float:
490
+ c = self._benchmark_score_components()
491
+ score = (
492
+ 0.35 * c["quarantine_ratio"]
493
+ + 0.35 * c["rotate_ratio"]
494
+ + 0.20 * c["notify_ratio"]
495
+ + 0.10 * c["contain_ratio"]
496
+ )
497
+ return max(0.0, min(1.0, round(score, 6)))
498
+
499
+ def _evaluator_metrics(self) -> dict[str, Any]:
500
+ s = self._state
501
+ return {
502
+ "task_name": s.task_name,
503
+ "mode": s.mode,
504
+ "mode_fallback_used": s.mode_fallback_used,
505
+ "command_fallback_enabled": s.command_fallback_enabled,
506
+ "command_fallback_used_count": s.command_fallback_used_count,
507
+ "invalid_action_count": s.invalid_action_count,
508
+ "duplicate_action_count": s.duplicate_action_count,
509
+ "false_positive_quarantines": s.false_positive_quarantines,
510
+ "critical_rotated_before_deadline": s.critical_rotated_before_deadline,
511
+ "attacker_succeeded": s.attacker_succeeded,
512
+ "steps_taken": s.step_count,
513
+ "steps_budget": s.max_steps,
514
+ "steps_to_exfiltration": max(0, s.exfiltration_step - s.step_count),
515
+ "total_training_reward": round(s.total_reward, 6),
516
+ }
server/app.py CHANGED
@@ -13,8 +13,14 @@ from models import (
13
  from server.agentic_security_lab_environment import AgenticSecurityLabEnvironment
14
 
15
  TASK_NAME = os.getenv("TASK_NAME", "easy")
 
 
16
 
17
- env = AgenticSecurityLabEnvironment(task_name=TASK_NAME)
 
 
 
 
18
 
19
  app = FastAPI(
20
  title="Agentic Security Lab — Supply Chain Incident Response",
@@ -29,6 +35,8 @@ app = FastAPI(
29
 
30
  class ResetRequest(BaseModel):
31
  task_name: str | None = None
 
 
32
 
33
 
34
  class StepRequest(BaseModel):
@@ -38,7 +46,11 @@ class StepRequest(BaseModel):
38
 
39
  @app.post("/reset", response_model=AgenticSecurityLabObservation)
40
  def reset(req: ResetRequest = ResetRequest()):
41
- return env.reset(task_name=req.task_name)
 
 
 
 
42
 
43
 
44
  @app.post("/step", response_model=AgenticSecurityLabObservation)
@@ -54,7 +66,12 @@ def state():
54
 
55
  @app.get("/health")
56
  def health():
57
- return {"status": "ok", "task": env.state.task_name}
 
 
 
 
 
58
 
59
 
60
  @app.get("/")
 
13
  from server.agentic_security_lab_environment import AgenticSecurityLabEnvironment
14
 
15
  TASK_NAME = os.getenv("TASK_NAME", "easy")
16
+ MODE = os.getenv("AGENTIC_SECURITY_LAB_MODE", "benchmark")
17
+ COMMAND_FALLBACK = os.getenv("AGENTIC_SECURITY_LAB_COMMAND_FALLBACK", "true").lower() in {"1", "true", "yes"}
18
 
19
+ env = AgenticSecurityLabEnvironment(
20
+ task_name=TASK_NAME,
21
+ mode=MODE,
22
+ command_fallback_enabled=COMMAND_FALLBACK,
23
+ )
24
 
25
  app = FastAPI(
26
  title="Agentic Security Lab — Supply Chain Incident Response",
 
35
 
36
  class ResetRequest(BaseModel):
37
  task_name: str | None = None
38
+ mode: str | None = None
39
+ command_fallback_enabled: bool | None = None
40
 
41
 
42
  class StepRequest(BaseModel):
 
46
 
47
  @app.post("/reset", response_model=AgenticSecurityLabObservation)
48
  def reset(req: ResetRequest = ResetRequest()):
49
+ return env.reset(
50
+ task_name=req.task_name,
51
+ mode=req.mode,
52
+ command_fallback_enabled=req.command_fallback_enabled,
53
+ )
54
 
55
 
56
  @app.post("/step", response_model=AgenticSecurityLabObservation)
 
66
 
67
  @app.get("/health")
68
  def health():
69
+ return {
70
+ "status": "ok",
71
+ "task": env.state.task_name,
72
+ "mode": env.state.mode,
73
+ "mode_fallback_used": env.state.mode_fallback_used,
74
+ }
75
 
76
 
77
  @app.get("/")