Skip to content

Commit 80ff2e5

Browse files
authored
Merge branch 'main' into dataset/code-review/gold-audit-639655
2 parents a8503c3 + b42b137 commit 80ff2e5

9 files changed

Lines changed: 720 additions & 19 deletions

File tree

docs/_data/code-review.json

Lines changed: 673 additions & 0 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ ignore = [
8787
"TRY301", # raise-within-try: suggested "abstract to inner function" refactor adds indirection
8888
]
8989

90+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
91+
# Force sandboxed rendering:
92+
"jinja2.Template".msg = "Use jinja2.sandbox.SandboxedEnvironment instead; the bare Template is not sandboxed."
93+
"jinja2.Environment".msg = "Use jinja2.sandbox.SandboxedEnvironment instead; the bare Environment is not sandboxed."
94+
9095
[tool.ruff.lint.per-file-ignores]
9196
"tests/**" = ["ANN"]
9297
"notebooks/**" = ["ANN", "T20"] # notebooks routinely print() for analysis output

src/bcbench/agent/shared/mcp.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55
from typing import Any
66

7-
from jinja2 import Template
7+
from jinja2.sandbox import SandboxedEnvironment
88

99
from bcbench.agent.shared.altool_paths import build_assembly_probing_paths, compiler_symbol_folder_for_container
1010
from bcbench.dataset import BaseDatasetEntry
@@ -13,6 +13,8 @@
1313

1414
logger = get_logger(__name__)
1515

16+
_jinja = SandboxedEnvironment(autoescape=False)
17+
1618

