fix(approval): approvals.deny applies inside isolated containers too (#91002)

Both shell-command guard entry points returned approval from the isolated-container fast
path before the operator's approvals.deny rules were evaluated, so a Docker sandbox with
no host mounts (or singularity/modal/daytona/vercel) skipped the entire user deny list.
The deny list is documented as never bypassable: it states what the agent may DO, not
what it can reach. Evaluate it before the container skip; the built-in dangerous-command
heuristics keep skipping there. hermes approvals test mirrors the new order.

Salvaged from #91029 by @fangliquanflq.
This commit is contained in:
fangliquanflq
2026-09-05 18:03:15 +05:30
committed by kshitij
parent c957432685
commit 1c37b9c457
2 changed files with 24 additions and 9 deletions

View File

@@ -857,6 +857,17 @@ def _should_skip_container_guards(env_type: str, has_host_access: bool = False)
return env_type in ("singularity", "modal", "daytona", "vercel_sandbox")
def _user_deny_block(command: str) -> dict | None:
"""The operator's ``approvals.deny`` rules are documented as never bypassable — not by yolo,
not by mode=off, and not by an isolated container either: they express intent about what the
agent may DO, not what it can reach, so they are evaluated before the container fast path."""
deny_pattern = _match_user_deny_rule(command)
if deny_pattern is None:
return None
logger.warning("User deny rule %r blocked command: %s", deny_pattern, command[:200])
return _user_deny_block_result(deny_pattern)
def _floor_block(command: str, *, sudo_guard: bool = False) -> dict | None:
"""Unconditional floors, BEFORE yolo / mode=off / cron approve-mode so no
session-level setting can bypass them: hardline catastrophic commands,
@@ -871,11 +882,7 @@ def _floor_block(command: str, *, sudo_guard: bool = False) -> dict | None:
if is_sudo_guess:
logger.warning("Sudo stdin guard block: %s (command: %s)", sudo_guess_desc, command[:200])
return _sudo_stdin_block_result(sudo_guess_desc)
deny_pattern = _match_user_deny_rule(command)
if deny_pattern is not None:
logger.warning("User deny rule %r blocked command: %s", deny_pattern, command[:200])
return _user_deny_block_result(deny_pattern)
return None
return _user_deny_block(command)
def check_dangerous_command(command: str, env_type: str,
@@ -885,7 +892,7 @@ def check_dangerous_command(command: str, env_type: str,
a Docker sandbox that bind-mounts host paths must not skip approval.
Returns ``{"approved": True/False, "message": str or None, ...}``."""
if _should_skip_container_guards(env_type, has_host_access=has_host_access):
return _approved()
return _user_deny_block(command) or _approved()
blocked = _floor_block(command)
if blocked is not None:
return blocked
@@ -976,7 +983,7 @@ def check_all_command_guards(command: str, env_type: str,
force=True replay cannot bypass one check when only the other was shown to the user.
``has_host_access``: a Docker sandbox with bind-mounted host paths takes the normal flow."""
if _should_skip_container_guards(env_type, has_host_access=has_host_access):
return _approved()
return _user_deny_block(command) or _approved()
blocked = _floor_block(command, sudo_guard=True)
if blocked is not None: