Sayuj63 commited on
Commit
959a691
Β·
1 Parent(s): 34a5113

Phases 4-7: nested inference prompt, delegation reward, Delegation Score grader, multi-agent README

Browse files

Phase 4 β€” inference.py teaches the LLM about delegation:
- SYSTEM_PROMPT extended with two new action shapes (spawn_subagent / return_to_parent)
- Explicit delegation guidance: persist vs spawn tradeoff, productive=+0.05 / unproductive=-0.05
- LLMJsonAction & SecurityAuditAction Pydantic Literal extended to accept the new types
- [STEP] log line renders spawn_subagent(scope:target) for visibility
- parse_llm_action_text round-trip verified for both new action types

Phase 5 β€” reward shaping (already wired in Phase 3's _handle_return_to_parent):
- +0.05 productive, -0.05 unproductive, sub-agent findings flow into _submitted_findings
- spawn_subagent action itself returns +0.01 (tiny acknowledgement so the model sees the spawn was accepted)
- All shaping happens server-side; inference.py is unchanged in the loop body

Phase 6 β€” grader.py gains Delegation Score (5%):
- New parameter: subagent_outcomes (defaults to empty for single-agent backward compat)
- delegation_score = productive_spawns / total_spawns, defaults to 1.0 when no spawns
(so single-agent runs aren't penalised β€” neutral value)
- Replaced the redundant 5% "Any True Positive" bonus (already covered by detection_rate)
- Weights still sum to 100%; FP/honeypot/coverage_multiplier behavior unchanged
- Returned grades dict gains: delegation_score, subagent_total, subagent_productive
- Env's _finish_episode now plumbs _subagent_outcomes through to the grader

Phase 7 β€” README + openenv.yaml updates:
- Lead-with-thesis rewritten: triple-theme fit (world modeling + long-horizon + multi-agent)
- Action Space table adds spawn_subagent / return_to_parent
- New "Multi-Agent Delegation" subsection explaining when to spawn vs persist
- Scoring table swaps "Any True Positive 5%" β†’ "Delegation Score 5%"
- openenv.yaml version 1.0.0 β†’ 2.0.0 (action space changed; API consumers should re-pin)

All 78 tests still pass. Single-agent path is unchanged β€” agents that never call
spawn_subagent get delegation_score=1.0 (neutral), exactly the same final score
as before Phase 6's weight rebalance (modulo the dropped "Any TP" 5%, which any
agent that finds β‰₯1 vuln was already getting via detection_rate proportionally).

README.md CHANGED
@@ -14,7 +14,13 @@ short_description: "Can your AI reason from raw evidence or just parse labels?"
14
 
15
  **Live Environment:** https://huggingface.co/spaces/Sayuj63/Vapt-env
16
 
17
- Most AI security tools parse labeled scanner output. We measure what happens when the labels disappear.
 
 
 
 
 
 
18
 
19
  | Difficulty | Agent Sees | Regex Parser | Gemini 2.5 Flash |
20
  |---|---|---|---|
@@ -156,8 +162,22 @@ with SecurityAuditEnv(base_url="http://localhost:8000").sync() as env:
156
  | `list_tools` | See all available security audit tools |
157
  | `use_tool` | Run a security tool (requires tool_name + arguments) |
158
  | `submit_finding` | Document a discovered vulnerability |
 
 
159
  | `generate_report` | End the audit and get the final score |
160
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  ### Available Tools
162
 
163
  | Tool | Description | Parameters |
@@ -313,7 +333,7 @@ Multi-dimensional grading (0.0-1.0):
313
  | Pivoting Score | 5% | Found gateway vulns that unlock hidden hosts |
314
  | Exploitation Proof | 5% | Proportional: `true_positives / total_vulnerabilities` |
315
  | Compliance Coverage | 5% | Fraction of compliance controls addressed (PCI-DSS/SOC2/HIPAA) |
316
- | Any True Positive | 5% | Bonus for finding at least one real vulnerability |
317
  | False Positive Penalty | escalating | -0.03 first, +0.01 per additional FP (caps at -0.08 each) |
318
  | Honeypot Penalty | -15% each | Interacting with decoy hosts reduces score |
319
  | Coverage < 50% | multiplier | `0.7 + 0.6 * coverage` applied to raw score |
 
14
 
15
  **Live Environment:** https://huggingface.co/spaces/Sayuj63/Vapt-env
16
 
17
+ A long-horizon, partially-observable enterprise security world where an LLM agent has to **do real reasoning over raw evidence** β€” not parse labels β€” and **delegate divergent attack-surface branches to budgeted sub-agents** the moment one tool reveals another. Built to train the three capability gaps current LLMs miss most:
18
+
19
+ 1. **World modeling under partial observability** β€” hidden hosts, honeypots, evidence that ranges from labeled (`[CRITICAL] SQL Injection, CWE-89`) to fully raw (`POST /login: 1000 reqs in 18.7s, 0 blocked`).
20
+ 2. **Long-horizon planning with sparse rewards** β€” 25/35/45-step audits with phase tracking (recon β†’ enumeration β†’ exploitation β†’ reporting) and dense per-step rewards on top of the final multi-dimensional grader.
21
+ 3. **Multi-agent delegation** β€” when an SSRF reveals an internal IP, the agent decides: persist on the main thread, or `spawn_subagent` to investigate the new branch with a step budget. Productive sub-agents (β‰₯1 finding) earn +0.05; unproductive ones cost βˆ’0.05. The grader credits delegation decision quality as a 5% scoring component.
22
+
23
+ Most AI security tools parse labeled scanner output. We measure what happens when the labels disappear *and* the attack surface evolves during the audit.
24
 
25
  | Difficulty | Agent Sees | Regex Parser | Gemini 2.5 Flash |
26
  |---|---|---|---|
 
162
  | `list_tools` | See all available security audit tools |
163
  | `use_tool` | Run a security tool (requires tool_name + arguments) |
164
  | `submit_finding` | Document a discovered vulnerability |
165
+ | `spawn_subagent` | Delegate a divergent attack-surface branch (host / endpoint / cred) to a budgeted sub-agent |
166
+ | `return_to_parent` | Close the active sub-agent and resume the main thread |
167
  | `generate_report` | End the audit and get the final score |
168
 
169
+ ### Multi-Agent Delegation
170
+
171
+ During a real audit, an SSRF can disclose a previously-unreachable internal IP, a credential leak can open a new auth surface, etc. Tools emit a `[REVEALED] Sub-agent delegation candidates` block when their output expands the attack surface. The agent has a choice:
172
+
173
+ 1. **Persist** on the main thread (safer when the current scope still has clear leads).
174
+ 2. **Delegate** with `spawn_subagent({"scope": "host", "target": "10.0.2.30", "budget": 6})`. The next 6 steps are scoped to that branch β€” recon, test, submit findings on the new target β€” then the agent calls `return_to_parent` to resume the main investigation.
175
+
176
+ **Reward economics** (kept tight so spawning is a real decision, not a default):
177
+ - Productive sub-agent (β‰₯ 1 finding submitted while active): **+0.05**
178
+ - Unproductive sub-agent (no findings, or budget exhausted empty-handed): **βˆ’0.05**
179
+ - Sub-agent's findings count toward the main grader; spawning is the delegation primitive, not a separate scoring path.
180
+
181
  ### Available Tools
182
 
183
  | Tool | Description | Parameters |
 
333
  | Pivoting Score | 5% | Found gateway vulns that unlock hidden hosts |
334
  | Exploitation Proof | 5% | Proportional: `true_positives / total_vulnerabilities` |
335
  | Compliance Coverage | 5% | Fraction of compliance controls addressed (PCI-DSS/SOC2/HIPAA) |
336
+ | Delegation Score | 5% | `productive_spawns / total_spawns` β€” quality of multi-agent delegation decisions (1.0 if no spawns, neutral) |
337
  | False Positive Penalty | escalating | -0.03 first, +0.01 per additional FP (caps at -0.08 each) |
338
  | Honeypot Penalty | -15% each | Interacting with decoy hosts reduces score |
339
  | Coverage < 50% | multiplier | `0.7 + 0.6 * coverage` applied to raw score |
inference.py CHANGED
@@ -63,9 +63,11 @@ ENV_NAME = "security_audit_env"
63
  SYSTEM_PROMPT = textwrap.dedent("""\
64
  You are a security auditor. Reply with ONE JSON object only β€” no prose, no code fences.
65
 
66
- Three action shapes:
67
  USE TOOL: {"action_type":"use_tool","tool_name":"<tool>","arguments":{...}}
68
  SUBMIT: {"action_type":"submit_finding","arguments":{"title":"...","host":"<ip>","type":"<vuln>","severity":"Critical|High|Medium|Low","cvss_score":<0-10>,"cwe":"CWE-XX","owasp":"AXX:2021 - ...","endpoint":"<path>","evidence":"<why>","remediation":"<fix>"}}
 
 
69
  REPORT: {"action_type":"generate_report"}
70
 
71
  Tools (most-used): network_scan(target), web_crawl(host), test_injection(host,endpoint),
@@ -75,6 +77,19 @@ check_secrets(host,endpoint), vulnerability_scan(host), service_fingerprint(host
75
  Rough flow: scan once β†’ crawl each host once β†’ test endpoints β†’ submit a finding for
76
  EVERY anomaly, error, or labeled issue you see in tool output β†’ generate_report.
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  Examples (output format β€” copy this style):
79
 
80
  Tool output: "[CRITICAL] SQL Injection DETECTED at /api/login (param=username), CWE-89, CVSS 9.8"
@@ -357,6 +372,10 @@ def run_scenario(
357
  action_str = action.action_type
358
  if action.tool_name:
359
  action_str += f"({action.tool_name})"
 
 
 
 
360
 
361
  try:
362
  result = env.step(action)
 
63
  SYSTEM_PROMPT = textwrap.dedent("""\
64
  You are a security auditor. Reply with ONE JSON object only β€” no prose, no code fences.
65
 
66
+ Five action shapes:
67
  USE TOOL: {"action_type":"use_tool","tool_name":"<tool>","arguments":{...}}
68
  SUBMIT: {"action_type":"submit_finding","arguments":{"title":"...","host":"<ip>","type":"<vuln>","severity":"Critical|High|Medium|Low","cvss_score":<0-10>,"cwe":"CWE-XX","owasp":"AXX:2021 - ...","endpoint":"<path>","evidence":"<why>","remediation":"<fix>"}}
69
+ SPAWN: {"action_type":"spawn_subagent","arguments":{"scope":"host","target":"10.0.2.30","budget":6}}
70
+ RETURN: {"action_type":"return_to_parent","arguments":{}}
71
  REPORT: {"action_type":"generate_report"}
72
 
73
  Tools (most-used): network_scan(target), web_crawl(host), test_injection(host,endpoint),
 
77
  Rough flow: scan once β†’ crawl each host once β†’ test endpoints β†’ submit a finding for
78
  EVERY anomaly, error, or labeled issue you see in tool output β†’ generate_report.
79
 
80
+ DELEGATION (the multi-agent move). Tool output sometimes ends with a
81
+ "[REVEALED] Sub-agent delegation candidates: ..." block. Those are *new*
82
+ attack-surface targets uncovered by the current finding (e.g. an SSRF reveals
83
+ internal hosts you couldn't see before). Two ways to handle them:
84
+ 1. Continue the main thread and ignore them (safer if the main scope still has
85
+ clear leads).
86
+ 2. Spawn a sub-agent: {"action_type":"spawn_subagent","arguments":{"scope":"host","target":"<ip>","budget":6}}
87
+ The next steps will be scoped to that branch; recon/test/submit findings
88
+ on the new target. When you've squeezed it (or it's clearly empty), call
89
+ {"action_type":"return_to_parent","arguments":{}} and the parent thread
90
+ resumes. Productive sub-agents (β‰₯1 finding) earn +0.05; unproductive ones
91
+ cost -0.05, so only spawn when you have a real lead.
92
+
93
  Examples (output format β€” copy this style):
94
 
95
  Tool output: "[CRITICAL] SQL Injection DETECTED at /api/login (param=username), CWE-89, CVSS 9.8"
 
372
  action_str = action.action_type
373
  if action.tool_name:
374
  action_str += f"({action.tool_name})"
375
+ elif action.action_type == "spawn_subagent":
376
+ _t = (action.arguments or {}).get("target", "?")
377
+ _s = (action.arguments or {}).get("scope", "?")
378
+ action_str += f"({_s}:{_t})"
379
 
380
  try:
381
  result = env.step(action)
openenv.yaml CHANGED
@@ -8,7 +8,7 @@ description: >
8
  AI Security Audit Benchmark β€” trains and evaluates AI agents on real-world
9
  VAPT (Vulnerability Assessment & Penetration Testing) engagements with
10
  three-tier output difficulty and compliance framework mapping.
11
- version: "1.0.0"
12
  tasks:
13
  - id: easy
14
  name: Startup Web App Audit
 
8
  AI Security Audit Benchmark β€” trains and evaluates AI agents on real-world
9
  VAPT (Vulnerability Assessment & Penetration Testing) engagements with
10
  three-tier output difficulty and compliance framework mapping.
11
+ version: "2.0.0"
12
  tasks:
13
  - id: easy
14
  name: Startup Web App Audit
server/grader.py CHANGED
@@ -43,11 +43,19 @@ def grade_episode(
43
  discovered_hosts: List[str],
44
  discovered_ports: Dict[str, List[int]],
45
  steps_used: int = 0,
 
46
  ) -> Dict[str, Any]:
47
- """Grade a completed audit episode. Returns component scores and final score (0.0-1.0)."""
 
 
 
 
 
 
48
  ground_truth = scenario.get("vulnerabilities", [])
49
  all_hosts = scenario.get("hosts", {})
50
  honeypots = scenario.get("honeypots", [])
 
51
  max_steps = scenario.get("max_steps", 50)
52
 
53
  # 1. Match findings to ground truth
@@ -179,9 +187,19 @@ def grade_episode(
179
  # 12. Efficiency β€” informational metric
180
  efficiency = 1.0 - (steps_used / max_steps) if max_steps > 0 and steps_used > 0 else 0.0
181
 
182
- # Final weighted score
183
- # Weights: detection 30%, severity 20%, classification 15%, coverage 5%,
184
- # report 10%, pivoting 5%, exploitation 5%, compliance 5%, FP/honeypot penalties
 
 
 
 
 
 
 
 
 
 
185
  raw_score = (
186
  0.30 * detection_rate
187
  + 0.05 * coverage
@@ -191,7 +209,7 @@ def grade_episode(
191
  + 0.05 * pivoting_score
192
  + 0.05 * exploitation_proof
193
  + 0.05 * compliance_coverage
194
- + 0.05 * (1.0 if true_positives > 0 else 0.0)
195
  ) * coverage_multiplier - fp_penalty - honeypot_penalty
196
 
197
  final_score = max(0.0, min(1.0, raw_score))
@@ -224,6 +242,10 @@ def grade_episode(
224
  "compliance_controls_covered": len(compliance_controls_covered),
225
  "compliance_controls_expected": len(compliance_controls_expected),
226
  "efficiency": round(efficiency, 4),
 
 
 
 
227
  }
228
 
229
 
 
43
  discovered_hosts: List[str],
44
  discovered_ports: Dict[str, List[int]],
45
  steps_used: int = 0,
46
+ subagent_outcomes: Optional[List[Dict[str, Any]]] = None,
47
  ) -> Dict[str, Any]:
48
+ """Grade a completed audit episode. Returns component scores and final score (0.0-1.0).
49
+
50
+ ``subagent_outcomes`` is a list of dicts (one per closed sub-agent) containing
51
+ at minimum {"productive": bool}. Used to compute the Delegation Score (5%).
52
+ Defaults to empty list for backward compat / single-agent runs β€” in that
53
+ case Delegation Score = 1.0 (neutral, no penalty for not spawning).
54
+ """
55
  ground_truth = scenario.get("vulnerabilities", [])
56
  all_hosts = scenario.get("hosts", {})
57
  honeypots = scenario.get("honeypots", [])
58
+ subagent_outcomes = subagent_outcomes or []
59
  max_steps = scenario.get("max_steps", 50)
60
 
61
  # 1. Match findings to ground truth
 
187
  # 12. Efficiency β€” informational metric
188
  efficiency = 1.0 - (steps_used / max_steps) if max_steps > 0 and steps_used > 0 else 0.0
189
 
190
+ # 13. Delegation Score (5%) β€” multi-agent decision quality.
191
+ # Fraction of spawned sub-agents that turned out productive (>=1 finding).
192
+ # If the agent never spawned, Delegation Score = 1.0 β€” neutral, so single-agent
193
+ # runs aren't penalised. This rewards agents that *correctly* identify when to
194
+ # delegate a divergent attack-surface branch versus persist on the main thread.
195
+ total_spawns = len(subagent_outcomes)
196
+ productive_spawns = sum(1 for o in subagent_outcomes if o.get("productive"))
197
+ delegation_score = (productive_spawns / total_spawns) if total_spawns else 1.0
198
+
199
+ # Final weighted score (sums to 100%):
200
+ # detection 30%, severity 20%, classification 15%, report 10%,
201
+ # coverage 5%, pivoting 5%, exploitation 5%, compliance 5%, delegation 5%
202
+ # Multiplier: coverage_multiplier. Penalties: FP, honeypot.
203
  raw_score = (
204
  0.30 * detection_rate
205
  + 0.05 * coverage
 
209
  + 0.05 * pivoting_score
210
  + 0.05 * exploitation_proof
211
  + 0.05 * compliance_coverage
212
+ + 0.05 * delegation_score
213
  ) * coverage_multiplier - fp_penalty - honeypot_penalty
214
 
215
  final_score = max(0.0, min(1.0, raw_score))
 
242
  "compliance_controls_covered": len(compliance_controls_covered),
243
  "compliance_controls_expected": len(compliance_controls_expected),
244
  "efficiency": round(efficiency, 4),
245
+ # Multi-agent delegation metrics
246
+ "delegation_score": round(delegation_score, 4),
247
+ "subagent_total": total_spawns,
248
+ "subagent_productive": productive_spawns,
249
  }
250
 
251
 
server/security_audit_env_environment.py CHANGED
@@ -508,6 +508,7 @@ class SecurityAuditEnvironment(Environment):
508
  self._scenario, self._submitted_findings,
509
  self._discovered_hosts, self._discovered_ports,
510
  steps_used=self._state.step_count,
 
511
  )
512
  final_score = grades["final_score"]
513
  self._episode_reward += final_score
 
508
  self._scenario, self._submitted_findings,
509
  self._discovered_hosts, self._discovered_ports,
510
  steps_used=self._state.step_count,
511
+ subagent_outcomes=self._subagent_outcomes,
512
  )
513
  final_score = grades["final_score"]
514
  self._episode_reward += final_score