diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 8ce1760..2a72a64 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -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 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 97481ea..57269f4 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -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 diff --git a/docs/reference/config.md b/docs/reference/config.md index 9111b69..b8243c7 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -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`) diff --git a/skillopt/model/codex_harness.py b/skillopt/model/codex_harness.py index bbc0512..6bbb093 100644 --- a/skillopt/model/codex_harness.py +++ b/skillopt/model/codex_harness.py @@ -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( diff --git a/tests/test_cursor_exec_backend.py b/tests/test_cursor_exec_backend.py index a1c1a96..3fea88f 100644 --- a/tests/test_cursor_exec_backend.py +++ b/tests/test_cursor_exec_backend.py @@ -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,