Spaces:
Sleeping
Sleeping
ajaxwin commited on
Commit Β·
cfae7a7
1
Parent(s): 0b06e9e
refactor: Update grading logic and submission handling across tasks for improved accuracy and consistency
Browse files- inference.py +2 -2
- server/tasks/task1/actions.py +42 -18
- server/tasks/task1/grader.py +36 -8
- server/tasks/task2/actions.py +40 -17
- server/tasks/task2/grader.py +14 -16
- server/tasks/task3/actions.py +23 -15
- server/tasks/task3/grader.py +41 -27
- utils/prompts.py +12 -0
- utils/semanticmatcher.py +16 -5
inference.py
CHANGED
|
@@ -258,7 +258,7 @@ def _run_t1_episode(env: Task1Environment, seed: int, ep_num: int) -> Dict[str,
|
|
| 258 |
|
| 259 |
if done:
|
| 260 |
v = r_val
|
| 261 |
-
grader_score =
|
| 262 |
break
|
| 263 |
|
| 264 |
if not is_last:
|
|
@@ -441,7 +441,7 @@ def _run_t3_episode(env: Task3Environment, seed: int, ep_num: int) -> Dict[str,
|
|
| 441 |
|
| 442 |
if done:
|
| 443 |
v = r_val
|
| 444 |
-
grader_score =
|
| 445 |
break
|
| 446 |
|
| 447 |
if not is_last:
|
|
|
|
| 258 |
|
| 259 |
if done:
|
| 260 |
v = r_val
|
| 261 |
+
grader_score = 0.999 if v >= 4.9 else (0.5 if v >= 0.9 else 0.0)
|
| 262 |
break
|
| 263 |
|
| 264 |
if not is_last:
|
|
|
|
| 441 |
|
| 442 |
if done:
|
| 443 |
v = r_val
|
| 444 |
+
grader_score = 0.999 if v >= 4.9 else (0.3 if v >= 0.999 else 0.0)
|
| 445 |
break
|
| 446 |
|
| 447 |
if not is_last:
|
server/tasks/task1/actions.py
CHANGED
|
@@ -117,38 +117,62 @@ def get_call_graph(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
|
|
| 117 |
)
|
| 118 |
|
| 119 |
|
| 120 |
-
def
|
| 121 |
-
"""Handle
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
if not fn_name or not vuln_type:
|
| 125 |
return (
|
| 126 |
-
"
|
| 127 |
-
|
|
|
|
| 128 |
)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
ctx._done
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
if score == 1.0:
|
| 134 |
msg = (
|
| 135 |
-
f"β
CORRECT! '{fn_name}' is the vulnerable function
|
| 136 |
-
f"
|
|
|
|
| 137 |
)
|
| 138 |
elif score == 0.5:
|
| 139 |
msg = (
|
| 140 |
-
f"
|
| 141 |
-
f"
|
|
|
|
|
|
|
| 142 |
)
|
| 143 |
else:
|
| 144 |
-
correct = ctx._grader.get_canonical_answer()
|
| 145 |
msg = (
|
| 146 |
-
f"β INCORRECT. '{fn_name}' is not the target
|
| 147 |
-
f"
|
|
|
|
|
|
|
| 148 |
)
|
|
|
|
| 149 |
return msg, Reward(
|
| 150 |
value=reward_val,
|
| 151 |
-
reason=f"
|
| 152 |
partial=False,
|
| 153 |
)
|
| 154 |
|
|
|
|
| 117 |
)
|
| 118 |
|
| 119 |
|
| 120 |
+
def submit_function(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
|
| 121 |
+
"""Handle SUBMIT_FUNCTION action for Task 1.
|
| 122 |
+
|
| 123 |
+
Expected params
|
| 124 |
+
---------------
|
| 125 |
+
function_name : str β name of the vulnerable function
|
| 126 |
+
vulnerability_type: str β short description of the vulnerability
|
| 127 |
+
"""
|
| 128 |
+
if ctx._submitted:
|
| 129 |
+
return (
|
| 130 |
+
"β You have already submitted for this episode. "
|
| 131 |
+
"Only ONE submission is allowed.",
|
| 132 |
+
Reward(value=0.0, reason="Second submit_function attempt", partial=False),
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
fn_name = params.get("function_name", "").strip()
|
| 136 |
+
vuln_type = params.get("vulnerability_type", "").strip()
|
| 137 |
+
|
| 138 |
if not fn_name or not vuln_type:
|
| 139 |
return (
|
| 140 |
+
"submit_function requires both 'function_name' and "
|
| 141 |
+
"'vulnerability_type' in params.",
|
| 142 |
+
Reward(value=0.0, reason="Malformed submission", partial=False),
|
| 143 |
)
|
| 144 |
+
|
| 145 |
+
ctx._submitted = True
|
| 146 |
+
ctx._done = True
|
| 147 |
+
|
| 148 |
+
score = ctx._grader.grade_submission(fn_name, vuln_type) # {0.0, 0.5, 1.0}
|
| 149 |
+
reward_val = ctx._grader.reward_for_score(score) # [0.0, 1.0]
|
| 150 |
+
correct = ctx._grader.get_canonical_answer()
|
| 151 |
+
|
| 152 |
if score == 1.0:
|
| 153 |
msg = (
|
| 154 |
+
f"β
CORRECT! '{fn_name}' is the vulnerable function "
|
| 155 |
+
f"and the vulnerability type matches. "
|
| 156 |
+
f"Score: 1.0 β Reward: {reward_val:.3f}"
|
| 157 |
)
|
| 158 |
elif score == 0.5:
|
| 159 |
msg = (
|
| 160 |
+
f"π‘ PARTIAL. '{fn_name}' is the correct function but the "
|
| 161 |
+
f"vulnerability type was not recognised. "
|
| 162 |
+
f"Score: 0.5 β Reward: {reward_val:.3f}. "
|
| 163 |
+
f"Expected vulnerability: '{correct['vulnerability']}'."
|
| 164 |
)
|
| 165 |
else:
|
|
|
|
| 166 |
msg = (
|
| 167 |
+
f"β INCORRECT. '{fn_name}' is not the target function. "
|
| 168 |
+
f"Score: 0.0 β Reward: {reward_val:.3f}. "
|
| 169 |
+
f"Correct answer: function='{correct['function']}', "
|
| 170 |
+
f"vulnerability='{correct['vulnerability']}'."
|
| 171 |
)
|
| 172 |
+
|
| 173 |
return msg, Reward(
|
| 174 |
value=reward_val,
|
| 175 |
+
reason=f"submit_function score={score:.1f}",
|
| 176 |
partial=False,
|
| 177 |
)
|
| 178 |
|
server/tasks/task1/grader.py
CHANGED
|
@@ -1,30 +1,58 @@
|
|
| 1 |
"""
|
| 2 |
grader.py (Task 1 β Targeted Vulnerability Detection)
|
| 3 |
-------------------------------------------------------
|
| 4 |
-
Deterministic grader.
|
| 5 |
|
| 6 |
1.0 β correct function + correct vulnerability keyword
|
| 7 |
0.5 β correct function + wrong/unrecognised vulnerability keyword
|
| 8 |
0.0 β wrong function name
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
from __future__ import annotations
|
| 11 |
from typing import Dict
|
| 12 |
from utils import SemanticMatcher
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
class Task1Grader:
|
| 15 |
def __init__(self, target_function: str, vulnerability_issue: str) -> None:
|
| 16 |
-
self.target_function
|
| 17 |
self.vulnerability_issue = vulnerability_issue
|
| 18 |
|
| 19 |
def grade_submission(self, submitted_function: str, submitted_vuln_type: str) -> float:
|
|
|
|
| 20 |
if submitted_function.strip().lower() != self.target_function:
|
| 21 |
-
return 0.0
|
| 22 |
-
return 1.0 if SemanticMatcher().match(self.vulnerability_issue, submitted_vuln_type) else 0.5
|
| 23 |
|
| 24 |
def reward_for_score(self, score: float) -> float:
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def get_canonical_answer(self) -> Dict[str, str]:
|
| 30 |
-
return {"function": self.target_function, "vulnerability": self.vulnerability_issue}
|
|
|
|
| 1 |
"""
|
| 2 |
grader.py (Task 1 β Targeted Vulnerability Detection)
|
| 3 |
-------------------------------------------------------
|
| 4 |
+
Deterministic grader. Grade range: 0.0 β 1.0
|
| 5 |
|
| 6 |
1.0 β correct function + correct vulnerability keyword
|
| 7 |
0.5 β correct function + wrong/unrecognised vulnerability keyword
|
| 8 |
0.0 β wrong function name
|
| 9 |
+
|
| 10 |
+
reward_for_score() normalises the raw RL reward to [0.0, 1.0]
|
| 11 |
+
using the fixed reward bounds [MIN_REWARD=-1.5, MAX_REWARD=5.0]:
|
| 12 |
+
normalised = (raw + 1.5) / 6.5
|
| 13 |
"""
|
| 14 |
from __future__ import annotations
|
| 15 |
from typing import Dict
|
| 16 |
from utils import SemanticMatcher
|
| 17 |
|
| 18 |
+
# Raw reward bounds β used only for normalisation
|
| 19 |
+
_MIN_REWARD = -1.5
|
| 20 |
+
_MAX_REWARD = 5.0
|
| 21 |
+
_REWARD_RANGE = _MAX_REWARD - _MIN_REWARD # 6.5
|
| 22 |
+
|
| 23 |
+
_SCORE_MIN = 0.001 # grades are strictly (0, 1)
|
| 24 |
+
_SCORE_MAX = 0.999
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _clamp(v: float) -> float:
|
| 28 |
+
return max(_SCORE_MIN, min(_SCORE_MAX, v))
|
| 29 |
+
|
| 30 |
+
|
| 31 |
class Task1Grader:
|
| 32 |
def __init__(self, target_function: str, vulnerability_issue: str) -> None:
|
| 33 |
+
self.target_function = target_function.lower()
|
| 34 |
self.vulnerability_issue = vulnerability_issue
|
| 35 |
|
| 36 |
def grade_submission(self, submitted_function: str, submitted_vuln_type: str) -> float:
|
| 37 |
+
"""Returns grade strictly in (0, 1)."""
|
| 38 |
if submitted_function.strip().lower() != self.target_function:
|
| 39 |
+
return _clamp(0.0) # β 0.001
|
| 40 |
+
return _clamp(1.0) if SemanticMatcher().match(self.vulnerability_issue, submitted_vuln_type) else _clamp(0.5)
|
| 41 |
|
| 42 |
def reward_for_score(self, score: float) -> float:
|
| 43 |
+
"""
|
| 44 |
+
Maps grade score β normalised reward strictly in (0, 1).
|
| 45 |
+
|
| 46 |
+
Raw rewards: correct=+5.0, partial=+1.0, wrong=-1.5
|
| 47 |
+
Normalised: (raw + 1.5) / 6.5 then clamped to (0.001, 0.999)
|
| 48 |
+
"""
|
| 49 |
+
if score >= _SCORE_MAX:
|
| 50 |
+
raw = 5.0
|
| 51 |
+
elif score >= 0.5:
|
| 52 |
+
raw = 1.0
|
| 53 |
+
else:
|
| 54 |
+
raw = -1.5
|
| 55 |
+
return _clamp((raw - _MIN_REWARD) / _REWARD_RANGE)
|
| 56 |
|
| 57 |
def get_canonical_answer(self) -> Dict[str, str]:
|
| 58 |
+
return {"function": self.target_function, "vulnerability": self.vulnerability_issue}
|
server/tasks/task2/actions.py
CHANGED
|
@@ -105,30 +105,53 @@ def get_similar_rule_action(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Rew
|
|
| 105 |
|
| 106 |
|
| 107 |
def submit_property(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
|
| 108 |
-
"""Handle SUBMIT_PROPERTY action.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
if ctx._submitted:
|
| 110 |
return (
|
| 111 |
-
"β You have already submitted
|
| 112 |
-
"Only
|
| 113 |
-
Reward(value=
|
| 114 |
)
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
| 118 |
return (
|
| 119 |
-
"
|
| 120 |
-
Reward(value=
|
| 121 |
)
|
| 122 |
-
|
| 123 |
ctx._submitted = True
|
| 124 |
-
ctx._done
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
return msg, Reward(
|
| 130 |
-
value=
|
| 131 |
-
reason=f"
|
| 132 |
partial=False,
|
| 133 |
)
|
| 134 |
|
|
|
|
| 105 |
|
| 106 |
|
| 107 |
def submit_property(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
|
| 108 |
+
"""Handle SUBMIT_PROPERTY action for Task 2.
|
| 109 |
+
|
| 110 |
+
Expected params
|
| 111 |
+
---------------
|
| 112 |
+
property : str β natural-language property describing the function's behaviour
|
| 113 |
+
"""
|
| 114 |
if ctx._submitted:
|
| 115 |
return (
|
| 116 |
+
"β You have already submitted for this episode. "
|
| 117 |
+
"Only ONE submission is allowed.",
|
| 118 |
+
Reward(value=0.0, reason="Second submit_property attempt", partial=False),
|
| 119 |
)
|
| 120 |
+
|
| 121 |
+
submitted_property = params.get("property", "").strip()
|
| 122 |
+
|
| 123 |
+
if not submitted_property:
|
| 124 |
return (
|
| 125 |
+
"submit_property requires a non-empty 'property' string in params.",
|
| 126 |
+
Reward(value=0.0, reason="Malformed submission", partial=False),
|
| 127 |
)
|
| 128 |
+
|
| 129 |
ctx._submitted = True
|
| 130 |
+
ctx._done = True
|
| 131 |
+
|
| 132 |
+
# grade() returns (float score in [0,1], confidence str)
|
| 133 |
+
score, confidence = ctx._grader.grade(submitted_property) # score already in [0.0, 1.0]
|
| 134 |
+
reward_val = float(score) # reward == grade for Task 2
|
| 135 |
+
|
| 136 |
+
if confidence == "strong":
|
| 137 |
+
msg = (
|
| 138 |
+
f"β
STRONG MATCH. Your property closely matches the target. "
|
| 139 |
+
f"Score: {score:.3f} β Reward: {reward_val:.3f}"
|
| 140 |
+
)
|
| 141 |
+
elif confidence == "moderate":
|
| 142 |
+
msg = (
|
| 143 |
+
f"π‘ MODERATE MATCH. Your property partially captures the target behaviour. "
|
| 144 |
+
f"Score: {score:.3f} β Reward: {reward_val:.3f}"
|
| 145 |
+
)
|
| 146 |
+
else:
|
| 147 |
+
msg = (
|
| 148 |
+
f"β LOW MATCH. Your property does not sufficiently match the target. "
|
| 149 |
+
f"Score: {score:.3f} β Reward: {reward_val:.3f}"
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
return msg, Reward(
|
| 153 |
+
value=reward_val,
|
| 154 |
+
reason=f"submit_property confidence={confidence} score={score:.3f}",
|
| 155 |
partial=False,
|
| 156 |
)
|
| 157 |
|
server/tasks/task2/grader.py
CHANGED
|
@@ -3,15 +3,17 @@ grader.py (Task 2 β Property Discovery)
|
|
| 3 |
-----------------------------------------
|
| 4 |
Deterministic scorer for natural-language property submissions.
|
| 5 |
One submission attempt per episode.
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
from typing import Tuple
|
| 11 |
from utils import SemanticMatcher
|
| 12 |
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
class Task2Grader:
|
| 17 |
"""
|
|
@@ -19,23 +21,19 @@ class Task2Grader:
|
|
| 19 |
|
| 20 |
Parameters
|
| 21 |
----------
|
| 22 |
-
function_name
|
| 23 |
-
property
|
| 24 |
"""
|
| 25 |
|
| 26 |
def __init__(self, function_name: str, property: str) -> None:
|
| 27 |
-
self.function_name
|
| 28 |
-
self.property
|
| 29 |
-
|
| 30 |
-
# ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
|
| 32 |
def grade(self, submitted: str) -> Tuple[float, str]:
|
| 33 |
-
"""Deterministic
|
| 34 |
if not submitted or not submitted.strip():
|
| 35 |
-
return 0.0, "no_match"
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
SemanticMatcherInstance.confidence()
|
| 41 |
-
)
|
|
|
|
| 3 |
-----------------------------------------
|
| 4 |
Deterministic scorer for natural-language property submissions.
|
| 5 |
One submission attempt per episode.
|
| 6 |
+
Grade range: 0.0 β 1.0 (matchscore output, already normalised).
|
| 7 |
"""
|
| 8 |
|
|
|
|
|
|
|
| 9 |
from typing import Tuple
|
| 10 |
from utils import SemanticMatcher
|
| 11 |
|
| 12 |
+
_SCORE_MIN = 0.001 # grades are strictly (0, 1)
|
| 13 |
+
_SCORE_MAX = 0.999
|
| 14 |
|
| 15 |
+
def _clamp(v: float) -> float:
|
| 16 |
+
return max(_SCORE_MIN, min(_SCORE_MAX, v))
|
| 17 |
|
| 18 |
class Task2Grader:
|
| 19 |
"""
|
|
|
|
| 21 |
|
| 22 |
Parameters
|
| 23 |
----------
|
| 24 |
+
function_name : name of the target function
|
| 25 |
+
property : the 'property' field from the target function's data
|
| 26 |
"""
|
| 27 |
|
| 28 |
def __init__(self, function_name: str, property: str) -> None:
|
| 29 |
+
self.function_name = function_name
|
| 30 |
+
self.property = property
|
|
|
|
|
|
|
| 31 |
|
| 32 |
def grade(self, submitted: str) -> Tuple[float, str]:
|
| 33 |
+
"""Deterministic grade strictly in (0, 1)."""
|
| 34 |
if not submitted or not submitted.strip():
|
| 35 |
+
return _clamp(0.0), "no_match" # β 0.001
|
| 36 |
|
| 37 |
+
matcher = SemanticMatcher()
|
| 38 |
+
score = matcher.matchscore(self.property, submitted) # already clamped by SemanticMatcher
|
| 39 |
+
return _clamp(score), matcher.confidence()
|
|
|
|
|
|
server/tasks/task3/actions.py
CHANGED
|
@@ -126,44 +126,52 @@ def get_property_specification(ctx: Any, qkey: str, params: Dict) -> Tuple[str,
|
|
| 126 |
|
| 127 |
|
| 128 |
def submit_function(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
|
| 129 |
-
"""Handle SUBMIT_FUNCTION action.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
if ctx._submitted:
|
| 131 |
return (
|
| 132 |
"β You have already submitted for this episode. "
|
| 133 |
"Only ONE submission is allowed.",
|
| 134 |
-
Reward(value=
|
| 135 |
)
|
|
|
|
| 136 |
fn_name = params.get("function_name", "").strip()
|
|
|
|
| 137 |
if not fn_name:
|
| 138 |
return (
|
| 139 |
"submit_function requires 'function_name' in params.",
|
| 140 |
-
Reward(value=
|
| 141 |
)
|
| 142 |
-
|
| 143 |
ctx._submitted = True
|
| 144 |
-
ctx._done
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
| 148 |
if score >= 0.9:
|
| 149 |
msg = (
|
| 150 |
f"β
CORRECT! '{fn_name}' is the function that violates the property. "
|
| 151 |
-
f"Score: 1.0 β Reward:
|
| 152 |
)
|
| 153 |
elif score >= 0.2:
|
| 154 |
msg = (
|
| 155 |
-
f"π‘ PARTIAL. '{fn_name}' is
|
| 156 |
f"closely related but not the primary rule-breaker. "
|
| 157 |
-
f"Score: 0.3 β Reward:
|
| 158 |
-
f"Correct answer: '{correct['target_function']}'."
|
| 159 |
)
|
| 160 |
else:
|
| 161 |
msg = (
|
| 162 |
f"β INCORRECT. '{fn_name}' does not violate the property. "
|
| 163 |
-
f"Score: 0.0 β Reward: {reward_val:.
|
| 164 |
-
f"Correct answer: '{correct['target_function']}'."
|
| 165 |
)
|
| 166 |
-
|
| 167 |
return msg, Reward(
|
| 168 |
value=reward_val,
|
| 169 |
reason=f"submit_function score={score:.1f}",
|
|
|
|
| 126 |
|
| 127 |
|
| 128 |
def submit_function(ctx: Any, qkey: str, params: Dict) -> Tuple[str, Reward]:
|
| 129 |
+
"""Handle SUBMIT_FUNCTION action for Task 3.
|
| 130 |
+
|
| 131 |
+
Expected params
|
| 132 |
+
---------------
|
| 133 |
+
function_name : str β name of the function that violates the given property
|
| 134 |
+
"""
|
| 135 |
if ctx._submitted:
|
| 136 |
return (
|
| 137 |
"β You have already submitted for this episode. "
|
| 138 |
"Only ONE submission is allowed.",
|
| 139 |
+
Reward(value=0.0, reason="Second submit_function attempt", partial=False),
|
| 140 |
)
|
| 141 |
+
|
| 142 |
fn_name = params.get("function_name", "").strip()
|
| 143 |
+
|
| 144 |
if not fn_name:
|
| 145 |
return (
|
| 146 |
"submit_function requires 'function_name' in params.",
|
| 147 |
+
Reward(value=0.0, reason="Malformed submission", partial=False),
|
| 148 |
)
|
| 149 |
+
|
| 150 |
ctx._submitted = True
|
| 151 |
+
ctx._done = True
|
| 152 |
+
|
| 153 |
+
score, reward_val = ctx._grader.grade_and_reward(fn_name) # reward_val in [0.0, 1.0]
|
| 154 |
+
correct = ctx._grader.get_canonical_answer()
|
| 155 |
+
|
| 156 |
if score >= 0.9:
|
| 157 |
msg = (
|
| 158 |
f"β
CORRECT! '{fn_name}' is the function that violates the property. "
|
| 159 |
+
f"Score: 1.0 β Reward: {reward_val:.3f}"
|
| 160 |
)
|
| 161 |
elif score >= 0.2:
|
| 162 |
msg = (
|
| 163 |
+
f"π‘ PARTIAL. '{fn_name}' is an internal subfunction of the target β "
|
| 164 |
f"closely related but not the primary rule-breaker. "
|
| 165 |
+
f"Score: 0.3 β Reward: {reward_val:.3f}. "
|
| 166 |
+
f"Correct answer: '{correct['target_function']['name']}'."
|
| 167 |
)
|
| 168 |
else:
|
| 169 |
msg = (
|
| 170 |
f"β INCORRECT. '{fn_name}' does not violate the property. "
|
| 171 |
+
f"Score: 0.0 β Reward: {reward_val:.3f}. "
|
| 172 |
+
f"Correct answer: '{correct['target_function']['name']}'."
|
| 173 |
)
|
| 174 |
+
|
| 175 |
return msg, Reward(
|
| 176 |
value=reward_val,
|
| 177 |
reason=f"submit_function score={score:.1f}",
|
server/tasks/task3/grader.py
CHANGED
|
@@ -3,23 +3,29 @@ grader.py (Task 3 β Rule Checker)
|
|
| 3 |
------------------------------------
|
| 4 |
Deterministic grader for function-identification submissions.
|
| 5 |
|
| 6 |
-
|
| 7 |
βββββββββββ
|
| 8 |
1.0 β submitted function is the exact target (case-insensitive)
|
| 9 |
0.3 β submitted function is a direct internal subfunction of the target
|
| 10 |
-
(a contract-internal function called by the target in the call graph)
|
| 11 |
0.0 β anything else
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
score 0.0 β -1.5
|
| 17 |
"""
|
| 18 |
|
| 19 |
-
from __future__ import annotations
|
| 20 |
import json
|
| 21 |
from typing import Dict, Any
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
class Task3Grader:
|
| 25 |
"""
|
|
@@ -27,25 +33,26 @@ class Task3Grader:
|
|
| 27 |
|
| 28 |
Parameters
|
| 29 |
----------
|
| 30 |
-
target_function
|
| 31 |
-
|
| 32 |
-
(direct callees of the target that are contract functions)
|
| 33 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
SCORE_CORRECT = 1.0
|
| 36 |
-
SCORE_PARTIAL = 0.3
|
| 37 |
-
SCORE_WRONG = 0.0
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
def __init__(self, target_function: Dict[str, Any], property_specification: Dict | str) -> None:
|
| 44 |
-
self.target_function
|
| 45 |
self.property_specification = property_specification
|
| 46 |
|
| 47 |
def grade(self, submitted_function: str) -> float:
|
| 48 |
-
"""Returns deterministic
|
| 49 |
norm = submitted_function.strip().lower()
|
| 50 |
if norm == self.target_function["name"].strip().lower():
|
| 51 |
return self.SCORE_CORRECT
|
|
@@ -54,22 +61,29 @@ class Task3Grader:
|
|
| 54 |
return self.SCORE_WRONG
|
| 55 |
|
| 56 |
def reward_for_score(self, score: float) -> float:
|
| 57 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
if score >= 0.9:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
|
| 64 |
def grade_and_reward(self, submitted_function: str):
|
| 65 |
-
"""Convenience: returns (
|
| 66 |
score = self.grade(submitted_function)
|
| 67 |
return score, self.reward_for_score(score)
|
| 68 |
|
| 69 |
def get_canonical_answer(self) -> Dict[str, Dict | str]:
|
| 70 |
"""For debugging / logging only β do not expose to the agent."""
|
| 71 |
return {
|
| 72 |
-
"target_function":
|
| 73 |
-
"property_specification": json.dumps(self.property_specification)
|
| 74 |
if isinstance(self.property_specification, dict) else self.property_specification,
|
| 75 |
-
}
|
|
|
|
| 3 |
------------------------------------
|
| 4 |
Deterministic grader for function-identification submissions.
|
| 5 |
|
| 6 |
+
Grade table
|
| 7 |
βββββββββββ
|
| 8 |
1.0 β submitted function is the exact target (case-insensitive)
|
| 9 |
0.3 β submitted function is a direct internal subfunction of the target
|
|
|
|
| 10 |
0.0 β anything else
|
| 11 |
|
| 12 |
+
reward_for_score() normalises the raw RL reward to [0.0, 1.0]
|
| 13 |
+
using the fixed reward bounds [MIN_REWARD=-1.5, MAX_REWARD=5.0]:
|
| 14 |
+
normalised = (raw + 1.5) / 6.5
|
|
|
|
| 15 |
"""
|
| 16 |
|
|
|
|
| 17 |
import json
|
| 18 |
from typing import Dict, Any
|
| 19 |
|
| 20 |
+
_T3_MIN_REWARD = -1.5
|
| 21 |
+
_T3_MAX_REWARD = 5.0
|
| 22 |
+
_T3_REWARD_RANGE = _T3_MAX_REWARD - _T3_MIN_REWARD # 6.5
|
| 23 |
+
|
| 24 |
+
_SCORE_MIN = 0.001 # grades are strictly (0, 1
|
| 25 |
+
_SCORE_MAX = 0.999
|
| 26 |
+
|
| 27 |
+
def _clamp(v: float) -> float:
|
| 28 |
+
return max(_SCORE_MIN, min(_SCORE_MAX, v))
|
| 29 |
|
| 30 |
class Task3Grader:
|
| 31 |
"""
|
|
|
|
| 33 |
|
| 34 |
Parameters
|
| 35 |
----------
|
| 36 |
+
target_function : dict with at least 'name' and 'code' keys
|
| 37 |
+
property_specification : the property the target function violates
|
|
|
|
| 38 |
"""
|
| 39 |
+
|
| 40 |
+
# Raw reward bounds β used only for normalisation
|
| 41 |
+
_MIN_REWARD = -1.5
|
| 42 |
+
_MAX_REWARD = 5.0
|
| 43 |
+
_REWARD_RANGE = _MAX_REWARD - _MIN_REWARD # 6.5
|
| 44 |
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
SCORE_CORRECT = _clamp(1.0) # 0.999
|
| 47 |
+
SCORE_PARTIAL = _clamp(0.3) # 0.300 (already inside (0,1))
|
| 48 |
+
SCORE_WRONG = _clamp(0.0) # 0.001
|
| 49 |
|
| 50 |
def __init__(self, target_function: Dict[str, Any], property_specification: Dict | str) -> None:
|
| 51 |
+
self.target_function = target_function
|
| 52 |
self.property_specification = property_specification
|
| 53 |
|
| 54 |
def grade(self, submitted_function: str) -> float:
|
| 55 |
+
"""Returns deterministic grade strictly in (0, 1)."""
|
| 56 |
norm = submitted_function.strip().lower()
|
| 57 |
if norm == self.target_function["name"].strip().lower():
|
| 58 |
return self.SCORE_CORRECT
|
|
|
|
| 61 |
return self.SCORE_WRONG
|
| 62 |
|
| 63 |
def reward_for_score(self, score: float) -> float:
|
| 64 |
+
"""
|
| 65 |
+
Maps grade score β normalised reward strictly in (0, 1).
|
| 66 |
+
|
| 67 |
+
Raw rewards: correct=+5.0, partial=+1.5, wrong=-1.5
|
| 68 |
+
Normalised: (raw + 1.5) / 6.5 then clamped to (0.001, 0.999)
|
| 69 |
+
"""
|
| 70 |
if score >= 0.9:
|
| 71 |
+
raw = 5.0
|
| 72 |
+
elif score >= 0.2:
|
| 73 |
+
raw = 1.5
|
| 74 |
+
else:
|
| 75 |
+
raw = -1.5
|
| 76 |
+
return _clamp((raw - _T3_MIN_REWARD) / _T3_REWARD_RANGE)
|
| 77 |
|
| 78 |
def grade_and_reward(self, submitted_function: str):
|
| 79 |
+
"""Convenience: returns (grade, normalised_reward), both strictly in (0, 1)."""
|
| 80 |
score = self.grade(submitted_function)
|
| 81 |
return score, self.reward_for_score(score)
|
| 82 |
|
| 83 |
def get_canonical_answer(self) -> Dict[str, Dict | str]:
|
| 84 |
"""For debugging / logging only β do not expose to the agent."""
|
| 85 |
return {
|
| 86 |
+
"target_function": self.target_function,
|
| 87 |
+
"property_specification": json.dumps(self.property_specification)
|
| 88 |
if isinstance(self.property_specification, dict) else self.property_specification,
|
| 89 |
+
}
|
utils/prompts.py
CHANGED
|
@@ -2,6 +2,10 @@ T1_SYSTEM = """You are an expert Solidity smart contract security auditor.
|
|
| 2 |
|
| 3 |
Given a contract, identify the ONE vulnerable function and its vulnerability type.
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
## Actions (choose ONE per turn, respond with JSON only):
|
| 6 |
{"action": "list_functions", "params": {}}
|
| 7 |
{"action": "get_function_code", "params": {"function_name": "<name>"}}
|
|
@@ -35,6 +39,10 @@ You will be shown a specific Solidity function. Your task is to write a precise
|
|
| 35 |
natural-language property (invariant / postcondition) that describes what the
|
| 36 |
function guarantees when it succeeds.
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
A good property covers:
|
| 39 |
- What state changes (balances, counters, flags)
|
| 40 |
- What assets are transferred (ETH, tokens, NFTs)
|
|
@@ -72,6 +80,10 @@ T3_SYSTEM = """You are a smart contract security auditor checking rule complianc
|
|
| 72 |
You are given a Solidity contract and a property (rule) in natural English.
|
| 73 |
Your task is to find the ONE function that violates this property.
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
## Actions (respond with JSON only, ONE action per turn):
|
| 76 |
{"action": "list_functions", "params": {}}
|
| 77 |
{"action": "get_property_specification", "params": {}}
|
|
|
|
| 2 |
|
| 3 |
Given a contract, identify the ONE vulnerable function and its vulnerability type.
|
| 4 |
|
| 5 |
+
Negative reward is given for each information-gathering action, so be strategic.
|
| 6 |
+
Focus on high-signal actions like get_function_code and get_function_summary, and only inspect
|
| 7 |
+
state variables or call graphs if you have a strong suspicion.
|
| 8 |
+
|
| 9 |
## Actions (choose ONE per turn, respond with JSON only):
|
| 10 |
{"action": "list_functions", "params": {}}
|
| 11 |
{"action": "get_function_code", "params": {"function_name": "<name>"}}
|
|
|
|
| 39 |
natural-language property (invariant / postcondition) that describes what the
|
| 40 |
function guarantees when it succeeds.
|
| 41 |
|
| 42 |
+
Negative reward is given for each information-gathering action, so be strategic.
|
| 43 |
+
Focus on high-signal actions like get_function_code and get_function_summary, and only inspect
|
| 44 |
+
state variables or call graphs if you have a strong suspicion.
|
| 45 |
+
|
| 46 |
A good property covers:
|
| 47 |
- What state changes (balances, counters, flags)
|
| 48 |
- What assets are transferred (ETH, tokens, NFTs)
|
|
|
|
| 80 |
You are given a Solidity contract and a property (rule) in natural English.
|
| 81 |
Your task is to find the ONE function that violates this property.
|
| 82 |
|
| 83 |
+
Negative reward is given for each information-gathering action, so be strategic.
|
| 84 |
+
Focus on high-signal actions like get_function_code and get_function_summary, and only inspect
|
| 85 |
+
state variables or call graphs if you have a strong suspicion.
|
| 86 |
+
|
| 87 |
## Actions (respond with JSON only, ONE action per turn):
|
| 88 |
{"action": "list_functions", "params": {}}
|
| 89 |
{"action": "get_property_specification", "params": {}}
|
utils/semanticmatcher.py
CHANGED
|
@@ -143,6 +143,17 @@ def cosine_similarity(vec_a: np.ndarray, vec_b: np.ndarray) -> float:
|
|
| 143 |
return float(np.dot(vec_a, vec_b) / (norm_a * norm_b))
|
| 144 |
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
# ββ Core matcher ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 147 |
|
| 148 |
class SemanticMatcher:
|
|
@@ -201,7 +212,7 @@ class SemanticMatcher:
|
|
| 201 |
# Fast-path: normalized exact match
|
| 202 |
if normalize(text_a) == normalize(text_b):
|
| 203 |
self.confidence_level = "strong"
|
| 204 |
-
return
|
| 205 |
|
| 206 |
tokens_a = tokenize_and_lemmatize(text_a)
|
| 207 |
tokens_b = tokenize_and_lemmatize(text_b)
|
|
@@ -219,13 +230,13 @@ class SemanticMatcher:
|
|
| 219 |
self.confidence_level = "moderate"
|
| 220 |
else:
|
| 221 |
self.confidence_level = "no_match"
|
| 222 |
-
return score
|
| 223 |
-
|
| 224 |
def match(self, text_a: str, text_b: str) -> bool:
|
| 225 |
"""Return True if the two texts are considered a match based on the score."""
|
| 226 |
score = self.matchscore(text_a, text_b)
|
| 227 |
return score >= self.match_threshold
|
| 228 |
-
|
| 229 |
def confidence(self) -> str:
|
| 230 |
-
"""Return 'strong' if score β₯ strong_threshold, else '
|
| 231 |
return self.confidence_level
|
|
|
|
| 143 |
return float(np.dot(vec_a, vec_b) / (norm_a * norm_b))
|
| 144 |
|
| 145 |
|
| 146 |
+
# ββ Score clamping βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 147 |
+
|
| 148 |
+
_SCORE_MIN = 0.001 # scores are strictly (0, 1) β never touch 0 or 1
|
| 149 |
+
_SCORE_MAX = 0.999
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _clamp(score: float) -> float:
|
| 153 |
+
"""Clamp score to the open interval (0, 1): [_SCORE_MIN, _SCORE_MAX]."""
|
| 154 |
+
return max(_SCORE_MIN, min(_SCORE_MAX, score))
|
| 155 |
+
|
| 156 |
+
|
| 157 |
# ββ Core matcher ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
|
| 159 |
class SemanticMatcher:
|
|
|
|
| 212 |
# Fast-path: normalized exact match
|
| 213 |
if normalize(text_a) == normalize(text_b):
|
| 214 |
self.confidence_level = "strong"
|
| 215 |
+
return _clamp(1.0) # β 0.999 (strictly less than 1)
|
| 216 |
|
| 217 |
tokens_a = tokenize_and_lemmatize(text_a)
|
| 218 |
tokens_b = tokenize_and_lemmatize(text_b)
|
|
|
|
| 230 |
self.confidence_level = "moderate"
|
| 231 |
else:
|
| 232 |
self.confidence_level = "no_match"
|
| 233 |
+
return _clamp(score) # strictly in (0, 1)
|
| 234 |
+
|
| 235 |
def match(self, text_a: str, text_b: str) -> bool:
|
| 236 |
"""Return True if the two texts are considered a match based on the score."""
|
| 237 |
score = self.matchscore(text_a, text_b)
|
| 238 |
return score >= self.match_threshold
|
| 239 |
+
|
| 240 |
def confidence(self) -> str:
|
| 241 |
+
"""Return 'strong' if score β₯ strong_threshold, else 'moderate' or 'no_match'."""
|
| 242 |
return self.confidence_level
|