Skip to content

[WIP]patcheval env#16

Open
sys555 wants to merge 2 commits into
v2from
feat/patchenv
Open

[WIP]patcheval env#16
sys555 wants to merge 2 commits into
v2from
feat/patchenv

Conversation

@sys555

@sys555 sys555 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator
  1. Eval in Patcheval;
  2. RL in Patcheval;

Summary by CodeRabbit

  • New Features

    • Added PatchEval support for automated vulnerability-fix evaluation, including dataset configurations, secure patch validation, scoring, and training/evaluation launch scripts.
    • Added support for configuring runner-script installation paths per Docker environment.
    • Added several CVE evaluation scenarios covering repository, redirect, command-injection, and path-traversal vulnerabilities.
  • Bug Fixes

    • Improved validation and error reporting for missing runner scripts and invalid container destinations.
    • Prevented conflicting runner-entrypoint mounts when automatic script installation is enabled.
  • Documentation

    • Added comprehensive PatchEval setup, workflow, and execution guidance.

@sys555 sys555 changed the title [WIP]Feat/patchenv [WIP]patcheval env Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The pull request adds a PatchEval environment with CVE datasets, Docker agent configuration, an LLM patch runner, rule-based scoring, evaluation and training launch scripts, and documentation. It also updates Docker runner installation paths and Geo3K environment settings.

PatchEval execution flow

