diff --git a/backend/VERSION b/backend/VERSION index 3d0e62313..703a816ae 100644 --- a/backend/VERSION +++ b/backend/VERSION @@ -1 +1 @@ -1.11.4 +1.11.4-fix.1 diff --git a/backend/app/services/agent_runtime/verification.py b/backend/app/services/agent_runtime/verification.py index 65bbc5c6b..b5f7e087f 100644 --- a/backend/app/services/agent_runtime/verification.py +++ b/backend/app/services/agent_runtime/verification.py @@ -1010,7 +1010,7 @@ async def verify( ) continue return VerificationResult( - outcome="repair", + outcome="fail", reason="an artifact/evidence reference is not readable", details={ "code": "tool_reference_unreadable", diff --git a/backend/app/services/sandbox/local/subprocess_backend.py b/backend/app/services/sandbox/local/subprocess_backend.py index 16c351d3d..2d0557b58 100644 --- a/backend/app/services/sandbox/local/subprocess_backend.py +++ b/backend/app/services/sandbox/local/subprocess_backend.py @@ -636,17 +636,17 @@ def is_allowed(relative_path: Path) -> bool: continue except OSError: continue - if file_path.suffix.lower() in banned_suffixes: - logger.warning( - f"[Sandbox Gateway] Blocked banned file extension: {rel_path}" - ) - continue if target_file is not None: try: if file_path.read_bytes() == target_file.read_bytes(): continue except OSError: pass + if file_path.suffix.lower() in banned_suffixes: + logger.warning( + f"[Sandbox Gateway] Blocked banned file extension: {rel_path}" + ) + continue publication_candidates[rel_path] = file_path deletion_candidates = { diff --git a/backend/tests/test_agent_runtime_node_executor.py b/backend/tests/test_agent_runtime_node_executor.py index e95106b79..188caa740 100644 --- a/backend/tests/test_agent_runtime_node_executor.py +++ b/backend/tests/test_agent_runtime_node_executor.py @@ -1691,6 +1691,29 @@ async def test_verification_repairs_are_bounded() -> None: assert verifier.calls == ["first", "second"] +@pytest.mark.asyncio +async def test_verification_integrity_failure_does_not_reenter_model() -> None: + run_id = uuid.uuid4() + model = ModelService(ModelStepResult(intent="finish", finish_content="done")) + verifier = Verifier( + VerificationResult( + outcome="fail", + reason="an artifact/evidence reference is not readable", + details={"code": "tool_reference_unreadable"}, + ) + ) + executor = _executor(model, verifier=verifier, max_verification_repairs=10) + + result = await _invoke(run_id, executor) + + lifecycle = result["lifecycle"] + assert lifecycle["status"] == "failed" + assert lifecycle["reason"] == "an artifact/evidence reference is not readable" + assert lifecycle.get("verification_attempt_count", 0) == 0 + assert model.calls == 1 + assert verifier.calls == ["done"] + + @pytest.mark.asyncio async def test_task_completion_gate_exhaustion_delivers_latest_candidate() -> None: run_id = uuid.uuid4() diff --git a/backend/tests/test_agent_runtime_reference_reader.py b/backend/tests/test_agent_runtime_reference_reader.py index 97986b938..7baf58d20 100644 --- a/backend/tests/test_agent_runtime_reference_reader.py +++ b/backend/tests/test_agent_runtime_reference_reader.py @@ -516,7 +516,7 @@ async def test_production_verifier_and_finalizer_propagate_only_read_back_refs() @pytest.mark.asyncio -async def test_production_verifier_repairs_an_unreadable_current_run_reference() -> None: +async def test_production_verifier_fails_fast_on_an_unreadable_current_run_reference() -> None: tenant_id = uuid.uuid4() run_id = uuid.uuid4() agent_id = uuid.uuid4() @@ -553,7 +553,7 @@ async def test_production_verifier_repairs_an_unreadable_current_run_reference() context, "done", ) - assert result.outcome == "repair" + assert result.outcome == "fail" assert result.details == { "code": "tool_reference_unreadable", "reference": reference, diff --git a/backend/tests/test_sandbox_subprocess_backend.py b/backend/tests/test_sandbox_subprocess_backend.py index eece192e3..c0dbf0620 100644 --- a/backend/tests/test_sandbox_subprocess_backend.py +++ b/backend/tests/test_sandbox_subprocess_backend.py @@ -326,6 +326,31 @@ async def test_sandbox_output_sanitization(tmp_path: Path) -> None: assert not (target / "evil.sh").exists() +@pytest.mark.asyncio +async def test_sandbox_does_not_report_unchanged_skill_scripts_as_blocked( + tmp_path: Path, + monkeypatch, +) -> None: + staging = tmp_path / "staging" + target = tmp_path / "target" + script_path = Path("skills/skill-creator/scripts/package_skill.py") + for root in (staging, target): + path = root / script_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("print('package')", encoding="utf-8") + + warnings: list[str] = [] + monkeypatch.setattr(subprocess_backend.logger, "warning", warnings.append) + + await SubprocessBackend(SandboxConfig())._verify_and_merge_outputs( + staging, + target, + ) + + assert warnings == [] + assert (target / script_path).read_text(encoding="utf-8") == "print('package')" + + @pytest.mark.asyncio async def test_sandbox_quota_ignores_unchanged_materialized_files( tmp_path: Path, diff --git a/frontend/VERSION b/frontend/VERSION index 3d0e62313..703a816ae 100644 --- a/frontend/VERSION +++ b/frontend/VERSION @@ -1 +1 @@ -1.11.4 +1.11.4-fix.1