mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-14 23:03:59 +00:00
fix(exec): uncap config exec timeout; 0 means no limit (#3595)
This commit is contained in:
parent
5937236f9d
commit
5b71f61f55
@ -53,14 +53,15 @@ class _ExecSession:
|
|||||||
process: asyncio.subprocess.Process,
|
process: asyncio.subprocess.Process,
|
||||||
command: str,
|
command: str,
|
||||||
cwd: str,
|
cwd: str,
|
||||||
timeout: int,
|
timeout: int | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
self.process = process
|
self.process = process
|
||||||
self.command = command
|
self.command = command
|
||||||
self.cwd = cwd
|
self.cwd = cwd
|
||||||
self.started_at = time.monotonic()
|
self.started_at = time.monotonic()
|
||||||
self.deadline = time.monotonic() + timeout
|
# timeout None/0 means no limit; an infinite deadline is never reached.
|
||||||
|
self.deadline = time.monotonic() + timeout if timeout else float("inf")
|
||||||
self.last_access = time.monotonic()
|
self.last_access = time.monotonic()
|
||||||
self._chunks: list[str] = []
|
self._chunks: list[str] = []
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
@ -169,7 +170,7 @@ class ExecSessionManager:
|
|||||||
command: str,
|
command: str,
|
||||||
cwd: str,
|
cwd: str,
|
||||||
env: dict[str, str],
|
env: dict[str, str],
|
||||||
timeout: int,
|
timeout: int | None,
|
||||||
shell_program: str | None,
|
shell_program: str | None,
|
||||||
login: bool,
|
login: bool,
|
||||||
yield_time_ms: int,
|
yield_time_ms: int,
|
||||||
|
|||||||
@ -46,7 +46,7 @@ _WORKSPACE_BOUNDARY_NOTE = (
|
|||||||
class ExecToolConfig(Base):
|
class ExecToolConfig(Base):
|
||||||
"""Shell exec tool configuration."""
|
"""Shell exec tool configuration."""
|
||||||
enable: bool = True
|
enable: bool = True
|
||||||
timeout: int = 60
|
timeout: int = Field(default=60, ge=0) # Hard timeout (s); 0 = no limit. Not capped by the per-call max.
|
||||||
path_append: str = ""
|
path_append: str = ""
|
||||||
sandbox: str = ""
|
sandbox: str = ""
|
||||||
allowed_env_keys: list[str] = Field(default_factory=list)
|
allowed_env_keys: list[str] = Field(default_factory=list)
|
||||||
@ -59,7 +59,7 @@ class _PreparedCommand:
|
|||||||
command: str
|
command: str
|
||||||
cwd: str
|
cwd: str
|
||||||
env: dict[str, str]
|
env: dict[str, str]
|
||||||
timeout: int
|
timeout: int | None
|
||||||
shell_program: str | None
|
shell_program: str | None
|
||||||
login: bool
|
login: bool
|
||||||
|
|
||||||
@ -324,6 +324,20 @@ class ExecTool(Tool):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return f"Error executing command: {exc}"
|
return f"Error executing command: {exc}"
|
||||||
|
|
||||||
|
def _resolve_timeout(self, timeout: int | None) -> int | None:
|
||||||
|
"""Resolve the effective hard timeout in seconds (None = no limit).
|
||||||
|
|
||||||
|
A per-call timeout supplied by the model stays capped at _MAX_TIMEOUT so
|
||||||
|
the LLM cannot request unbounded execution. The config-level default
|
||||||
|
(self.timeout) may exceed that cap, and 0 disables the limit entirely
|
||||||
|
for trusted long-running tasks (#3595).
|
||||||
|
"""
|
||||||
|
if timeout:
|
||||||
|
return min(timeout, self._MAX_TIMEOUT)
|
||||||
|
if self.timeout and self.timeout > 0:
|
||||||
|
return self.timeout
|
||||||
|
return None
|
||||||
|
|
||||||
def _prepare_command(
|
def _prepare_command(
|
||||||
self,
|
self,
|
||||||
command: str,
|
command: str,
|
||||||
@ -369,7 +383,7 @@ class ExecTool(Tool):
|
|||||||
command = wrap_command(self.sandbox, command, workspace, cwd)
|
command = wrap_command(self.sandbox, command, workspace, cwd)
|
||||||
cwd = str(Path(workspace).resolve())
|
cwd = str(Path(workspace).resolve())
|
||||||
|
|
||||||
effective_timeout = min(timeout or self.timeout, self._MAX_TIMEOUT)
|
effective_timeout = self._resolve_timeout(timeout)
|
||||||
env = self._build_env()
|
env = self._build_env()
|
||||||
|
|
||||||
if self.path_append:
|
if self.path_append:
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import sys
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from nanobot.agent.tools import (
|
from nanobot.agent.tools import (
|
||||||
ArraySchema,
|
ArraySchema,
|
||||||
@ -16,7 +17,7 @@ from nanobot.agent.tools import (
|
|||||||
)
|
)
|
||||||
from nanobot.agent.tools.base import Tool
|
from nanobot.agent.tools.base import Tool
|
||||||
from nanobot.agent.tools.registry import ToolRegistry
|
from nanobot.agent.tools.registry import ToolRegistry
|
||||||
from nanobot.agent.tools.shell import ExecTool
|
from nanobot.agent.tools.shell import ExecTool, ExecToolConfig
|
||||||
from nanobot.security.network import configure_ssrf_whitelist
|
from nanobot.security.network import configure_ssrf_whitelist
|
||||||
|
|
||||||
|
|
||||||
@ -663,6 +664,26 @@ async def test_exec_timeout_capped_at_max() -> None:
|
|||||||
assert "Exit code: 0" in result
|
assert "Exit code: 0" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_exec_config_timeout_uncapped_and_zero() -> None:
|
||||||
|
"""Config timeout is no longer capped at 600 and accepts 0 = no limit (#3595)."""
|
||||||
|
assert ExecToolConfig(timeout=0).timeout == 0
|
||||||
|
assert ExecToolConfig(timeout=3600).timeout == 3600
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
ExecToolConfig(timeout=-1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_timeout_config_uncapped_and_unlimited() -> None:
|
||||||
|
"""Config timeout drives the hard timeout uncapped; 0 means no limit (#3595)."""
|
||||||
|
assert ExecTool(timeout=3600)._resolve_timeout(None) == 3600
|
||||||
|
assert ExecTool(timeout=0)._resolve_timeout(None) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_timeout_per_call_still_capped() -> None:
|
||||||
|
"""Per-call (LLM) timeout stays capped at _MAX_TIMEOUT even with unlimited config."""
|
||||||
|
assert ExecTool(timeout=0)._resolve_timeout(9999) == ExecTool._MAX_TIMEOUT
|
||||||
|
assert ExecTool(timeout=60)._resolve_timeout(120) == 120
|
||||||
|
|
||||||
|
|
||||||
# --- _resolve_type and nullable param tests ---
|
# --- _resolve_type and nullable param tests ---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user