fix(agent): stop on workspace violations from tool errors
Treat workspace and safety guard failures as fatal regardless of whether they arrive from tool preparation, returned tool output, or raised exceptions. Made-with: Cursor
This commit is contained in:
@@ -764,6 +764,15 @@ class AgentRunner:
|
|||||||
"status": "error",
|
"status": "error",
|
||||||
"detail": prep_error.split(": ", 1)[-1][:120],
|
"detail": prep_error.split(": ", 1)[-1][:120],
|
||||||
}
|
}
|
||||||
|
if self._is_workspace_violation(prep_error):
|
||||||
|
logger.warning(
|
||||||
|
"Tool {} blocked by workspace/safety guard during preparation; aborting turn: {}",
|
||||||
|
tool_call.name,
|
||||||
|
prep_error.replace("\n", " ").strip()[:200],
|
||||||
|
)
|
||||||
|
event["detail"] = ("workspace_violation: "
|
||||||
|
+ prep_error.replace("\n", " ").strip())[:160]
|
||||||
|
return prep_error, event, RuntimeError(prep_error)
|
||||||
return prep_error + hint, event, RuntimeError(prep_error) if spec.fail_on_tool_error else None
|
return prep_error + hint, event, RuntimeError(prep_error) if spec.fail_on_tool_error else None
|
||||||
try:
|
try:
|
||||||
if tool is not None:
|
if tool is not None:
|
||||||
@@ -781,6 +790,15 @@ class AgentRunner:
|
|||||||
if isinstance(exc, AskUserInterrupt):
|
if isinstance(exc, AskUserInterrupt):
|
||||||
event["status"] = "waiting"
|
event["status"] = "waiting"
|
||||||
return "", event, exc
|
return "", event, exc
|
||||||
|
if self._is_workspace_violation(str(exc)):
|
||||||
|
logger.warning(
|
||||||
|
"Tool {} blocked by workspace/safety guard; aborting turn: {}",
|
||||||
|
tool_call.name,
|
||||||
|
str(exc).replace("\n", " ").strip()[:200],
|
||||||
|
)
|
||||||
|
event["detail"] = ("workspace_violation: "
|
||||||
|
+ str(exc).replace("\n", " ").strip())[:160]
|
||||||
|
return f"Error: {type(exc).__name__}: {exc}", event, exc
|
||||||
if spec.fail_on_tool_error:
|
if spec.fail_on_tool_error:
|
||||||
return f"Error: {type(exc).__name__}: {exc}", event, exc
|
return f"Error: {type(exc).__name__}: {exc}", event, exc
|
||||||
return f"Error: {type(exc).__name__}: {exc}", event, None
|
return f"Error: {type(exc).__name__}: {exc}", event, None
|
||||||
|
|||||||
@@ -312,6 +312,46 @@ async def test_runner_returns_structured_tool_error():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_runner_stops_on_workspace_violation_without_fail_on_tool_error():
|
||||||
|
from nanobot.agent.runner import AgentRunSpec, AgentRunner
|
||||||
|
|
||||||
|
provider = MagicMock()
|
||||||
|
provider.chat_with_retry = AsyncMock(side_effect=[
|
||||||
|
LLMResponse(
|
||||||
|
content="working",
|
||||||
|
tool_calls=[ToolCallRequest(id="call_1", name="read_file", arguments={"path": "/tmp/outside.md"})],
|
||||||
|
),
|
||||||
|
LLMResponse(content="should not continue", tool_calls=[]),
|
||||||
|
])
|
||||||
|
tools = MagicMock()
|
||||||
|
tools.get_definitions.return_value = []
|
||||||
|
tools.execute = AsyncMock(
|
||||||
|
side_effect=PermissionError("Path /tmp/outside.md is outside allowed directory /workspace")
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = AgentRunner(provider)
|
||||||
|
|
||||||
|
result = await runner.run(AgentRunSpec(
|
||||||
|
initial_messages=[],
|
||||||
|
tools=tools,
|
||||||
|
model="test-model",
|
||||||
|
max_iterations=2,
|
||||||
|
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
|
||||||
|
))
|
||||||
|
|
||||||
|
assert provider.chat_with_retry.await_count == 1
|
||||||
|
assert result.stop_reason == "tool_error"
|
||||||
|
assert "outside allowed directory" in (result.error or "")
|
||||||
|
assert result.tool_events == [
|
||||||
|
{
|
||||||
|
"name": "read_file",
|
||||||
|
"status": "error",
|
||||||
|
"detail": "workspace_violation: Path /tmp/outside.md is outside allowed directory /workspace",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_runner_persists_large_tool_results_for_follow_up_calls(tmp_path):
|
async def test_runner_persists_large_tool_results_for_follow_up_calls(tmp_path):
|
||||||
from nanobot.agent.runner import AgentRunSpec, AgentRunner
|
from nanobot.agent.runner import AgentRunSpec, AgentRunner
|
||||||
|
|||||||
Reference in New Issue
Block a user