Guard Cursor file-edit rollouts with sandboxing

This commit is contained in:
James Davies
2026-07-20 21:15:04 +01:00
parent 586b1e3f6c
commit 4aa59ed0a8
5 changed files with 42 additions and 8 deletions

View File

@@ -205,7 +205,9 @@ directly through this backend.
Cursor Agent first, then select it with `model.target_backend=cursor_exec`.
Read-only rollouts use Ask mode; artifact-producing rollouts add Cursor's
headless `--force` flag inside the benchmark workspace. SkillOpt enables the
Cursor sandbox by default and does not approve MCP servers automatically.
Cursor sandbox by default and rejects file-edit rollouts if it is disabled;
read-only Ask-mode rollouts may explicitly disable it. SkillOpt does not approve
MCP servers automatically.
### Three OpenAI-compatible paths

View File

@@ -2,10 +2,9 @@
> **Version note.** This reference tracks `main`. PyPI 0.2.0 does not yet
> include the generic research `openai_compatible` backend, Sleep handoff,
> Sleep support for non-Azure OpenAI-compatible endpoints, or the Sleep
> Sleep support for non-Azure OpenAI-compatible endpoints, the Sleep
> `--preferences` flag, or the research `cursor_exec` target harness; use a
> source install from `main` for those
> features until the next release.
> source install from `main` for those features until the next release.
## Training
@@ -108,8 +107,10 @@ python scripts/eval_only.py \
`cursor_exec` runs the target only; the optimizer remains separately
configured. Read-only rollouts use Cursor Ask mode. Rollouts that request file
edits use `--force` inside the benchmark workspace, with Cursor sandboxing
enabled by default. Override the executable or sandbox through
`model.cursor_exec_path` and `model.cursor_exec_sandbox`.
enabled. The harness refuses file-edit rollouts when the Cursor sandbox is
disabled. Read-only Ask-mode rollouts may explicitly disable it. Override the
executable or sandbox through `model.cursor_exec_path` and
`model.cursor_exec_sandbox`.
## SkillOpt-Sleep

View File

@@ -66,7 +66,7 @@ defaults to `claude` and can be overridden with `CLAUDE_CLI_BIN`.
| `model.codex_exec_*` | Codex path, sandbox, profile, SDK mode, reasoning, network/search, and approval policy |
| `model.claude_code_exec_*` | Claude path, profile, SDK mode, effort, and thinking-token cap |
| `model.cursor_exec_path` | Cursor Agent executable path; default `cursor-agent` |
| `model.cursor_exec_sandbox` | Cursor sandbox mode: `enabled` (default) or `disabled` |
| `model.cursor_exec_sandbox` | Cursor sandbox mode: `enabled` (default) or `disabled`; file-edit rollouts require `enabled` |
## Training (`train`)

View File

@@ -18,7 +18,6 @@ from skillopt.model.backend_config import (
get_target_backend,
)
ANSWER_SCHEMA: dict[str, Any] = {
"type": "object",
"properties": {
@@ -1212,6 +1211,11 @@ def run_cursor_exec(
actual_sandbox = str(sandbox or config["sandbox"])
if actual_sandbox not in {"enabled", "disabled"}:
raise ValueError("Cursor Agent sandbox must be 'enabled' or 'disabled'")
if allow_file_edits and actual_sandbox == "disabled":
raise ValueError(
"Cursor Agent file-edit rollouts require sandbox='enabled'; "
"refusing to combine --force with a disabled sandbox"
)
for attempt in range(retries + 1):
attempt_prompt = _exec_prompt(

View File

@@ -201,6 +201,33 @@ def test_cursor_exec_force_is_limited_to_file_edit_rollouts(
assert "You may modify files" in prompt
def test_cursor_exec_rejects_file_edits_with_disabled_sandbox(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
work_dir = _workspace(tmp_path)
calls = 0
def fake_run(_cmd: list[str], **_kwargs: Any) -> SimpleNamespace:
nonlocal calls
calls += 1
return SimpleNamespace(returncode=0, stdout=_result(), stderr="")
backend_config.configure_cursor_exec(sandbox="disabled")
monkeypatch.setattr(harness.subprocess, "run", fake_run)
with pytest.raises(ValueError, match="refusing to combine --force"):
harness.run_cursor_exec(
work_dir=str(work_dir),
prompt="Write solution.py.",
model="composer-2.5",
timeout=10,
allow_file_edits=True,
)
assert calls == 0
def test_cursor_exec_retries_zero_exit_malformed_output(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,