Layer / File(s) Summary
PatchEval data and evaluation contracts
env/patcheval/PatchEval, env/patcheval/datasets/*, env/patcheval/patcheval_config.yaml, env/patcheval/patcheval_start.yaml, env/patcheval/rule_evaluator.py, evaluator/configs/patcheval_rule_eval.yaml
CVE records, environment aliases, agent setup, evaluation configuration, and score normalization are added.
Patch generation and validation runner
env/patcheval/runner.py
The runner builds vulnerability context, requests unified diffs from the gateway, filters patch paths, applies patches, runs validation commands, and emits JSON results.
Docker runner installation wiring
manager/simulation_config.py, clusters/docker_clusters.py
Docker configuration now validates runner paths, avoids conflicting mounts, and installs the runner at the configured container destination.
PatchEval launch and training orchestration
rl/examples/patcheval/env.sh, rl/examples/patcheval/run_*.sh, rl/examples/patcheval/README.md, rl/examples/patcheval/reward_1_fix_patches.txt
Environment defaults, buffer and gateway startup, Ray/SLIME submission, PatchEval documentation, and reward trajectories are added.

Environment configuration updates

Layer / File(s) Summary
Geo3K environment configuration
env/geo3k/geo3k_config.yaml, rl/examples/geo3k_vl/env.sh, rl/examples/geo3k_vl/run_slime_generator.sh
Geo3K uses shared-storage paths, a shorter training schedule, a node gateway address, and offline W&B mode.
Repository ignore rule
.gitignore
The local wheels/ directory is ignored.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Launcher
  participant PatchEvalRunner
  participant Gateway
  participant TargetContainer
  Launcher->>PatchEvalRunner: Send evaluation request
  PatchEvalRunner->>TargetContainer: Read referenced source context
  PatchEvalRunner->>Gateway: Request patch from chat completions
  Gateway-->>PatchEvalRunner: Return unified diff
  PatchEvalRunner->>TargetContainer: Apply patch and run validation
  TargetContainer-->>PatchEvalRunner: Return validation metrics
  PatchEvalRunner-->>Launcher: Emit structured result
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the PatchEval changes but too vague to describe the main update clearly. Rename it to something specific like "Add PatchEval evaluation and training environment support".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/patchenv
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/patchenv

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (7)
rl/examples/geo3k_vl/env.sh (2)

71-72: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the gateway host configurable.

The fixed 100.99.221.45 address will break when this script runs on another training node. Preserve an override, for example ${AIEVOBOX_GATEWAY_HOST:-100.99.221.45}, and verify that the gateway binds to an interface reachable from Docker.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl/examples/geo3k_vl/env.sh` around lines 71 - 72, Update the
AIEVOBOX_GATEWAY_HOST assignment in env.sh to allow an externally provided value
while retaining 100.99.221.45 as the default, and ensure the gateway binding
uses an interface reachable from Docker.

4-4: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the repository root overrideable.

This hardcodes all derived DB, agent, and evaluation paths to /mnt/shared-storage-user/leishanzhe/...; launches from another node or account will fail if that mount differs. Use an environment override with this value as the default, and validate the root exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl/examples/geo3k_vl/env.sh` at line 4, Update the AIEVOBOX_ROOT assignment
in env.sh to honor an existing environment value while defaulting to the current
repository path, then validate that the resolved root exists before continuing.
Keep all derived paths based on the validated AIEVOBOX_ROOT value.
rl/examples/patcheval/env.sh (1)

35-36: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No validation that SLIME_GLOBAL_BATCH_SIZE is divisible by RL_GROUP_SIZE.

SLIME_ROLLOUT_BATCH_SIZE=$((SLIME_GLOBAL_BATCH_SIZE / RL_GROUP_SIZE)) (Line 36) silently truncates on integer division if the two values don't divide evenly, which would quietly change training batch semantics when either override is customized. A guard (e.g. fail if remainder != 0) would surface misconfiguration early instead of silently truncating.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl/examples/patcheval/env.sh` around lines 35 - 36, Validate that
SLIME_GLOBAL_BATCH_SIZE is evenly divisible by RL_GROUP_SIZE before calculating
SLIME_ROLLOUT_BATCH_SIZE. Add a shell guard that detects a nonzero remainder and
exits with a clear configuration error; only perform the existing integer
division when validation succeeds.
env/patcheval/runner.py (4)

52-53: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Policy-rejected patches and truly unextracted patches share the same failure_stage, losing training signal.

When _patch_is_allowed rejects a diff (Line 52-53), patch is cleared before _evaluate_patch runs, so metrics.patch_extracted becomes False and failure_stage is reported as "patch_extraction" (Line 112) — identical to the case where the model produced no diff at all. These are different failure modes (policy violation vs. no output) and conflating them makes it harder to diagnose/shape reward during RL training.

Consider a distinct stage, e.g. "patch_rejected", when the diff was extracted but blocked by _patch_is_allowed.

Also applies to: 104-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env/patcheval/runner.py` around lines 52 - 53, Update the patch handling in
the runner flow around _patch_is_allowed and _evaluate_patch to preserve whether
a non-empty diff was extracted before clearing policy-rejected patches. Report a
distinct failure_stage such as "patch_rejected" for disallowed diffs, while
retaining "patch_extraction" for cases where no diff was produced.

41-50: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No retry/backoff around the single gateway call.

_call_gateway makes one urlopen attempt with no retry; any transient network hiccup during training will surface as an infrastructure_error episode (Line 84-101) rather than being retried, reducing rollout throughput. Worth a small retry-with-backoff wrapper given this runs inside an RL training loop where transient failures are expected at scale.

Also applies to: 248-264

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env/patcheval/runner.py` around lines 41 - 50, Wrap the gateway invocation in
the runner flow around _call_gateway with a small bounded retry mechanism and
backoff for transient network failures. Preserve the existing request
parameters, timeout, and infrastructure-error behavior after retries are
exhausted; apply the same retry handling to the additional _call_gateway
invocation noted in the comment.

1-346: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

No test coverage for the reward-determining helpers.

_extract_patch, _patch_is_allowed, and _evaluate_patch directly determine the RL reward signal but ship with no unit tests in this PR. Given how central these are to training correctness, some coverage (e.g. malformed diffs, forbidden-path patches, PoC/unit-test transitions) would help catch regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env/patcheval/runner.py` around lines 1 - 346, Add unit tests covering the
reward-determining helpers _extract_patch, _patch_is_allowed, and
_evaluate_patch. Include malformed or fenced diff extraction, forbidden-path
rejection, and _evaluate_patch transitions for patch application, PoC, and
unit-test outcomes; place tests in the existing test suite without modifying
validation scripts.

226-245: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Reject .. path components in patch targets in env/patcheval/runner.py:226-245. git apply --check already blocks path escapes, so this is only defense in depth, but adding a ".." in Path(path).parts guard would keep malformed targets from reaching /workspace/fix-run.sh later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env/patcheval/runner.py` around lines 226 - 245, Update _patch_is_allowed to
reject any patch target whose Path(path).parts contains the parent-directory
component "..". Preserve the existing forbidden-name, test-path, and valid-path
handling while adding this defense-in-depth guard before allowing the patch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@clusters/docker_clusters.py`:
- Around line 530-532: The runner installation fallback is evaluated before the
environment-specific runner path is resolved, causing custom paths with omitted
install_runner_script to skip copying. Update the relevant installation flow
around runner_container_path and the fallback check near the runner-copy helper
so the per-environment path is resolved first and used when deciding whether
installation is required; preserve explicit install_runner_script decisions.

In `@env/patcheval/PatchEval`:
- Line 1: Add the missing .gitmodules entry for the env/patcheval/PatchEval
gitlink, including its submodule path and repository URL, so fresh clones and
git submodule status initialize it correctly; alternatively remove the gitlink
and track the directory as regular files if it is not intended to remain a
submodule.

In `@env/patcheval/patcheval_config.yaml`:
- Line 3: Replace the :latest tag in each env_image entry, including the listed
repeated entries, with the tested immutable release tag or image digest used by
PatchEval. Ensure every configured image reference remains pinned to a fixed
version for reproducible evaluations.

In `@env/patcheval/runner.py`:
- Around line 22-101: Move _read_request() and session_id extraction inside
main()’s safety-net try block so parsing failures also produce a structured
failed result. Initialize session_id before the try with a safe fallback value,
then replace it when parsing succeeds, ensuring the exception handler can always
include session_id without triggering another error. Preserve the existing
failed-result fields and return behavior.

In `@manager/simulation_config.py`:
- Around line 468-476: Update the mount validation around normalized_mounts and
runner_mount so that, when install_runner_script is enabled, any normalized
mount targeting runner_entrypoint.target is rejected before Docker receives it;
preserve the existing _append_mount_if_missing behavior for non-conflicting
mounts.

In `@rl/examples/patcheval/env.sh`:
- Line 3: Update the AIEVOBOX_ROOT assignment to use the same ${VAR:-default}
pattern as the other environment variables, preserving an existing
caller-provided value while supplying a portable repository-relative or
documented default instead of the hardcoded personal path.

In `@rl/examples/patcheval/run_external_eval.sh`:
- Line 8: Remove the hardcoded external IP fallback from PATCH_EVAL_API_BASE in
run_external_eval.sh and require callers to set this endpoint explicitly,
matching the existing PATCH_EVAL_API_KEY validation behavior; preserve the
variable’s use by the evaluation commands once validated.

---

Nitpick comments:
In `@env/patcheval/runner.py`:
- Around line 52-53: Update the patch handling in the runner flow around
_patch_is_allowed and _evaluate_patch to preserve whether a non-empty diff was
extracted before clearing policy-rejected patches. Report a distinct
failure_stage such as "patch_rejected" for disallowed diffs, while retaining
"patch_extraction" for cases where no diff was produced.
- Around line 41-50: Wrap the gateway invocation in the runner flow around
_call_gateway with a small bounded retry mechanism and backoff for transient
network failures. Preserve the existing request parameters, timeout, and
infrastructure-error behavior after retries are exhausted; apply the same retry
handling to the additional _call_gateway invocation noted in the comment.
- Around line 1-346: Add unit tests covering the reward-determining helpers
_extract_patch, _patch_is_allowed, and _evaluate_patch. Include malformed or
fenced diff extraction, forbidden-path rejection, and _evaluate_patch
transitions for patch application, PoC, and unit-test outcomes; place tests in
the existing test suite without modifying validation scripts.
- Around line 226-245: Update _patch_is_allowed to reject any patch target whose
Path(path).parts contains the parent-directory component "..". Preserve the
existing forbidden-name, test-path, and valid-path handling while adding this
defense-in-depth guard before allowing the patch.

In `@rl/examples/geo3k_vl/env.sh`:
- Around line 71-72: Update the AIEVOBOX_GATEWAY_HOST assignment in env.sh to
allow an externally provided value while retaining 100.99.221.45 as the default,
and ensure the gateway binding uses an interface reachable from Docker.
- Line 4: Update the AIEVOBOX_ROOT assignment in env.sh to honor an existing
environment value while defaulting to the current repository path, then validate
that the resolved root exists before continuing. Keep all derived paths based on
the validated AIEVOBOX_ROOT value.

In `@rl/examples/patcheval/env.sh`:
- Around line 35-36: Validate that SLIME_GLOBAL_BATCH_SIZE is evenly divisible
by RL_GROUP_SIZE before calculating SLIME_ROLLOUT_BATCH_SIZE. Add a shell guard
that detects a nonzero remainder and exits with a clear configuration error;
only perform the existing integer division when validation succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 794b67ce-37cf-472f-ab8e-98a6348bc7c7

📥 Commits

Reviewing files that changed from the base of the PR and between f96512c and cf578ed.

📒 Files selected for processing (25)
  • .gitignore
  • =10.1
  • clusters/docker_clusters.py
  • env/geo3k/geo3k_config.yaml
  • env/patcheval/PatchEval
  • env/patcheval/datasets/cve-2020-25459.jsonl
  • env/patcheval/datasets/cve-2020-26215.jsonl
  • env/patcheval/datasets/cve-2021-23376.jsonl
  • env/patcheval/datasets/cve-2022-1986.jsonl
  • env/patcheval/datasets/cve-2024-25620.jsonl
  • env/patcheval/patcheval_config.yaml
  • env/patcheval/patcheval_start.yaml
  • env/patcheval/rule_evaluator.py
  • env/patcheval/runner.py
  • evaluator/configs/patcheval_rule_eval.yaml
  • manager/simulation_config.py
  • rl/examples/geo3k_vl/=10.1
  • rl/examples/geo3k_vl/env.sh
  • rl/examples/geo3k_vl/run_slime_generator.sh
  • rl/examples/patcheval/README.md
  • rl/examples/patcheval/env.sh
  • rl/examples/patcheval/reward_1_fix_patches.txt
  • rl/examples/patcheval/run_buffer_server.sh
  • rl/examples/patcheval/run_external_eval.sh
  • rl/examples/patcheval/run_slime_generator.sh

Comment on lines +530 to +532
runner_container_path = str(
docker_cfg.get("runner_container_path", self._runner_container_path) or self._runner_container_path
).strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the per-environment path before deciding whether to install.

The fallback at Lines 521-524 checks self._runner_container_path before this custom path is resolved. For a custom runner path with install_runner_script omitted, Lines 576-581 enable installation, but this helper returns early and never copies the runner. Resolve runner_container_path before the fallback check, or pass the resolved install decision into this method.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clusters/docker_clusters.py` around lines 530 - 532, The runner installation
fallback is evaluated before the environment-specific runner path is resolved,
causing custom paths with omitted install_runner_script to skip copying. Update
the relevant installation flow around runner_container_path and the fallback
check near the runner-copy helper so the per-environment path is resolved first
and used when deciding whether installation is required; preserve explicit
install_runner_script decisions.

Comment thread env/patcheval/PatchEval
@@ -0,0 +1 @@
Subproject commit ae9b3a7233e9e2fe86dcba8baba24beff9bcbba8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git config -f .gitmodules --get-regexp '^submodule\..*\.path$'
git submodule status -- env/patcheval/PatchEval
git ls-tree HEAD env/patcheval/PatchEval

if [ -d env/patcheval/PatchEval/.git ] || [ -f env/patcheval/PatchEval/.git ]; then
  git -C env/patcheval/PatchEval cat-file -e ae9b3a7233e9e2fe86dcba8baba24beff9bcbba8^{commit}
fi

Repository: AI45Lab/SAfactory

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== top-level files ==\n'
git ls-files | sed -n '1,200p'

printf '\n== .gitmodules ==\n'
if [ -f .gitmodules ]; then
  cat -n .gitmodules
else
  echo '(missing)'
fi

printf '\n== path object ==\n'
git ls-tree HEAD env/patcheval/PatchEval || true

printf '\n== submodule status ==\n'
git submodule status -- env/patcheval/PatchEval || true

printf '\n== file type ==\n'
if [ -e env/patcheval/PatchEval ]; then
  stat -c '%F %n' env/patcheval/PatchEval
else
  echo 'path missing'
fi

Repository: AI45Lab/SAfactory

Length of output: 6012


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import os, subprocess, sys

def run(cmd):
    p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    print(f"$ {' '.join(cmd)}")
    print(p.stdout)
    print(f"exit={p.returncode}\n")

run(["git", "rev-parse", "--is-inside-work-tree"])
run(["git", "ls-tree", "HEAD", "env/patcheval/PatchEval"])
run(["git", "submodule", "status", "--", "env/patcheval/PatchEval"])
print("exists", os.path.exists("env/patcheval/PatchEval"))
print("isdir", os.path.isdir("env/patcheval/PatchEval"))
print("isfile", os.path.isfile("env/patcheval/PatchEval"))
PY

Repository: AI45Lab/SAfactory

Length of output: 519


🏁 Script executed:

#!/bin/bash
set -u

echo '== .gitmodules =='
if [ -f .gitmodules ]; then
  cat -n .gitmodules
else
  echo '(missing)'
fi

echo '== git ls-tree =='
git ls-tree HEAD env/patcheval/PatchEval || true

echo '== git submodule status =='
git submodule status -- env/patcheval/PatchEval || true

echo '== path info =='
if [ -e env/patcheval/PatchEval ]; then
  stat -c '%F %n' env/patcheval/PatchEval
else
  echo 'missing'
fi

Repository: AI45Lab/SAfactory

Length of output: 450


Add the missing submodule mapping for env/patcheval/PatchEval.
env/patcheval/PatchEval is tracked as a gitlink, but .gitmodules has no entry for it, so git submodule status fails and fresh clones or CI can’t initialize the path. Add the submodule registration or stop tracking it as a submodule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env/patcheval/PatchEval` at line 1, Add the missing .gitmodules entry for the
env/patcheval/PatchEval gitlink, including its submodule path and repository
URL, so fresh clones and git submodule status initialize it correctly;
alternatively remove the gitlink and track the directory as regular files if it
is not intended to remain a submodule.

@@ -0,0 +1,40 @@
environments:
- env_name: patcheval_cve_2021_23376
env_image: ghcr.io/anonymous2578-data/cve-2021-23376:latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Pin PatchEval images to immutable versions.

Using :latest lets the image revision drift from the dataset’s fixed source locations and checks, making evaluation and training rewards non-reproducible. Pin each image to a tested release tag or digest.

Also applies to: 11-11, 19-19, 27-27, 35-35

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env/patcheval/patcheval_config.yaml` at line 3, Replace the :latest tag in
each env_image entry, including the listed repeated entries, with the tested
immutable release tag or image digest used by PatchEval. Ensure every configured
image reference remains pinned to a fixed version for reproducible evaluations.

Comment thread env/patcheval/runner.py
Comment on lines +22 to +101
def main() -> int:
started_at = time.perf_counter()
request = _read_request()
session_id = _required_text(request.get("session_id"), "session_id")

try:
env_params = request.get("env_params") if isinstance(request.get("env_params"), dict) else {}
dataset = env_params.get("dataset") if isinstance(env_params.get("dataset"), dict) else {}
cve_id = _required_text(dataset.get("cve_id") or env_params.get("cve_id"), "cve_id")
work_dir = Path(_required_text(dataset.get("work_dir") or env_params.get("work_dir"), "work_dir"))
problem = _required_text(dataset.get("problem_statement"), "problem_statement")
timeout_s = _float(request.get("agent_start_timeout_s"), DEFAULT_TIMEOUT_S)

if not work_dir.is_dir():
raise RuntimeError(f"PatchEval work directory does not exist: {work_dir}")

_remove_answer_leaks()
source_context = _collect_source_context(problem, work_dir)
prompt = _build_prompt(cve_id, problem, source_context)
response_text = _call_gateway(
base_url=_resolve_base_url(request, session_id),
model=_required_text(
request.get("model") or env_params.get("route_model") or os.environ.get("SAFACTORY_ROUTE_MODEL"),
"model",
),
prompt=prompt,
temperature=_float(request.get("temperature"), 1.0),
timeout_s=timeout_s,
)
patch = _extract_patch(response_text)
if patch and not _patch_is_allowed(patch):
patch = ""
result = _evaluate_patch(patch, work_dir, timeout_s)

_write_result(
{
"session_id": session_id,
"status": "succeeded",
"total_reward": result["score"],
"step_count": 1,
"terminated": True,
"truncated": False,
"error_text": None,
"metrics": {
"bench": "patcheval",
"cve_id": cve_id,
"score": result["score"],
"strict_success": result["strict_success"],
"patch_extracted": bool(patch),
"patch_applied": result["patch_applied"],
"poc_passed": result["poc_passed"],
"unit_test_present": result["unit_test_present"],
"unit_tests_passed": result["unit_tests_passed"],
"failure_stage": result["failure_stage"],
"patch": patch,
"poc_log": result["poc_log"],
"unit_test_log": result["unit_test_log"],
"duration_ms": round((time.perf_counter() - started_at) * 1000, 3),
},
}
)
return 0
except Exception as exc: # runtime must always emit a result object
_write_result(
{
"session_id": session_id,
"status": "failed",
"total_reward": 0.0,
"step_count": 0,
"terminated": True,
"truncated": isinstance(exc, subprocess.TimeoutExpired),
"error_text": str(exc),
"metrics": {
"bench": "patcheval",
"infrastructure_error": True,
"duration_ms": round((time.perf_counter() - started_at) * 1000, 3),
},
}
)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Request parsing runs outside the safety-net try, breaking the "always emit a result" contract.

_read_request() and the session_id extraction (lines 24-25) execute before the try block whose except is explicitly commented as "runtime must always emit a result object" (Line 84). If stdin/env JSON is missing/invalid, or session_id is absent, _required_text/_read_request raise RuntimeError outside the try, so the process crashes with an unhandled traceback instead of writing a structured failed result — exactly the failure mode this handler is supposed to prevent.

🐛 Proposed fix: move request parsing inside the try, with a safe session_id fallback
 def main() -> int:
     started_at = time.perf_counter()
-    request = _read_request()
-    session_id = _required_text(request.get("session_id"), "session_id")
+    session_id = "unknown"
 
     try:
+        request = _read_request()
+        session_id = _required_text(request.get("session_id"), "session_id")
         env_params = request.get("env_params") if isinstance(request.get("env_params"), dict) else {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def main() -> int:
started_at = time.perf_counter()
request = _read_request()
session_id = _required_text(request.get("session_id"), "session_id")
try:
env_params = request.get("env_params") if isinstance(request.get("env_params"), dict) else {}
dataset = env_params.get("dataset") if isinstance(env_params.get("dataset"), dict) else {}
cve_id = _required_text(dataset.get("cve_id") or env_params.get("cve_id"), "cve_id")
work_dir = Path(_required_text(dataset.get("work_dir") or env_params.get("work_dir"), "work_dir"))
problem = _required_text(dataset.get("problem_statement"), "problem_statement")
timeout_s = _float(request.get("agent_start_timeout_s"), DEFAULT_TIMEOUT_S)
if not work_dir.is_dir():
raise RuntimeError(f"PatchEval work directory does not exist: {work_dir}")
_remove_answer_leaks()
source_context = _collect_source_context(problem, work_dir)
prompt = _build_prompt(cve_id, problem, source_context)
response_text = _call_gateway(
base_url=_resolve_base_url(request, session_id),
model=_required_text(
request.get("model") or env_params.get("route_model") or os.environ.get("SAFACTORY_ROUTE_MODEL"),
"model",
),
prompt=prompt,
temperature=_float(request.get("temperature"), 1.0),
timeout_s=timeout_s,
)
patch = _extract_patch(response_text)
if patch and not _patch_is_allowed(patch):
patch = ""
result = _evaluate_patch(patch, work_dir, timeout_s)
_write_result(
{
"session_id": session_id,
"status": "succeeded",
"total_reward": result["score"],
"step_count": 1,
"terminated": True,
"truncated": False,
"error_text": None,
"metrics": {
"bench": "patcheval",
"cve_id": cve_id,
"score": result["score"],
"strict_success": result["strict_success"],
"patch_extracted": bool(patch),
"patch_applied": result["patch_applied"],
"poc_passed": result["poc_passed"],
"unit_test_present": result["unit_test_present"],
"unit_tests_passed": result["unit_tests_passed"],
"failure_stage": result["failure_stage"],
"patch": patch,
"poc_log": result["poc_log"],
"unit_test_log": result["unit_test_log"],
"duration_ms": round((time.perf_counter() - started_at) * 1000, 3),
},
}
)
return 0
except Exception as exc: # runtime must always emit a result object
_write_result(
{
"session_id": session_id,
"status": "failed",
"total_reward": 0.0,
"step_count": 0,
"terminated": True,
"truncated": isinstance(exc, subprocess.TimeoutExpired),
"error_text": str(exc),
"metrics": {
"bench": "patcheval",
"infrastructure_error": True,
"duration_ms": round((time.perf_counter() - started_at) * 1000, 3),
},
}
)
return 0
def main() -> int:
started_at = time.perf_counter()
session_id = "unknown"
try:
request = _read_request()
session_id = _required_text(request.get("session_id"), "session_id")
env_params = request.get("env_params") if isinstance(request.get("env_params"), dict) else {}
dataset = env_params.get("dataset") if isinstance(env_params.get("dataset"), dict) else {}
cve_id = _required_text(dataset.get("cve_id") or env_params.get("cve_id"), "cve_id")
work_dir = Path(_required_text(dataset.get("work_dir") or env_params.get("work_dir"), "work_dir"))
problem = _required_text(dataset.get("problem_statement"), "problem_statement")
timeout_s = _float(request.get("agent_start_timeout_s"), DEFAULT_TIMEOUT_S)
if not work_dir.is_dir():
raise RuntimeError(f"PatchEval work directory does not exist: {work_dir}")
_remove_answer_leaks()
source_context = _collect_source_context(problem, work_dir)
prompt = _build_prompt(cve_id, problem, source_context)
response_text = _call_gateway(
base_url=_resolve_base_url(request, session_id),
model=_required_text(
request.get("model") or env_params.get("route_model") or os.environ.get("SAFACTORY_ROUTE_MODEL"),
"model",
),
prompt=prompt,
temperature=_float(request.get("temperature"), 1.0),
timeout_s=timeout_s,
)
patch = _extract_patch(response_text)
if patch and not _patch_is_allowed(patch):
patch = ""
result = _evaluate_patch(patch, work_dir, timeout_s)
_write_result(
{
"session_id": session_id,
"status": "succeeded",
"total_reward": result["score"],
"step_count": 1,
"terminated": True,
"truncated": False,
"error_text": None,
"metrics": {
"bench": "patcheval",
"cve_id": cve_id,
"score": result["score"],
"strict_success": result["strict_success"],
"patch_extracted": bool(patch),
"patch_applied": result["patch_applied"],
"poc_passed": result["poc_passed"],
"unit_test_present": result["unit_test_present"],
"unit_tests_passed": result["unit_tests_passed"],
"failure_stage": result["failure_stage"],
"patch": patch,
"poc_log": result["poc_log"],
"unit_test_log": result["unit_test_log"],
"duration_ms": round((time.perf_counter() - started_at) * 1000, 3),
},
}
)
return 0
except Exception as exc: # runtime must always emit a result object
_write_result(
{
"session_id": session_id,
"status": "failed",
"total_reward": 0.0,
"step_count": 0,
"terminated": True,
"truncated": isinstance(exc, subprocess.TimeoutExpired),
"error_text": str(exc),
"metrics": {
"bench": "patcheval",
"infrastructure_error": True,
"duration_ms": round((time.perf_counter() - started_at) * 1000, 3),
},
}
)
return 0
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 84-84: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env/patcheval/runner.py` around lines 22 - 101, Move _read_request() and
session_id extraction inside main()’s safety-net try block so parsing failures
also produce a structured failed result. Initialize session_id before the try
with a safe fallback value, then replace it when parsing succeeds, ensuring the
exception handler can always include session_id without triggering another
error. Preserve the existing failed-result fields and return behavior.

Comment on lines +468 to +476
install_runner_script = bool(container.get("install_runner_script", False))
mounts = container.get("mounts", container.get("volumes", [])) or []
if isinstance(mounts, (str, dict)):
mounts = [mounts]
if not isinstance(mounts, list):
raise ValueError(f"container.mounts for {agent_name!r} must be a list in {cfg_path}")
normalized_mounts = [_normalize_mount(mount, cfg_path) for mount in mounts]
runner_mount = _mount_from_runner_entrypoint(runner_entrypoint)
if runner_mount:
if runner_mount and not install_runner_script:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject mounts that shadow the installed runner target.

When install_runner_script is enabled, Line 476 bypasses _append_mount_if_missing, so an existing mount at runner_entrypoint.target is never rejected. Validate that no normalized mount targets the runner destination before passing it to Docker.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@manager/simulation_config.py` around lines 468 - 476, Update the mount
validation around normalized_mounts and runner_mount so that, when
install_runner_script is enabled, any normalized mount targeting
runner_entrypoint.target is rejected before Docker receives it; preserve the
existing _append_mount_if_missing behavior for non-conflicting mounts.

@@ -0,0 +1,36 @@
#!/usr/bin/env bash

export AIEVOBOX_ROOT=/mnt/shared-storage-user/leishanzhe/repo/SAfactory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

AIEVOBOX_ROOT is hardcoded and not overridable, unlike every other variable in this file.

Every other variable in this file (Lines 12-26, 29-30) uses the ${VAR:-default} pattern so callers can override via environment. AIEVOBOX_ROOT (Line 3) is assigned unconditionally, so exporting it before sourcing env.sh is silently clobbered. It's also a personal, single-user absolute path (/mnt/shared-storage-user/leishanzhe/...) baked into a checked-in script, which breaks for any other engineer running this launch script as-is.

♻️ Proposed fix
-export AIEVOBOX_ROOT=/mnt/shared-storage-user/leishanzhe/repo/SAfactory
+export AIEVOBOX_ROOT=${AIEVOBOX_ROOT:-/mnt/shared-storage-user/leishanzhe/repo/SAfactory}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export AIEVOBOX_ROOT=/mnt/shared-storage-user/leishanzhe/repo/SAfactory
export AIEVOBOX_ROOT=${AIEVOBOX_ROOT:-/mnt/shared-storage-user/leishanzhe/repo/SAfactory}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl/examples/patcheval/env.sh` at line 3, Update the AIEVOBOX_ROOT assignment
to use the same ${VAR:-default} pattern as the other environment variables,
preserving an existing caller-provided value while supplying a portable
repository-relative or documented default instead of the hardcoded personal
path.

AIEVOBOX_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." &>/dev/null && pwd)"

PYTHON_BIN=${PYTHON_BIN:-python3}
PATCH_EVAL_API_BASE=${PATCH_EVAL_API_BASE:-http://35.220.164.252:3888/v1}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Hardcoded external IP address as default API endpoint.

PATCH_EVAL_API_BASE defaults to a raw external IP (http://35.220.164.252:3888/v1) baked into a checked-in script. Baking a specific external endpoint IP into source control is fragile (breaks silently if the IP changes) and exposes internal/partner infrastructure details in the repository. Consider requiring this to be explicitly set (like PATCH_EVAL_API_KEY already is at Line 17-21) rather than defaulting to a real address.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl/examples/patcheval/run_external_eval.sh` at line 8, Remove the hardcoded
external IP fallback from PATCH_EVAL_API_BASE in run_external_eval.sh and
require callers to set this endpoint explicitly, matching the existing
PATCH_EVAL_API_KEY validation behavior; preserve the variable’s use by the
evaluation commands once validated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant