diff --git a/docs/reference/config.md b/docs/reference/config.md index cdf44e61..639f99bb 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -65,8 +65,11 @@ defaults to `claude` and can be overridden with `CLAUDE_CLI_BIN`. | `model.qwen_chat_*` | Shared `base_url`, `api_key`, `temperature`, `timeout_seconds`, `max_tokens`, and `enable_thinking` | | `model.optimizer_qwen_chat_*` / `model.target_qwen_chat_*` | Per-role Qwen overrides | | `model.minimax_*` | MiniMax `base_url`, `api_key`, shared `minimax_model`, `temperature`, `max_tokens`, and `enable_thinking`; `minimax_model` applies when MiniMax is the target | -| `model.codex_exec_*` | Codex path, sandbox, profile, SDK mode, reasoning, network/search, and approval policy | +| `model.codex_exec_*` | Codex path (`model.codex_cli_bin` and `model.codex_path` aliases accepted), sandbox (`model.sandbox` and `model.codex_sandbox` aliases accepted), profile, SDK mode, reasoning, network/search, and approval policy | | `model.claude_code_exec_*` | Claude path, profile, SDK mode, effort, and thinking-token cap | + +> [!WARNING] +> When setting the Codex sandbox mode to `danger-full-access` (or using the `CODEX_SANDBOX_MODE="danger-full-access"` environment alias), the model is granted complete and unrestricted write access to the host filesystem. Use this mode with extreme caution, preferably only within an isolated container environment. | `model.cursor_exec_path` | Cursor Agent executable path; default `cursor-agent` | | `model.cursor_exec_sandbox` | Cursor sandbox mode: `enabled` (default) or `disabled`; file-edit rollouts require `enabled` | | `model.copilot_exec_path` | GitHub Copilot CLI executable path; default `copilot` | diff --git a/scripts/eval_only.py b/scripts/eval_only.py index b5f40ce2..76e74d00 100644 --- a/scripts/eval_only.py +++ b/scripts/eval_only.py @@ -488,15 +488,15 @@ def _set_role(key: str, value: str) -> None: set_optimizer_deployment(cfg.get("optimizer_model", default_model_for_backend(backend))) set_target_deployment(cfg.get("target_model", default_model_for_backend(backend))) configure_codex_exec( - path=cfg.get("codex_exec_path", "codex"), - sandbox=cfg.get("codex_exec_sandbox", "workspace-write"), - profile=cfg.get("codex_exec_profile", ""), - full_auto=cfg.get("codex_exec_full_auto", False), - reasoning_effort=cfg.get("codex_exec_reasoning_effort", "none"), - use_sdk=cfg.get("codex_exec_use_sdk", None), - network_access=cfg.get("codex_exec_network_access", False), - web_search=cfg.get("codex_exec_web_search", False), - approval_policy=cfg.get("codex_exec_approval_policy", "never"), + path=cfg.get("codex_exec_path") or cfg.get("codex_path") or cfg.get("codex_cli_bin") or cfg.get("codex_bin") or None, + sandbox=cfg.get("codex_exec_sandbox") or cfg.get("sandbox") or cfg.get("codex_sandbox") or None, + profile=cfg.get("codex_exec_profile") or None, + full_auto=cfg.get("codex_exec_full_auto"), + reasoning_effort=cfg.get("codex_exec_reasoning_effort") or None, + use_sdk=cfg.get("codex_exec_use_sdk"), + network_access=cfg.get("codex_exec_network_access"), + web_search=cfg.get("codex_exec_web_search"), + approval_policy=cfg.get("codex_exec_approval_policy") or None, ) configure_claude_code_exec( path=cfg.get("claude_code_exec_path", "claude"), diff --git a/skillopt/config.py b/skillopt/config.py index acc7d5b3..2f70128b 100644 --- a/skillopt/config.py +++ b/skillopt/config.py @@ -38,7 +38,11 @@ "model.rewrite_reasoning_effort": "rewrite_reasoning_effort", "model.rewrite_max_completion_tokens": "rewrite_max_completion_tokens", "model.codex_exec_path": "codex_exec_path", + "model.codex_path": "codex_exec_path", + "model.codex_cli_bin": "codex_exec_path", "model.codex_exec_sandbox": "codex_exec_sandbox", + "model.codex_sandbox": "codex_exec_sandbox", + "model.sandbox": "codex_exec_sandbox", "model.codex_exec_profile": "codex_exec_profile", "model.codex_exec_full_auto": "codex_exec_full_auto", "model.codex_exec_reasoning_effort": "codex_exec_reasoning_effort", diff --git a/skillopt/engine/trainer.py b/skillopt/engine/trainer.py index 18256033..63a08a8d 100644 --- a/skillopt/engine/trainer.py +++ b/skillopt/engine/trainer.py @@ -747,15 +747,15 @@ def _build_eval_env(split: str, env_num: int, seed: int): set_optimizer_deployment(cfg["optimizer_model"]) set_target_deployment(cfg["target_model"]) configure_codex_exec( - path=cfg.get("codex_exec_path", "codex"), - sandbox=cfg.get("codex_exec_sandbox", "workspace-write"), - profile=cfg.get("codex_exec_profile", ""), - full_auto=cfg.get("codex_exec_full_auto", False), - reasoning_effort=cfg.get("codex_exec_reasoning_effort", "none"), - use_sdk=cfg.get("codex_exec_use_sdk", None), - network_access=cfg.get("codex_exec_network_access", False), - web_search=cfg.get("codex_exec_web_search", False), - approval_policy=cfg.get("codex_exec_approval_policy", "never"), + path=cfg.get("codex_exec_path") or cfg.get("codex_path") or cfg.get("codex_cli_bin") or cfg.get("codex_bin") or None, + sandbox=cfg.get("codex_exec_sandbox") or cfg.get("sandbox") or cfg.get("codex_sandbox") or None, + profile=cfg.get("codex_exec_profile") or None, + full_auto=cfg.get("codex_exec_full_auto"), + reasoning_effort=cfg.get("codex_exec_reasoning_effort") or None, + use_sdk=cfg.get("codex_exec_use_sdk"), + network_access=cfg.get("codex_exec_network_access"), + web_search=cfg.get("codex_exec_web_search"), + approval_policy=cfg.get("codex_exec_approval_policy") or None, ) configure_claude_code_exec( path=cfg.get("claude_code_exec_path", "claude"), diff --git a/skillopt/model/backend_config.py b/skillopt/model/backend_config.py index dbca7694..e199fd54 100644 --- a/skillopt/model/backend_config.py +++ b/skillopt/model/backend_config.py @@ -15,8 +15,8 @@ def _parse_bool(value: str | None, default: bool) -> bool: OPTIMIZER_BACKEND = normalize_backend_name(os.environ.get("OPTIMIZER_BACKEND", "openai_chat")) TARGET_BACKEND = normalize_backend_name(os.environ.get("TARGET_BACKEND", "openai_chat")) -CODEX_EXEC_PATH = os.environ.get("CODEX_EXEC_PATH", "codex") -CODEX_EXEC_SANDBOX = os.environ.get("CODEX_EXEC_SANDBOX", "workspace-write") +CODEX_EXEC_PATH = os.environ.get("CODEX_EXEC_PATH") or os.environ.get("CODEX_CLI_BIN") or os.environ.get("CODEX_PATH") or "codex" +CODEX_EXEC_SANDBOX = os.environ.get("CODEX_EXEC_SANDBOX") or os.environ.get("CODEX_SANDBOX_MODE") or os.environ.get("CODEX_SANDBOX") or "workspace-write" CODEX_EXEC_PROFILE = os.environ.get("CODEX_EXEC_PROFILE", "") CODEX_EXEC_FULL_AUTO = _parse_bool(os.environ.get("CODEX_EXEC_FULL_AUTO"), True) CODEX_EXEC_REASONING_EFFORT = os.environ.get("CODEX_EXEC_REASONING_EFFORT", "none") @@ -133,9 +133,11 @@ def configure_codex_exec( if path is not None: CODEX_EXEC_PATH = str(path).strip() or "codex" os.environ["CODEX_EXEC_PATH"] = CODEX_EXEC_PATH + os.environ["CODEX_CLI_BIN"] = CODEX_EXEC_PATH if sandbox is not None: CODEX_EXEC_SANDBOX = str(sandbox).strip() or "workspace-write" os.environ["CODEX_EXEC_SANDBOX"] = CODEX_EXEC_SANDBOX + os.environ["CODEX_SANDBOX_MODE"] = CODEX_EXEC_SANDBOX if profile is not None: CODEX_EXEC_PROFILE = str(profile).strip() os.environ["CODEX_EXEC_PROFILE"] = CODEX_EXEC_PROFILE diff --git a/skillopt/model/codex_backend.py b/skillopt/model/codex_backend.py index 09149093..b9447983 100644 --- a/skillopt/model/codex_backend.py +++ b/skillopt/model/codex_backend.py @@ -21,9 +21,9 @@ ) -CODEX_BIN = os.environ.get("CODEX_CLI_BIN", "codex") -CODEX_PROFILE = os.environ.get("CODEX_PROFILE", "review") -CODEX_SANDBOX_MODE = os.environ.get("CODEX_SANDBOX_MODE", "read-only") +CODEX_BIN = os.environ.get("CODEX_EXEC_PATH") or os.environ.get("CODEX_CLI_BIN") or "codex" +CODEX_PROFILE = os.environ.get("CODEX_EXEC_PROFILE") or os.environ.get("CODEX_PROFILE") or "review" +CODEX_SANDBOX_MODE = os.environ.get("CODEX_EXEC_SANDBOX") or os.environ.get("CODEX_SANDBOX_MODE") or "read-only" OPTIMIZER_DEPLOYMENT = os.environ.get("OPTIMIZER_DEPLOYMENT", "gpt-4o") TARGET_DEPLOYMENT = os.environ.get("TARGET_DEPLOYMENT", "gpt-4o") diff --git a/skillopt/model/codex_harness.py b/skillopt/model/codex_harness.py index fad3983c..be5b0d93 100644 --- a/skillopt/model/codex_harness.py +++ b/skillopt/model/codex_harness.py @@ -955,10 +955,12 @@ def _run_codex_cli_exec( cmd.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"']) actual_full_auto = bool(config.get("full_auto", True)) if full_auto is None else bool(full_auto) actual_sandbox = str(sandbox or config["sandbox"]) - if actual_full_auto: - cmd.append("--full-auto") - else: - cmd.extend(["--sandbox", actual_sandbox]) + cmd.extend(["--sandbox", actual_sandbox]) + + approval_policy = str(config.get("approval_policy", "never")) + if not actual_full_auto and approval_policy == "never": + approval_policy = "ask" + cmd.extend(["--approval-policy", approval_policy]) if model: cmd.extend(["-m", model]) for data_dir in data_dirs or []: diff --git a/tests/test_codex_config_aliases.py b/tests/test_codex_config_aliases.py new file mode 100644 index 00000000..d599ac60 --- /dev/null +++ b/tests/test_codex_config_aliases.py @@ -0,0 +1,85 @@ +import os +from unittest import mock + +from skillopt.config import flatten_config +from skillopt.model.backend_config import configure_codex_exec + + +def test_codex_config_aliases_flatten_config(): + structured_cfg = { + "model": { + "sandbox": "danger-full-access", + "codex_cli_bin": "/custom/path/codex" + } + } + flat = flatten_config(structured_cfg) + assert flat["codex_exec_sandbox"] == "danger-full-access" + assert flat["codex_exec_path"] == "/custom/path/codex" + + +def test_codex_backend_env_aliases(monkeypatch): + monkeypatch.setenv("CODEX_CLI_BIN", "/env/path/codex") + monkeypatch.setenv("CODEX_SANDBOX_MODE", "danger-full-access-env") + + import importlib + + import skillopt.model.backend_config + import skillopt.model.codex_backend + + importlib.reload(skillopt.model.backend_config) + importlib.reload(skillopt.model.codex_backend) + + assert skillopt.model.codex_backend.CODEX_BIN == "/env/path/codex" + assert skillopt.model.codex_backend.CODEX_SANDBOX_MODE == "danger-full-access-env" + + +def test_configure_codex_exec_sets_aliases(monkeypatch): + monkeypatch.setattr("skillopt.model.backend_config.CODEX_EXEC_PATH", "") + monkeypatch.setattr("skillopt.model.backend_config.CODEX_EXEC_SANDBOX", "") + monkeypatch.setattr("skillopt.model.backend_config.CODEX_EXEC_APPROVAL_POLICY", "") + + configure_codex_exec(path="/configured/codex", sandbox="configured-sandbox") + + assert os.environ["CODEX_EXEC_PATH"] == "/configured/codex" + assert os.environ["CODEX_CLI_BIN"] == "/configured/codex" + + assert os.environ["CODEX_EXEC_SANDBOX"] == "configured-sandbox" + assert os.environ["CODEX_SANDBOX_MODE"] == "configured-sandbox" + + +@mock.patch("skillopt.model.codex_harness.subprocess.run") +def test_codex_harness_sandbox_passed_with_full_auto(mock_run, monkeypatch): + from skillopt.model.codex_harness import _run_codex_cli_exec + + # Isolate global config + monkeypatch.setattr("skillopt.model.backend_config.CODEX_EXEC_SANDBOX", "") + monkeypatch.setattr("skillopt.model.backend_config.CODEX_EXEC_FULL_AUTO", False) + monkeypatch.setattr("skillopt.model.backend_config.CODEX_EXEC_APPROVAL_POLICY", "ask") + + configure_codex_exec(sandbox="danger-full-access", full_auto=True, approval_policy="never") + + mock_proc = mock.MagicMock() + mock_proc.returncode = 0 + mock_proc.stdout = "" + mock_proc.stderr = "" + mock_run.return_value = mock_proc + + with mock.patch("skillopt.model.codex_harness._persist_codex_artifacts"), \ + mock.patch("skillopt.model.azure_openai.tracker.record", create=True): + _run_codex_cli_exec( + work_dir=".", + prompt="test", + model="test-model", + timeout=10, + ) + + mock_run.assert_called_once() + args = mock_run.call_args[0][0] + + assert "--sandbox" in args + sandbox_idx = args.index("--sandbox") + assert args[sandbox_idx + 1] == "danger-full-access" + + assert "--approval-policy" in args + approval_idx = args.index("--approval-policy") + assert args[approval_idx + 1] == "never"