chore(runner): tighten workspace guard comments and Windows tests

Keep the workspace-boundary changes easier to review by trimming long explanatory comments down to short local notes. Also make the #3599 POSIX command regression skip on Windows and normalize workspace violation signatures to POSIX separators so the throttle tests are platform-stable.

Tests:
- uv run pytest tests/tools/test_exec_security.py tests/utils/test_workspace_violation_throttle.py -q
- uv run pytest -q

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xubin Ren
2026-05-04 01:18:39 +08:00
committed by Xubin Ren
co-authored by Cursor
parent b8406be215
commit 2a7433b7ec
4 changed files with 28 additions and 116 deletions
+6 -48
View File
@@ -12,12 +12,7 @@ from nanobot.utils.helpers import stringify_text_blocks
_MAX_REPEAT_EXTERNAL_LOOKUPS = 2
# Workspace-violation throttle: how many times the LLM is allowed to bump
# against the same outside-workspace target *within a single turn* before the
# tool result is escalated with a hard "stop trying to bypass the policy"
# instruction. Two free attempts give the model room to e.g. read_file then
# fall back to exec without immediately escalating; the third attempt at the
# same target is treated as a clear bypass loop.
# Third same-target workspace violation in a turn escalates to "stop retrying".
_MAX_REPEAT_WORKSPACE_VIOLATIONS = 2
EMPTY_FINAL_RESPONSE_MESSAGE = (
@@ -107,29 +102,7 @@ def repeated_external_lookup_error(
)
# --- Workspace-violation throttle --------------------------------------------
#
# When ``restrict_to_workspace`` is on and the LLM tries to read or exec
# something outside of the workspace, we want to *tell* the model that it
# hit a hard policy boundary -- not silently abort the whole turn and not
# allow it to spin forever swapping ``read_file`` for ``exec cat`` for
# ``python -c open(...)`` (the actual loop reported in #3493). The strategy
# is two-fold:
#
# 1. Each individual guard error already includes structured instructions
# that tell the model "don't try to bypass this; ask the user for help".
# 2. We additionally count how many times the *same outside target* has
# been refused within the current turn. After two free attempts the
# third refusal swaps in a much more forceful message that quotes the
# target path and explicitly orders the model to stop and surface the
# boundary back to the user. The model is still free to do something
# else (different target, different question) -- only the bypass loop
# is interrupted.
#
# This intentionally does *not* fatal-abort the turn: max_iterations and
# the empty-final-response retries already provide the ultimate ceiling
# for runaway loops, and aborting is what produced the silent-hang bug
# in #3605 in the first place.
# Workspace-boundary violations are soft errors, with per-target throttling.
_OUTSIDE_PATH_PATTERN = re.compile(r"(?:^|[\s|>'\"])((?:/[^\s\"'>;|<]+)|(?:~[^\s\"'>;|<]+))")
@@ -138,14 +111,7 @@ def workspace_violation_signature(
tool_name: str,
arguments: dict[str, Any],
) -> str | None:
"""Return a stable signature for the outside-workspace target a tool tried.
The signature is shared across tool names so that the LLM cannot bypass
the throttle by switching from ``read_file`` to ``exec cat`` to
``python -c open(...)`` against the same path. Returns ``None`` when
the call has no obvious outside target (e.g. SSRF rejections, deny
pattern hits, or tools whose argument shape we don't understand).
"""
"""Return a stable cross-tool signature for the outside-workspace target."""
for key in ("path", "file_path", "target", "source", "destination"):
val = arguments.get(key)
if isinstance(val, str) and val.strip():
@@ -167,9 +133,9 @@ def workspace_violation_signature(
def _normalize_violation_target(raw: str) -> str:
"""Normalize *raw* path so that equivalent spellings collide on the same key."""
try:
normalized = str(Path(raw).expanduser().resolve())
normalized = Path(raw).expanduser().resolve().as_posix()
except Exception:
normalized = raw
normalized = raw.replace("\\", "/")
return f"violation:{normalized}".lower()
@@ -178,15 +144,7 @@ def repeated_workspace_violation_error(
arguments: dict[str, Any],
seen_counts: dict[str, int],
) -> str | None:
"""Return an escalated error string after repeated bypass attempts.
Returns ``None`` while the LLM is still within the soft retry budget --
callers should fall back to the tool's own error message in that case.
Once the budget is exceeded, returns a hard "stop trying" instruction
that quotes the offending target. Throttle state lives in
*seen_counts* (a per-turn dict), so the budget naturally resets across
turns without persisting LLM-controlled keys.
"""
"""Return an escalated error after repeated bypass attempts."""
signature = workspace_violation_signature(tool_name, arguments)
if signature is None:
return None