diff --git a/TODO.md b/TODO.md index 20a8f88..9e12e4f 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,7 @@ ### P0 (Critical) - [ ] `BUG`: True isolated runner flow: clone/fetch/checkout PR branch inside the ephemeral container itself, not on host before prompt generation. -- [ ] `BUG`: Remove host-side fallback path for review execution, or gate it behind explicit `ALLOW_HOST_FALLBACK=false` by default so isolation cannot be bypassed silently. +- [x] `BUG`: Remove host-side fallback path for review execution, or gate it behind explicit `ALLOW_HOST_FALLBACK=false` by default so isolation cannot be bypassed silently. - [x] `BUG`: Enforce `.codex-review.yml` `enabled=false` at runtime (currently loaded but not enforced). - [x] `BUG`: Remove `.codex-review.yml` fix policy (`commands.allow_fix`) and rely on global `ENABLE_FIX_COMMANDS`. - [x] `BUG`: Add stuck-job recovery for `running` jobs (lease timeout + requeue/fail) so one crashed worker does not deadlock the queue. diff --git a/src/gitea_codex_bot/workers/container_runner.py b/src/gitea_codex_bot/workers/container_runner.py index 281ecf5..39a5988 100644 --- a/src/gitea_codex_bot/workers/container_runner.py +++ b/src/gitea_codex_bot/workers/container_runner.py @@ -14,7 +14,7 @@ from typing import Any from gitea_codex_bot.config import Settings from gitea_codex_bot.services.gitea import GiteaClient from gitea_codex_bot.services.repo_config import RepoReviewConfig -from gitea_codex_bot.services.reviewer import normalize_review_result, prepare_review_prompt, run_review_for_pr +from gitea_codex_bot.services.reviewer import normalize_review_result, prepare_review_prompt from gitea_codex_bot.types import ParsedCommand CONTAINER_CODEX_HOME = "/root/.codex" @@ -52,11 +52,8 @@ def run_review_ephemeral( parsed["_meta"] = _extract_result_meta_from_codex_stdout(completed.stdout, settings) return normalize_review_result(parsed), repo_cfg except Exception as exc: - if settings.codex_auth_mode == "chatgpt": - logger.warning("Ephemeral chatgpt runner failed, skipping API-key fallback: %s", exc) - return _chatgpt_runner_failure_result(exc), repo_cfg - result, _repo_cfg = run_review_for_pr(settings, gitea, repo, pr_number, command) - return result, _repo_cfg + logger.warning("Ephemeral runner failed without host fallback: %s", exc) + return _ephemeral_runner_failure_result(exc, settings.codex_auth_mode), repo_cfg def _build_install_and_run_command(settings: Settings) -> str: @@ -121,9 +118,10 @@ def _build_docker_command(settings: Settings, *, container_name: str, install_an return cmd -def _chatgpt_runner_failure_result(exc: Exception) -> dict[str, Any]: +def _ephemeral_runner_failure_result(exc: Exception, auth_mode: str) -> dict[str, Any]: message = str(exc).strip() or exc.__class__.__name__ - summary = f"ChatGPT auth runner failed before review execution. Error: {message}" + mode_label = "ChatGPT auth" if auth_mode == "chatgpt" else "API-key auth" + summary = f"{mode_label} runner failed before review execution. Error: {message}" return { "verdict": "has_issues", "confidence": 0.6, @@ -134,7 +132,7 @@ def _chatgpt_runner_failure_result(exc: Exception) -> dict[str, Any]: "file": "runner", "line_start": 1, "line_end": 1, - "title": "Ephemeral chatgpt review runner failed", + "title": "Ephemeral review runner failed", "body": message, "suggestion": "Check ephemeral runner logs for model/auth/network issues, then rerun @codex review.", } diff --git a/tests/test_container_runner.py b/tests/test_container_runner.py index 0d41def..dac7d6d 100644 --- a/tests/test_container_runner.py +++ b/tests/test_container_runner.py @@ -117,11 +117,6 @@ def test_run_review_ephemeral_chatgpt_does_not_fallback_to_api_key_path( lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("docker unavailable")), ) - def _api_fallback_should_not_run(*_args, **_kwargs): - raise AssertionError("API-key fallback should not run in chatgpt mode") - - monkeypatch.setattr("gitea_codex_bot.workers.container_runner.run_review_for_pr", _api_fallback_should_not_run) - from gitea_codex_bot.types import ParsedCommand result, _repo_cfg = run_review_ephemeral( @@ -135,6 +130,33 @@ def test_run_review_ephemeral_chatgpt_does_not_fallback_to_api_key_path( assert "ChatGPT auth runner failed" in result["summary"] +def test_run_review_ephemeral_api_key_mode_does_not_fallback_to_host(monkeypatch: pytest.MonkeyPatch) -> None: + get_settings.cache_clear() + settings = get_settings() + + monkeypatch.setattr( + "gitea_codex_bot.workers.container_runner.prepare_review_prompt", + lambda *_args, **_kwargs: ("prompt", {"diff": ""}, object()), + ) + monkeypatch.setattr("gitea_codex_bot.workers.container_runner.GiteaClient", lambda _settings: object()) + monkeypatch.setattr( + "gitea_codex_bot.workers.container_runner.subprocess.run", + lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("docker unavailable")), + ) + + from gitea_codex_bot.types import ParsedCommand + + result, _repo_cfg = run_review_ephemeral( + settings, + repo="acme/repo", + pr_number=1, + command=ParsedCommand(name="review", raw="@codex review"), + ) + + assert result["verdict"] == "has_issues" + assert "API-key auth runner failed" in result["summary"] + + def test_parse_codex_exec_stdout_from_stream_item_text_json() -> None: stdout = '\n'.join( [