diff --git a/sdk/python/agentfield/harness/_runner.py b/sdk/python/agentfield/harness/_runner.py index 5e4a8d918..f83ab1d8e 100644 --- a/sdk/python/agentfield/harness/_runner.py +++ b/sdk/python/agentfield/harness/_runner.py @@ -294,15 +294,18 @@ async def run( # project_dir, or cwd when project_dir is unset. Besides keeping the file # inside the agent root, this prevents concurrent runs sharing one cwd # from overwriting or deleting each other's fixed output filename. - resolved_project_dir = options.get("project_dir") - if isinstance(resolved_project_dir, str) and resolved_project_dir: - base_dir = resolved_project_dir - else: - base_dir = resolved_cwd - os.makedirs(base_dir, exist_ok=True) - temp_output_dir: Optional[str] = tempfile.mkdtemp( - prefix=".agentfield-out-", dir=base_dir - ) + # A schema-free run never writes or reads that file, so it must not need + # a writable root either: allocating one unconditionally aborted + # text-only permission_mode="plan" dispatches before the provider ran. + temp_output_dir: Optional[str] = None + if schema is not None: + resolved_project_dir = options.get("project_dir") + if isinstance(resolved_project_dir, str) and resolved_project_dir: + base_dir = resolved_project_dir + else: + base_dir = resolved_cwd + os.makedirs(base_dir, exist_ok=True) + temp_output_dir = tempfile.mkdtemp(prefix=".agentfield-out-", dir=base_dir) output_dir = temp_output_dir # schema_mode selects how the agent is asked to produce the output: diff --git a/sdk/python/tests/test_harness_runner.py b/sdk/python/tests/test_harness_runner.py index adce2b751..27b14e358 100644 --- a/sdk/python/tests/test_harness_runner.py +++ b/sdk/python/tests/test_harness_runner.py @@ -240,6 +240,39 @@ async def test_run_without_schema_returns_plain_harness_result(tmp_path): assert result.session_id == "sess-1" +@pytest.mark.asyncio +async def test_run_without_schema_does_not_require_writable_project_dir(tmp_path): + """A schema-free run must reach the provider without allocating artifacts. + + The per-run ``.agentfield-out-*`` directory only exists to hold the schema + output file (#684, #891). Allocating it unconditionally aborted a text-only + ``permission_mode="plan"`` run during setup, before the provider was ever + dispatched, whenever the project root could not be written to. + """ + blocker = tmp_path / "blocker.txt" + blocker.write_text("not a directory", encoding="utf-8") + # Cannot be created on any platform: its parent is a regular file. + unwritable_root = blocker / "project" + + provider = MockProvider([RawResult(result="plan text")]) + runner = HarnessRunner() + + with patch("agentfield.harness._runner.build_provider", return_value=provider): + result = await runner.run( + "hello", + provider="codex", + permission_mode="plan", + cwd=str(tmp_path), + project_dir=str(unwritable_root), + ) + + assert provider.call_count == 1, "schema-free run must dispatch the provider" + assert result.is_error is False + assert result.result == "plan text" + assert result.parsed is None + assert list(tmp_path.glob(".agentfield-out-*")) == [] + + @pytest.mark.asyncio async def test_run_with_schema_injects_prompt_suffix_and_parses_output(tmp_path): provider = FileWritingProvider(json.dumps({"name": "ok", "count": 1}))