"""Require external approval before executing shell tool calls."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Mapping
from dataclasses import dataclass
from typing import Literal, Protocol
from .._compat import StrEnum, TypeAliasType
from ..agent import AgentRunContext
from ..events import AgentEvent, AgentEventType
from ..messages import ToolCall
from ..tools.base import ToolResult
from .external import ExternalEventExtension
SHELL_APPROVAL_TOOL_NAME = "bash"
SHELL_APPROVAL_EVENT_NAME = "shell_approval_requested"
SHELL_APPROVAL_RESPONSE_EVENT_NAME = "shell_approval_response"
ShellCommandNext = TypeAliasType("ShellCommandNext", Callable[[], Awaitable[ToolResult]])
[docs]
class ShellApprovalMode(StrEnum):
"""Persisted approval policy for one Agent session."""
REVIEW = "review"
ALLOW_ALL = "allow_all"
[docs]
class ShellApprovalStorage(Protocol):
"""Persistence boundary for session policy and remembered commands."""
[docs]
async def get_session_mode(self, session_id: str) -> ShellApprovalMode:
"""Return the persisted approval policy for one session."""
...
[docs]
async def set_session_mode(self, session_id: str, mode: ShellApprovalMode) -> None:
"""Persist the approval policy for one session."""
[docs]
async def clear_session_mode(self, session_id: str) -> None:
"""Remove the persisted approval policy for one session."""
[docs]
async def is_allowed(self, session_id: str, command: str) -> bool:
"""Return whether one session permits execution of one exact command."""
...
[docs]
async def allow_command(self, command: str) -> None:
"""Persist one command in the allowlist."""
[docs]
@dataclass(frozen=True, slots=True)
class ShellApprovalResponse:
"""External decision for one pending shell command.
Args:
decision: ``"execute"`` runs the command; ``"abort"`` refuses it.
remember: Whether the command should join this session's allowlist.
"""
decision: Literal["execute", "abort"]
remember: bool = False
[docs]
@classmethod
def from_payload(cls, payload: Mapping[str, object]) -> ShellApprovalResponse:
"""Validate one external response payload; unknown keys are ignored."""
decision = payload.get("decision")
if decision not in ("execute", "abort"):
raise ValueError(f"Unsupported shell approval decision: {decision!r}")
remember = payload.get("remember", False)
if not isinstance(remember, bool):
raise ValueError("Shell approval 'remember' must be a boolean")
return cls(decision=decision, remember=remember)
[docs]
class ShellCommandAborted(RuntimeError):
"""Raised into normal tool failure handling when the user aborts a command."""
[docs]
class ShellApprovalRequestedEvent(AgentEvent):
"""CUSTOM event asking a UI to approve or abort one shell command."""
__slots__ = ()
def __init__(
self,
session_id: str,
call: ToolCall,
*,
command: str,
timeout_seconds: int,
remember_supported: bool,
) -> None:
super().__init__(
AgentEventType.CUSTOM,
session_id=session_id,
tool_calls=[call],
name=SHELL_APPROVAL_EVENT_NAME,
payload={
"session_id": session_id,
"tool_call_id": call.id,
"command": command,
"timeout_seconds": timeout_seconds,
"remember_supported": remember_supported,
"response_event": SHELL_APPROVAL_RESPONSE_EVENT_NAME,
},
)
[docs]
class ShellApprovalExtension(ExternalEventExtension):
"""Intercept bash and pause until a trusted external decision arrives.
Multiple shell calls may wait concurrently. Each response is correlated by
its own tool-call ID, so approving one call releases that call immediately
without waiting for the others.
"""
def __init__(self, storage: ShellApprovalStorage | None = None, *, enabled: bool = True) -> None:
super().__init__(
response_event_name=SHELL_APPROVAL_RESPONSE_EVENT_NAME,
correlation_field="tool_call_id",
)
self.storage = storage
self.enabled = enabled