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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user