Skip to content

[WIP]patcheval env#16

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

[WIP]patcheval env#16
sys555 wants to merge 4 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 support for loading Docker images from local archives, with optional cleanup after use.
    • Added configurable runner-script installation paths and validation.
    • Added a complete PatchEval evaluation and training workflow, including configuration generation, strict execution, gateway startup, and buffer/RL launch scripts.
    • Official PatchEval results now determine evaluation scores and artifacts.
    • Added Docker archive and cleanup command-line options.
  • Documentation

    • Expanded PatchEval workflow, configuration, evaluation, and training guidance.
  • Tests

    • Added coverage for strict prompt/response handling and official evaluation scoring.

@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

This PR adds Docker image archive loading and cleanup, per-environment runner installation, official PatchEval evaluation, strict patch execution, generated configurations, launch scripts, training orchestration, and related Geo3K runtime updates.

Changes

PatchEval execution flow

Layer / File(s) Summary
PatchEval configuration and generated task contracts
env/patcheval/PatchEval, manager/types.py, args.py, manager/simulation_config.py, env/patcheval/generate_full_config.py
Adds Docker archive settings, runner-path validation, PatchEval dataset validation, official-record filtering, archive checks, and generated environment/agent configurations.
Strict patch generation and validation
env/patcheval/strict_runner.py, tests/test_patcheval_strict_protocol.py
Adds the strict runner flow for prompt construction, gateway requests, official patch application, PoC/unit-test execution, structured results, and protocol tests.
Official evaluation result bridge
env/patcheval/rule_evaluator.py, tests/test_patcheval_rule_evaluator.py
Runs the official evaluator asynchronously and maps validation results to binary scores, normalized scores, artifacts, and failure metadata.
Docker archive and runner-path runtime
clusters/docker_clusters.py, env/patcheval/docker_archive_adapter/sitecustomize.py, requirements.txt
Loads missing images from tar archives, tracks and cleans loaded images, installs runner scripts at per-environment paths, and patches Docker SDK operations for archive loading and proxy propagation.
PatchEval launch and training orchestration
rl/examples/patcheval/*, rl/buffer_server.py
Adds environment defaults, gateway/evaluation startup, generated-config buffer startup, Ray/SLIME submission, W&B trajectory data, and operational documentation.
Geo3K environment updates
Layer / File(s) Summary
Geo3K runtime configuration
env/geo3k/geo3k_config.yaml, rl/examples/geo3k_vl/env.sh, rl/examples/geo3k_vl/run_slime_generator.sh
Updates the dataset path, shared workspace root, RL epoch count, gateway host, and W&B mode.
Local artifact ignore rules
.gitignore
Ignores the local wheels/ directory.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.82% 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 vague and generic, and it doesn't clearly describe the main PatchEval evaluation and RL environment changes. Rename it to a concise, specific summary such as "Add PatchEval environment support for evaluation and RL".
✅ 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.

Comment thread env/patcheval/patcheval_config.yaml Outdated
@@ -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 Outdated
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.

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
manager/simulation_config.py (1)

472-480: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject conflicting runner mounts when install_runner_script is enabled manager/simulation_config.py:479-503
_append_mount_if_missing is skipped in this branch, so an existing mount on runner_entrypoint.target still slips through. Run the same target-conflict check before setting runner_container_path.

🤖 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 472 - 480, Update the runner-mount
handling near _mount_from_runner_entrypoint and runner_container_path so that,
when install_runner_script is enabled, it still checks normalized_mounts for an
existing mount targeting runner_entrypoint.target and rejects conflicts before
setting runner_container_path. Preserve the existing _append_mount_if_missing
behavior for non-conflicting mounts.
♻️ Duplicate comments (1)
clusters/docker_clusters.py (1)

531-568: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Runner-install auto-detection still uses the backend default path, not the per-environment path.

_install_runner_script_if_needed derives its auto-detect fallback with self._runner_container_path in record.run_command (Line 538) before it resolves the per-environment runner_container_path at Lines 545-547. _start_container itself resolves the correct per-env path at Lines 687-692 for logging, but never passes that resolved value into _install_runner_script_if_needed. So for an agent config with a custom runner_container_path and install_runner_script omitted, record.run_command uses the custom path while the fallback check tests against the backend default — the substring test fails, install_runner evaluates falsy, and the method returns early without ever copying the runner script into the container.

This is the same gap flagged in a prior review on this file.

🛠️ Proposed fix
     async def _install_runner_script_if_needed(
         self,
         record: DockerContainerRecord,
         docker_cfg: Dict[str, Any],
     ) -> None:
+        runner_container_path = str(
+            docker_cfg.get("runner_container_path", self._runner_container_path) or self._runner_container_path
+        ).strip()
         install_runner = docker_cfg.get("install_runner_script")
         if install_runner is None:
-            install_runner = self._runner_container_path in record.run_command
+            install_runner = runner_container_path in record.run_command
         if not install_runner:
             return

         runner_host_path = self._resolve_runner_host_path(docker_cfg.get("runner_script_host_path"))
         if runner_host_path is None:
             runner_host_path = self._runner_host_path
-        runner_container_path = str(
-            docker_cfg.get("runner_container_path", self._runner_container_path) or self._runner_container_path
-        ).strip()

Also applies to: 687-692

🤖 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 531 - 568, Update
_install_runner_script_if_needed and its caller _start_container so
auto-detection checks record.run_command against the resolved per-environment
runner_container_path, not self._runner_container_path. Resolve the configured
path before evaluating the omitted install_runner_script fallback, pass or reuse
that value consistently for installation and logging, and preserve explicit
install_runner_script behavior.
🧹 Nitpick comments (1)
env/patcheval/docker_archive_adapter/sitecustomize.py (1)

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

Missing env var crashes at import time with a raw KeyError.

ARCHIVE_DIR is built from os.environ["PATCHEVAL_IMAGE_ARCHIVE_DIR"] at module scope, which runs whenever this directory is placed on PYTHONPATH (per the module docstring). If the variable is unset, this raises an unguarded KeyError before _install()'s own explicit "archive directory does not exist" check ever gets a chance to produce a clearer message. Since sitecustomize modules are auto-imported by every Python process that inherits this PYTHONPATH, an unset var turns into an opaque traceback rather than the intended actionable error.

🛠️ Proposed fix
-ARCHIVE_DIR = Path(os.environ["PATCHEVAL_IMAGE_ARCHIVE_DIR"]).expanduser()
+_archive_dir_env = os.environ.get("PATCHEVAL_IMAGE_ARCHIVE_DIR", "").strip()
+if not _archive_dir_env:
+    raise RuntimeError("PATCHEVAL_IMAGE_ARCHIVE_DIR must be set when this sitecustomize adapter is on PYTHONPATH")
+ARCHIVE_DIR = Path(_archive_dir_env).expanduser()
🤖 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/docker_archive_adapter/sitecustomize.py` around lines 16 - 19,
Update the module-level ARCHIVE_DIR initialization in sitecustomize so a missing
PATCHEVAL_IMAGE_ARCHIVE_DIR does not raise an unguarded KeyError during import;
use the existing _install() validation path to emit its intended actionable
archive-directory error, while preserving the current behavior when the variable
is set.
🤖 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 570-671: Update _start_container to hold the per-image lock across
image preparation and the actual docker run, preventing
_cleanup_archive_loaded_image from removing the image before creation. Refactor
_ensure_image_available so it does not reacquire the same lock when called from
this locked startup path, while preserving serialization for other callers.
Apply the same protection to pull-policy handling where the image is used.

In `@env/patcheval/generate_full_config.py`:
- Around line 96-102: Update archive_name to match the loader’s stem derivation
for untagged images: preserve the repository name and return its .tar archive
without appending “-latest”; continue using the repository-tag stem for
explicitly tagged images.

In `@env/patcheval/rule_evaluator.py`:
- Around line 26-34: Update the EvalResult construction in the strict-success
evaluation flow so the reason is conditional on strict_success: retain the
existing success message when true, and provide an accurate failure reason when
false. Keep the score calculation and other result fields unchanged.

In `@rl/examples/patcheval/run_external_eval.sh`:
- Line 253: Update the --max-steps argument in run_external_eval.sh so the
official run_official_s1.sh path honors the five-step S1.4 contract instead of
forcing a single step. Preserve the configured value from env.sh and ensure the
formal evaluation runs with five feedback steps.

---

Outside diff comments:
In `@manager/simulation_config.py`:
- Around line 472-480: Update the runner-mount handling near
_mount_from_runner_entrypoint and runner_container_path so that, when
install_runner_script is enabled, it still checks normalized_mounts for an
existing mount targeting runner_entrypoint.target and rejects conflicts before
setting runner_container_path. Preserve the existing _append_mount_if_missing
behavior for non-conflicting mounts.

---

Duplicate comments:
In `@clusters/docker_clusters.py`:
- Around line 531-568: Update _install_runner_script_if_needed and its caller
_start_container so auto-detection checks record.run_command against the
resolved per-environment runner_container_path, not self._runner_container_path.
Resolve the configured path before evaluating the omitted install_runner_script
fallback, pass or reuse that value consistently for installation and logging,
and preserve explicit install_runner_script behavior.

---

Nitpick comments:
In `@env/patcheval/docker_archive_adapter/sitecustomize.py`:
- Around line 16-19: Update the module-level ARCHIVE_DIR initialization in
sitecustomize so a missing PATCHEVAL_IMAGE_ARCHIVE_DIR does not raise an
unguarded KeyError during import; use the existing _install() validation path to
emit its intended actionable archive-directory error, while preserving the
current behavior when the variable is set.
🪄 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: 3fe45ff5-8c6b-4e7b-a0fe-bed1576e4ae6

📥 Commits

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

📒 Files selected for processing (20)
  • .dockerignore
  • args.py
  • clusters/docker_clusters.py
  • env/patcheval/docker_archive_adapter/sitecustomize.py
  • env/patcheval/generate_full_config.py
  • env/patcheval/official_bridge.py
  • env/patcheval/rule_evaluator.py
  • env/patcheval/strict_runner.py
  • manager/simulation_config.py
  • manager/types.py
  • requirements.txt
  • rl/buffer_server.py
  • rl/examples/patcheval/.gitignore
  • rl/examples/patcheval/README.md
  • rl/examples/patcheval/env.sh
  • rl/examples/patcheval/run_buffer_server.sh
  • rl/examples/patcheval/run_external_eval.sh
  • rl/examples/patcheval/run_official_s1.sh
  • tests/test_patcheval_official_bridge.py
  • tests/test_patcheval_strict_protocol.py

Comment on lines +570 to 671
async def _ensure_image_available(self, image: str) -> None:
image_lock = self._image_locks.setdefault(image, asyncio.Lock())
async with image_lock:
inspect_result = await self._run_command(
[self.docker_bin, "image", "inspect", image],
check=False,
timeout_s=self._inspect_timeout_s,
)
if inspect_result.returncode == 0:
return

archive_path = self._archive_path_for_image(image)
if archive_path is None:
raise RuntimeError(
f"Required Docker image {image!r} is unavailable and no matching archive was found"
)

log.info("loading Docker image %s from archive %s", image, archive_path)
await self._run_required(
[self.docker_bin, "load", "-i", str(archive_path)],
action=f"load image archive for {image}",
timeout_s=self._start_timeout_s,
)
verify_result = await self._run_command(
[self.docker_bin, "image", "inspect", image],
check=False,
timeout_s=self._inspect_timeout_s,
)
if verify_result.returncode != 0:
raise RuntimeError(
f"Docker archive {archive_path} loaded but expected image tag {image!r} is unavailable"
)
self._archive_loaded_images.add(image)

async def _cleanup_archive_loaded_image(self, image: str) -> None:
if not self._cleanup_image_on_finish or image not in self._archive_loaded_images:
return

image_lock = self._image_locks.setdefault(image, asyncio.Lock())
async with image_lock:
try:
containers = await self._run_command(
[self.docker_bin, "ps", "-aq", "--filter", f"ancestor={image}"],
check=False,
timeout_s=self._inspect_timeout_s,
)
except subprocess.TimeoutExpired:
log.warning(
"timed out after %.1fs checking containers for archive-loaded image %s; defer image cleanup",
self._inspect_timeout_s,
image,
)
return
if containers.returncode != 0:
log.warning(
"failed to check containers using archive-loaded image %s: %s",
image,
self._tail(containers.stderr),
)
return
if (containers.stdout or "").strip():
return

result = await self._run_command(
[self.docker_bin, "image", "rm", image],
check=False,
timeout_s=self._remove_timeout_s,
)
if result.returncode == 0 or self._is_missing_image_error(result.stderr):
self._archive_loaded_images.discard(image)
log.info("removed archive-loaded Docker image %s", image)
return
log.warning("failed to remove archive-loaded Docker image %s: %s", image, self._tail(result.stderr))

def _archive_path_for_image(self, image: str) -> Optional[Path]:
if self._image_archive_dir is None:
return None

image_name = str(image or "").strip().rsplit("/", 1)[-1]
if "@" in image_name:
repository, digest = image_name.split("@", 1)
archive_stem = f"{repository}-{digest.replace(':', '-')}"
elif ":" in image_name:
repository, tag = image_name.rsplit(":", 1)
archive_stem = f"{repository}-{tag}"
else:
archive_stem = f"{image_name}-latest"

for suffix in (".tar", ".tar.gz", ".tgz"):
candidate = self._image_archive_dir / f"{archive_stem}{suffix}"
if candidate.is_file() and candidate.stat().st_size > 0:
return candidate
return None

async def _start_container(self, *, env_name: str, image: str) -> DockerContainerRecord:
docker_cfg = self._docker_cfg_for_env(env_name)
pull_policy = self._resolve_pull_policy(docker_cfg)
if pull_policy == "always":
await self._run_required([self.docker_bin, "pull", image], action=f"pull image {image}")
else:
await self._ensure_image_available(image)

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 | 🏗️ Heavy lift

Image can be removed by a concurrent cleanup between "ensure available" and the actual docker run.

_ensure_image_available holds the per-image lock only through loading/verification (Line 570-602), then releases it. _start_container subsequently issues the real docker run (Line 743) outside that lock. If another task is concurrently finishing the last container that used the same image, _cleanup_archive_loaded_image (Line 604) can acquire the now-free per-image lock, see zero matching containers via docker ps --filter ancestor=... (the new container hasn't been created yet), and remove the image right before/while the new docker run executes — causing a spurious "no such image" failure. This only manifests when cleanup_image_on_finish is enabled (the intended PatchEval use case) with concurrent container starts, which this backend explicitly supports via _startup_sem.

See consolidated comment for the cross-file fix (this pattern also exists in env/patcheval/docker_archive_adapter/sitecustomize.py).

🤖 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 570 - 671, Update _start_container
to hold the per-image lock across image preparation and the actual docker run,
preventing _cleanup_archive_loaded_image from removing the image before
creation. Refactor _ensure_image_available so it does not reacquire the same
lock when called from this locked startup path, while preserving serialization
for other callers. Apply the same protection to pull-policy handling where the
image is used.

Comment on lines +96 to +102
def archive_name(image: str) -> str:
image_name = image.rsplit("/", 1)[-1]
if ":" in image_name:
repository, tag = image_name.rsplit(":", 1)
else:
repository, tag = image_name, "latest"
return f"{repository}-{tag}.tar"

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
# Confirm the loader's stem derivation to validate the naming contract.
fd -a sitecustomize.py env/patcheval/docker_archive_adapter --exec sed -n '50,80p' {}

Repository: AI45Lab/SAfactory

Length of output: 1412


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant functions and nearby callers.
sed -n '1,240p' env/patcheval/generate_full_config.py

echo '---'
# Find all references to archive_name and image sources.
rg -n "archive_name\(|image\b|latest|tag" env/patcheval/generate_full_config.py env/patcheval -g '!**/__pycache__/**'

Repository: AI45Lab/SAfactory

Length of output: 14394


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import json

dataset = Path("env/patcheval/PatchEval/patcheval/exp_agent/sweagent/dataset.jsonl")
official = Path("env/patcheval/PatchEval/patcheval/datasets/input.json")

def scan_jsonl(path):
    total = 0
    no_tag = 0
    samples = []
    with path.open("r", encoding="utf-8") as f:
        for line in f:
            if not line.strip():
                continue
            total += 1
            rec = json.loads(line)
            img = str(rec.get("image_name", "")).strip()
            last = img.rsplit("/", 1)[-1]
            if ":" not in last:
                no_tag += 1
                if len(samples) < 10:
                    samples.append(img)
    return total, no_tag, samples

total, no_tag, samples = scan_jsonl(dataset)
print(f"dataset: {dataset}")
print(f"records={total} no_tag={no_tag}")
for s in samples:
    print(s)

with official.open("r", encoding="utf-8") as f:
    records = json.load(f)
count = 0
samples = []
for rec in records:
    img = rec.get("image_name")
    if isinstance(img, str) and img.strip():
        count += 1
        last = img.rsplit("/", 1)[-1]
        if ":" not in last and len(samples) < 10:
            samples.append(img)
print(f"official records with image_name={count}")
print("official no-tag samples:")
for s in samples:
    print(s)
PY

Repository: AI45Lab/SAfactory

Length of output: 593


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the dataset files used by env/patcheval/generate_full_config.py.
fd -a 'dataset.jsonl' env/patcheval
echo '---'
fd -a 'input.json' env/patcheval

echo '---'
# Print every image_name pattern from the likely dataset files if present.
python3 - <<'PY'
from pathlib import Path
import json

candidates = list(Path("env/patcheval").rglob("dataset.jsonl")) + list(Path("env/patcheval").rglob("input.json"))
seen = set()
for path in candidates:
    if path in seen:
        continue
    seen.add(path)
    print(f"FILE: {path}")
    try:
        if path.name == "dataset.jsonl":
            total = 0
            no_tag = 0
            samples = []
            with path.open("r", encoding="utf-8") as f:
                for line in f:
                    if not line.strip():
                        continue
                    total += 1
                    rec = json.loads(line)
                    img = str(rec.get("image_name", "")).strip()
                    last = img.rsplit("/", 1)[-1]
                    if ":" not in last:
                        no_tag += 1
                        if len(samples) < 10:
                            samples.append(img)
            print(f"records={total} no_tag={no_tag}")
            for s in samples:
                print(f"  {s}")
        elif path.name == "input.json":
            with path.open("r", encoding="utf-8") as f:
                records = json.load(f)
            total = 0
            no_tag = 0
            samples = []
            for rec in records if isinstance(records, list) else []:
                img = str(rec.get("image_name", "")).strip()
                if not img:
                    continue
                total += 1
                last = img.rsplit("/", 1)[-1]
                if ":" not in last:
                    no_tag += 1
                    if len(samples) < 10:
                        samples.append(img)
            print(f"records_with_image_name={total} no_tag={no_tag}")
            for s in samples:
                print(f"  {s}")
    except Exception as e:
        print(f"ERROR: {e}")
PY

Repository: AI45Lab/SAfactory

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where image_name values come from and whether examples are tagged or untagged.
rg -n '"image_name"|env_image|archive_name\(' . -g '!**/__pycache__/**' -g '!**/.git/**'

echo '---'
# Show files in env/patcheval that mention the dataset or archive conventions.
rg -n 'dataset.jsonl|input.json|archive|docker_archive_adapter|latest|image_name' env/patcheval -g '!**/__pycache__/**'

Repository: AI45Lab/SAfactory

Length of output: 9623


Align archive_name with the loader’s stem rule.
generate_full_config.py writes bar-latest.tar for any image without a tag, but env/patcheval/docker_archive_adapter/sitecustomize.py looks for bar.tar. That leaves colonless images undeployable; use the same stem derivation in both places.

🔧 Align the stem derivation
 def archive_name(image: str) -> str:
-    image_name = image.rsplit("/", 1)[-1]
-    if ":" in image_name:
-        repository, tag = image_name.rsplit(":", 1)
-    else:
-        repository, tag = image_name, "latest"
-    return f"{repository}-{tag}.tar"
+    stem = image.rsplit("/", 1)[-1].replace(":", "-")
+    return f"{stem}.tar"
📝 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 archive_name(image: str) -> str:
image_name = image.rsplit("/", 1)[-1]
if ":" in image_name:
repository, tag = image_name.rsplit(":", 1)
else:
repository, tag = image_name, "latest"
return f"{repository}-{tag}.tar"
def archive_name(image: str) -> str:
stem = image.rsplit("/", 1)[-1].replace(":", "-")
return f"{stem}.tar"
🤖 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/generate_full_config.py` around lines 96 - 102, Update
archive_name to match the loader’s stem derivation for untagged images: preserve
the repository name and return its .tar archive without appending “-latest”;
continue using the repository-tag stem for explicitly tagged images.

Comment thread env/patcheval/rule_evaluator.py Outdated
--pool-size "${PATCH_EVAL_POOL_SIZE}" \
--max-workers "${PATCH_EVAL_POOL_SIZE}" \
--no-circuit-breaker \
--max-steps 1 \

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

Honor the five-step S1.4 contract.

Line 253 forces every setting to one step. This makes the formal run_official_s1.sh path run S1.4 without the five feedback steps configured in env.sh.

Proposed fix
+if [[ "${PATCH_EVAL_SETTING}" == "s1.4" ]]; then
+  PATCH_EVAL_MAX_STEPS=${PATCH_EVAL_MAX_STEPS:-5}
+else
+  PATCH_EVAL_MAX_STEPS=${PATCH_EVAL_MAX_STEPS:-1}
+fi
+
 ...
-  --max-steps 1 \
+  --max-steps "${PATCH_EVAL_MAX_STEPS}" \
📝 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
--max-steps 1 \
if [[ "${PATCH_EVAL_SETTING}" == "s1.4" ]]; then
PATCH_EVAL_MAX_STEPS=${PATCH_EVAL_MAX_STEPS:-5}
else
PATCH_EVAL_MAX_STEPS=${PATCH_EVAL_MAX_STEPS:-1}
fi
--max-steps "${PATCH_EVAL_MAX_STEPS}" \
🤖 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 253, Update the
--max-steps argument in run_external_eval.sh so the official run_official_s1.sh
path honors the five-step S1.4 contract instead of forcing a single step.
Preserve the configured value from env.sh and ensure the formal evaluation runs
with five feedback steps.

@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: 4

🧹 Nitpick comments (1)
tests/test_patcheval_rule_evaluator.py (1)

35-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the failure branches of evaluate_rule.

Current tests only cover the "Repair Success" and generic validation-failure happy paths via a monkeypatched _load_official_evaluation. Consider adding cases for: missing cve_id/patch/language (early EvalResult.failed), poc_passed is None (container failed to start), and an exception raised from run_evaluation. Also assert on result.reason in the failure test to guard against regressions of the "reason always claims success" bug fixed in this file.

🤖 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 `@tests/test_patcheval_rule_evaluator.py` around lines 35 - 73, Extend the
tests around evaluate_rule to cover missing cve_id, patch, or language returning
EvalResult.failed, poc_passed=None representing container startup failure, and
run_evaluation raising an exception. Assert each failure’s status and reason,
including the existing validation-failure case, to ensure reasons do not
incorrectly report success.
🤖 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 `@env/patcheval/generate_full_config.py`:
- Around line 36-38: Remove the machine-specific default from
DEFAULT_OFFICIAL_RUNTIME and make --official-runtime-dir an explicit required
CLI argument, ensuring the generator cannot fall back to a personal path and
still passes the provided directory through the existing required_runtime_files
validation.

In `@env/patcheval/rule_evaluator.py`:
- Around line 53-75: Apply request.env_params["rule_evaluator_timeout_s"] to the
asyncio.to_thread call that runs evaluation.run_evaluation, using an async
timeout mechanism and preserving the existing EvalResult.failed response for
timeout failures. Ensure timeout handling reports that the official PatchEval
evaluation exceeded the configured limit, while retaining the current exception
handling for other evaluation errors.

In `@rl/examples/patcheval/run_eval.sh`:
- Line 11: Update the default PATCH_EVAL_API_BASE configuration so Gateway never
sends PATCH_EVAL_API_KEY to a cleartext endpoint by default. Use the HTTPS base
URL, or require an explicit opt-in before permitting an http:// value, while
preserving the existing API key forwarding behavior for secure endpoints.
- Around line 24-25: Stop defaulting GATEWAY_HOST to the first address returned
by hostname -I; require it explicitly or derive it using a known Docker-routable
address. Update the related guidance at rl/examples/patcheval/run_eval.sh lines
24-25 and 145, and rl/examples/patcheval/README.md line 165 so all documented
and runtime behavior uses the same safe configuration.

---

Nitpick comments:
In `@tests/test_patcheval_rule_evaluator.py`:
- Around line 35-73: Extend the tests around evaluate_rule to cover missing
cve_id, patch, or language returning EvalResult.failed, poc_passed=None
representing container startup failure, and run_evaluation raising an exception.
Assert each failure’s status and reason, including the existing
validation-failure case, to ensure reasons do not incorrectly report success.
🪄 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: 88a7c8b1-ad0d-49dd-bdc8-b645afa8604d

📥 Commits

Reviewing files that changed from the base of the PR and between 109eb9c and 6e7c4e4.

📒 Files selected for processing (7)
  • env/patcheval/generate_full_config.py
  • env/patcheval/rule_evaluator.py
  • env/patcheval/strict_runner.py
  • rl/examples/patcheval/README.md
  • rl/examples/patcheval/env.sh
  • rl/examples/patcheval/run_eval.sh
  • tests/test_patcheval_rule_evaluator.py
💤 Files with no reviewable changes (1)
  • rl/examples/patcheval/env.sh

Comment on lines +36 to +38
DEFAULT_OFFICIAL_RUNTIME = Path(
"/mnt/shared-storage-user/evobox-share/leishanzhe/dataset/patcheval-runtime"
)

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

Hardcoded personal path as a CLI default.

DEFAULT_OFFICIAL_RUNTIME bakes in what looks like a specific engineer's home-mount path (.../leishanzhe/dataset/patcheval-runtime). Anyone running this generator without explicitly passing --official-runtime-dir will hit the required_runtime_files failure (Line 251-259) on any machine other than the original author's.

🔧 Suggested fix
-DEFAULT_OFFICIAL_RUNTIME = Path(
-    "/mnt/shared-storage-user/evobox-share/leishanzhe/dataset/patcheval-runtime"
-)
+DEFAULT_OFFICIAL_RUNTIME = Path(
+    os.environ.get("PATCHEVAL_RUNTIME_DIR", "/opt/patcheval-runtime")
+)

Or simply make --official-runtime-dir a required argument instead of defaulting to a machine-specific path.

📝 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
DEFAULT_OFFICIAL_RUNTIME = Path(
"/mnt/shared-storage-user/evobox-share/leishanzhe/dataset/patcheval-runtime"
)
DEFAULT_OFFICIAL_RUNTIME = Path(
os.environ.get("PATCHEVAL_RUNTIME_DIR", "/opt/patcheval-runtime")
)
🤖 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/generate_full_config.py` around lines 36 - 38, Remove the
machine-specific default from DEFAULT_OFFICIAL_RUNTIME and make
--official-runtime-dir an explicit required CLI argument, ensuring the generator
cannot fall back to a personal path and still passes the provided directory
through the existing required_runtime_files validation.

Comment on lines +53 to +75
try:
evaluation_class = _load_official_evaluation(request.env_params)
evaluation = evaluation_class(
logger=logging.getLogger(f"safactory.patcheval.official.{cve_id.lower()}"),
cve=cve_id,
)
poc_passed, poc_log, unit_tests_passed, unit_test_log, validation_type = await asyncio.to_thread(
evaluation.run_evaluation,
cve_id,
patch,
language,
f"safactory_{request.session_id.replace('-', '_')}",
[],
)
except Exception as exc:
return EvalResult.failed(
session_id=request.session_id,
eval_id=spec.eval_id,
method=spec.method.value,
reason="official PatchEval evaluation raised an exception",
error_text=str(exc),
artifacts={"bench": "patcheval", "cve_id": cve_id, "patch": patch},
)

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

rule_evaluator_timeout_s is generated but never applied — the blocking evaluation call has no timeout.

generate_full_config.py sets env_params["rule_evaluator_timeout_s"] specifically for this evaluator (default 3600s, CLI-configurable), but evaluate_rule never reads that key. await asyncio.to_thread(evaluation.run_evaluation, ...) has no timeout at all — a hung/blocked official Docker evaluation will hang this coroutine indefinitely, and even if an outer caller later cancels it, the underlying thread can't be forcibly stopped (Python threads aren't killable), so the run continues consuming resources in the background.

🔧 Suggested fix: enforce the generated timeout
     try:
         evaluation_class = _load_official_evaluation(request.env_params)
         evaluation = evaluation_class(
             logger=logging.getLogger(f"safactory.patcheval.official.{cve_id.lower()}"),
             cve=cve_id,
         )
-        poc_passed, poc_log, unit_tests_passed, unit_test_log, validation_type = await asyncio.to_thread(
-            evaluation.run_evaluation,
-            cve_id,
-            patch,
-            language,
-            f"safactory_{request.session_id.replace('-', '_')}",
-            [],
-        )
+        timeout_s = float(request.env_params.get("rule_evaluator_timeout_s") or spec.timeout_s)
+        poc_passed, poc_log, unit_tests_passed, unit_test_log, validation_type = await asyncio.wait_for(
+            asyncio.to_thread(
+                evaluation.run_evaluation,
+                cve_id,
+                patch,
+                language,
+                f"safactory_{request.session_id.replace('-', '_')}",
+                [],
+            ),
+            timeout=timeout_s,
+        )
+    except asyncio.TimeoutError as exc:
+        return EvalResult.failed(
+            session_id=request.session_id,
+            eval_id=spec.eval_id,
+            method=spec.method.value,
+            reason="official PatchEval evaluation timed out",
+            error_text=str(exc),
+            artifacts={"bench": "patcheval", "cve_id": cve_id, "patch": patch},
+        )
     except Exception as exc:
📝 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
try:
evaluation_class = _load_official_evaluation(request.env_params)
evaluation = evaluation_class(
logger=logging.getLogger(f"safactory.patcheval.official.{cve_id.lower()}"),
cve=cve_id,
)
poc_passed, poc_log, unit_tests_passed, unit_test_log, validation_type = await asyncio.to_thread(
evaluation.run_evaluation,
cve_id,
patch,
language,
f"safactory_{request.session_id.replace('-', '_')}",
[],
)
except Exception as exc:
return EvalResult.failed(
session_id=request.session_id,
eval_id=spec.eval_id,
method=spec.method.value,
reason="official PatchEval evaluation raised an exception",
error_text=str(exc),
artifacts={"bench": "patcheval", "cve_id": cve_id, "patch": patch},
)
try:
evaluation_class = _load_official_evaluation(request.env_params)
evaluation = evaluation_class(
logger=logging.getLogger(f"safactory.patcheval.official.{cve_id.lower()}"),
cve=cve_id,
)
timeout_s = float(request.env_params.get("rule_evaluator_timeout_s") or spec.timeout_s)
poc_passed, poc_log, unit_tests_passed, unit_test_log, validation_type = await asyncio.wait_for(
asyncio.to_thread(
evaluation.run_evaluation,
cve_id,
patch,
language,
f"safactory_{request.session_id.replace('-', '_')}",
[],
),
timeout=timeout_s,
)
except asyncio.TimeoutError as exc:
return EvalResult.failed(
session_id=request.session_id,
eval_id=spec.eval_id,
method=spec.method.value,
reason="official PatchEval evaluation timed out",
error_text=str(exc),
artifacts={"bench": "patcheval", "cve_id": cve_id, "patch": patch},
)
except Exception as exc:
return EvalResult.failed(
session_id=request.session_id,
eval_id=spec.eval_id,
method=spec.method.value,
reason="official PatchEval evaluation raised an exception",
error_text=str(exc),
artifacts={"bench": "patcheval", "cve_id": cve_id, "patch": patch},
)
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 67-67: 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/rule_evaluator.py` around lines 53 - 75, Apply
request.env_params["rule_evaluator_timeout_s"] to the asyncio.to_thread call
that runs evaluation.run_evaluation, using an async timeout mechanism and
preserving the existing EvalResult.failed response for timeout failures. Ensure
timeout handling reports that the official PatchEval evaluation exceeded the
configured limit, while retaining the current exception handling for other
evaluation errors.

: "${DOCKER_HOST:?Set DOCKER_HOST before running}"

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 | 🟠 Major | ⚡ Quick win

Do not send the API key to a cleartext endpoint.

The default PATCH_EVAL_API_BASE uses http://, and lines 76-77 configure Gateway to send PATCH_EVAL_API_KEY to it. Use HTTPS by default, or require an explicit insecure opt-in.

🤖 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_eval.sh` at line 11, Update the default
PATCH_EVAL_API_BASE configuration so Gateway never sends PATCH_EVAL_API_KEY to a
cleartext endpoint by default. Use the HTTPS base URL, or require an explicit
opt-in before permitting an http:// value, while preserving the existing API key
forwarding behavior for secure endpoints.

Comment on lines +24 to +25
GATEWAY_HOST=${GATEWAY_HOST:-$(hostname -I | awk '{print $1}')}
PATCH_EVAL_NO_PROXY=${PATCH_EVAL_NO_PROXY:-host.docker.internal,localhost,127.0.0.1,::1,${GATEWAY_HOST},10.0.0.0/8,100.96.0.0/12,.pjlab.org.cn}

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== run_eval.sh (relevant sections) ==\n'
sed -n '1,220p' rl/examples/patcheval/run_eval.sh | nl -ba | sed -n '1,220p'

printf '\n== README.md (relevant sections) ==\n'
sed -n '130,190p' rl/examples/patcheval/README.md | nl -ba | sed -n '1,120p'

printf '\n== Search for gateway URL usage ==\n'
rg -n "gateway-base-url|GATEWAY_HOST|host.docker.internal|127\.0\.0\.1:8000/v1/sessions|PATCH_EVAL_NO_PROXY" rl/examples/patcheval -S

Repository: AI45Lab/SAfactory

Length of output: 234


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' rl/examples/patcheval/run_eval.sh | nl -ba | sed -n '1,220p'
sed -n '130,190p' rl/examples/patcheval/README.md | nl -ba | sed -n '1,120p'
rg -n "gateway-base-url|GATEWAY_HOST|host.docker.internal|127\.0\.0\.1:8000/v1/sessions|PATCH_EVAL_NO_PROXY" rl/examples/patcheval -S

Repository: AI45Lab/SAfactory

Length of output: 195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

def show(path, start, end):
    print(f"\n== {path} {start}-{end} ==")
    lines = Path(path).read_text().splitlines()
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}: {lines[i-1]}")
show("rl/examples/patcheval/run_eval.sh", 1, 220)
show("rl/examples/patcheval/README.md", 130, 190)

print("\n== Search ==\n")
import subprocess, shlex
cmd = r"rg -n \"gateway-base-url|GATEWAY_HOST|host.docker.internal|127\.0\.0\.1:8000/v1/sessions|PATCH_EVAL_NO_PROXY\" rl/examples/patcheval -S"
subprocess.run(cmd, shell=True, check=False)
PY

Repository: AI45Lab/SAfactory

Length of output: 10198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

def find_files(root, needle):
    for p in Path(root).rglob("*"):
        if p.is_file():
            try:
                text = p.read_text(errors="ignore")
            except Exception:
                continue
            if needle in text:
                print(p)

print("== matches for gateway-base-url ==")
find_files(".", "gateway-base-url")

print("\n== matches for GATEWAY_HOST ==")
find_files(".", "GATEWAY_HOST")
PY

Repository: AI45Lab/SAfactory

Length of output: 790


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== args.py: gateway-base-url definition and related args ==\n'
python3 - <<'PY'
from pathlib import Path
path = Path("args.py")
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if "gateway-base-url" in line or "gateway" in line.lower() and "base" in line.lower():
        start = max(1, i-12)
        end = min(len(lines), i+20)
        print(f"\n-- {path} {start}-{end} --")
        for j in range(start, end + 1):
            print(f"{j:4d}: {lines[j-1]}")
PY

printf '\n== docs/gateway.md relevant sections ==\n'
python3 - <<'PY'
from pathlib import Path
path = Path("docs/gateway.md")
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if "gateway-base-url" in line or "127.0.0.1" in line or "docker" in line.lower():
        start = max(1, i-10)
        end = min(len(lines), i+25)
        print(f"\n-- {path} {start}-{end} --")
        for j in range(start, end + 1):
            print(f"{j:4d}: {lines[j-1]}")
PY

printf '\n== rl/gateway_autostart.py relevant sections ==\n'
python3 - <<'PY'
from pathlib import Path
path = Path("rl/gateway_autostart.py")
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if "GATEWAY_HOST" in line or "gateway" in line.lower() and "url" in line.lower():
        start = max(1, i-12)
        end = min(len(lines), i+20)
        print(f"\n-- {path} {start}-{end} --")
        for j in range(start, end + 1):
            print(f"{j:4d}: {lines[j-1]}")
PY

Repository: AI45Lab/SAfactory

Length of output: 12176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== definitions/usages of gateway-base-url ==\n'
rg -n "gateway-base-url|gateway_base_url|GATEWAY_BASE_URL" args.py launcher.py rl docs README.md README_CN.md rl -S

printf '\n== launcher.py around gateway handling ==\n'
python3 - <<'PY'
from pathlib import Path
path = Path("launcher.py")
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if "gateway" in line.lower():
        start = max(1, i-12)
        end = min(len(lines), i+20)
        print(f"\n-- {path} {start}-{end} --")
        for j in range(start, end + 1):
            print(f"{j:4d}: {lines[j-1]}")
PY

printf '\n== args.py around gateway options ==\n'
python3 - <<'PY'
from pathlib import Path
path = Path("args.py")
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
    if "gateway" in line.lower():
        start = max(1, i-12)
        end = min(len(lines), i+20)
        print(f"\n-- {path} {start}-{end} --")
        for j in range(start, end + 1):
            print(f"{j:4d}: {lines[j-1]}")
PY

Repository: AI45Lab/SAfactory

Length of output: 19580


Avoid auto-selecting the first local IP. hostname -I | awk '{print $1}' can pick an address the Docker containers can’t reach on multi-homed hosts or some DOCKER_HOST setups; make GATEWAY_HOST explicit or derive a known routable host address.

📍 Affects 2 files
  • rl/examples/patcheval/run_eval.sh#L24-L25 (this comment)
  • rl/examples/patcheval/run_eval.sh#L145-L145
  • rl/examples/patcheval/README.md#L165-L165
🤖 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_eval.sh` around lines 24 - 25, Stop defaulting
GATEWAY_HOST to the first address returned by hostname -I; require it explicitly
or derive it using a known Docker-routable address. Update the related guidance
at rl/examples/patcheval/run_eval.sh lines 24-25 and 145, and
rl/examples/patcheval/README.md line 165 so all documented and runtime behavior
uses the same safe configuration.

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