1719
def _build_server_entry(server: dict[str, Any], template_context: dict[str, Any]) -> tuple[str, dict[str, Any]]:
1820
server_type: str = server["type"]
@@ -26,7 +28,7 @@ def _build_server_entry(server: dict[str, Any], template_context: dict[str, Any]
2628
}
2729
case "stdio":
2830
args: list[str] = server["args"]
29-
rendered_args = [Template(arg).render(**template_context) for arg in args]
31+
rendered_args = [_jinja.from_string(arg).render(**template_context) for arg in args]
3032
command: str = shutil.which(server["command"]) or server["command"]
3133
entry: dict[str, Any] = {
3234
"type": server_type,

src/bcbench/agent/shared/prompt.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import re
22
from pathlib import Path
33

4-
from jinja2 import Template
4+
from jinja2.sandbox import SandboxedEnvironment
55

66
from bcbench.config import get_config
77
from bcbench.dataset import BaseDatasetEntry
88
from bcbench.types import EvaluationCategory
99

1010
_config = get_config()
1111

12+
_jinja = SandboxedEnvironment(autoescape=False)
13+
1214

1315
def _transform_image_paths(content: str) -> str:
1416
dest_dir = _config.file_patterns.problem_statement_dest_dir
@@ -26,8 +28,7 @@ def build_prompt(entry: BaseDatasetEntry, repo_path: Path, config: dict, categor
2628

2729
task = _transform_image_paths(entry.get_task())
2830

29-
template = Template(template_str)
30-
return template.render(
31+
return _jinja.from_string(template_str).render(
3132
repo_path=repo_path,
3233
task=task,
3334
project_paths=", ".join(entry.project_paths),

src/bcbench/cli_options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# Note: Defaults are provided in function signatures, not here
1212
RepoPath = Annotated[Path, typer.Option(help="Path to repository")]
1313

14-
OutputDir = Annotated[Path, typer.Option(help="Directory to save evaluation results")]
14+
OutputDir = Annotated[Path, typer.Option(help="Directory to save evaluation results", file_okay=False, dir_okay=True)]
1515

1616
RunId = Annotated[str, typer.Option(envvar="GITHUB_RUN_ID", help="Unique identifier for this evaluation run")]
1717

src/bcbench/dataset/codereview.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from enum import StrEnum
4+
from typing import Annotated
45

56
from pydantic import BaseModel, ConfigDict, Field, field_validator
67

@@ -46,11 +47,11 @@ def from_input(cls, value: str) -> Severity:
4647
class ReviewComment(BaseModel):
4748
model_config = ConfigDict(frozen=True)
4849

49-
file: str
50-
line_start: int
51-
line_end: int | None = None
50+
file: Annotated[str, Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9./_-]*\.(al|json)$")]
51+
line_start: Annotated[int, Field(ge=1)]
52+
line_end: Annotated[int, Field(ge=1)] | None = None
5253
domain: str | None = None
53-
body: str
54+
body: Annotated[str, Field(min_length=1)]
5455
severity: Severity | None = None
5556

5657
@field_validator("severity", mode="before")

src/bcbench/dataset/dataset_entry.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
class TestEntry(BaseModel):
2121
model_config = ConfigDict(frozen=True)
2222

23-
codeunitID: int
23+
codeunitID: Annotated[int, Field(gt=0)]
2424
functionName: Annotated[frozenset[str], Field(min_length=1)]
2525

2626

2727
class EntryMetadata(BaseModel):
2828
model_config = ConfigDict(frozen=True)
2929

3030
area: str | None = None
31-
image_count: int | None = None
31+
image_count: Annotated[int, Field(ge=0)] | None = None
3232
persona: str | None = None
3333

3434

@@ -44,8 +44,8 @@ class BaseDatasetEntry(BaseModel):
4444
base_commit: str = Field(pattern=r"^[a-fA-F0-9]{40}$")
4545
created_at: Annotated[str, Field(min_length=1)]
4646
environment_setup_version: str = Field(pattern=r"^[0-9]{2}\.[0-9]{1}$")
47-
project_paths: list[str] = []
48-
patch: Annotated[str, Field(min_length=1)]
47+
project_paths: list[Annotated[str, Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9 \\/-]*$")]] = []
48+
patch: Annotated[str, Field(min_length=1, pattern=r"^[^\x00]*$")]
4949

5050
@classmethod
5151
def load(cls, dataset_path: Path, entry_id: str | None = None, random: int | None = None) -> list[Self]:
@@ -112,7 +112,7 @@ class _BugFixTestGenBase(BaseDatasetEntry):
112112

113113
fail_to_pass: Annotated[list[TestEntry], Field(alias="FAIL_TO_PASS", min_length=1)]
114114
pass_to_pass: Annotated[list[TestEntry], Field(alias="PASS_TO_PASS")] = []
115-
test_patch: Annotated[str, Field(min_length=1)]
115+
test_patch: Annotated[str, Field(min_length=1, pattern=r"^[^\x00]*$")]
116116

117117
@property
118118
def problem_statement_dir(self) -> Path:
@@ -158,9 +158,9 @@ class NL2ALEntry(BaseDatasetEntry):
158158
"""Dataset entry for NL2AL category — generate AL code from natural language."""
159159

160160
base_commit: str | None = None
161-
nl_prompt: Annotated[str, Field(min_length=1)]
161+
nl_prompt: Annotated[str, Field(min_length=1, pattern=r"^[^\x00]*$")]
162162
expected: Annotated[list[ChecklistAssertion], Field(min_length=1)]
163-
page: str
163+
page: Annotated[str, Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9 ./]*$")]
164164
audience: Literal["Business", "Technical", "Both"]
165165

166166
def get_task(self) -> str:

src/bcbench/operations/hooks_operations.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import json
3+
import shlex
34
from pathlib import Path
45

56
from bcbench.config import get_config
@@ -60,15 +61,16 @@ def _setup_claude_hooks(repo_path: Path, script_path: str, tool_log_path: Path)
6061
with contextlib.suppress(json.JSONDecodeError):
6162
existing_settings = json.loads(settings_file.read_text(encoding="utf-8"))
6263

63-
tool_log_resolved = str(tool_log_path.resolve())
64+
tool_log_quoted = shlex.quote(str(tool_log_path.resolve()))
65+
script_path_quoted = shlex.quote(script_path)
6466
existing_settings["hooks"] = {
6567
"PreToolUse": [
6668
{
6769
"matcher": "",
6870
"hooks": [
6971
{
7072
"type": "command",
71-
"command": f'BCBENCH_TOOL_LOG="{tool_log_resolved}" pwsh -ExecutionPolicy Bypass -File "{script_path}"',
73+
"command": f"BCBENCH_TOOL_LOG={tool_log_quoted} pwsh -ExecutionPolicy Bypass -File {script_path_quoted}",
7274
}
7375
],
7476
}

tests/test_hooks_operations.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import shlex
23
from pathlib import Path
34

45
from bcbench.operations.hooks_operations import setup_hooks
@@ -102,3 +103,19 @@ def test_tool_log_path_is_absolute_in_env(self, tmp_path: Path):
102103
log_path = hooks_config["hooks"]["preToolUse"][0]["env"]["BCBENCH_TOOL_LOG"]
103104

104105
assert Path(log_path).is_absolute()
106+
107+
def test_claude_command_escapes_shell_metacharacters(self, tmp_path: Path):
108+
repo_path = tmp_path / "repo"
109+
repo_path.mkdir()
110+
output_dir = tmp_path / "results$(id)`whoami`"
111+
output_dir.mkdir()
112+
113+
setup_hooks(repo_path, AgentType.CLAUDE, output_dir)
114+
115+
settings = json.loads((repo_path / ".claude" / "settings.local.json").read_text(encoding="utf-8"))
116+
command = settings["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
117+
118+
# shlex.quote wraps the path in single quotes so $() and backticks are inert.
119+
assert command.startswith("BCBENCH_TOOL_LOG='")
120+
assert 'BCBENCH_TOOL_LOG="' not in command
121+
assert shlex.quote(str((output_dir / "tool_usage.jsonl").resolve())) in command

0 commit comments

Comments
 (0)