From a1235cf255752a532df2a09416c215a3f8db300d Mon Sep 17 00:00:00 2001 From: MrKan Date: Wed, 8 Apr 2026 15:33:05 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20shell=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=20=D0=BF=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=20=D1=8E?= =?UTF-8?q?=D0=B7=D0=B5=D1=80=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 13 ++-- src/agent/backends/__init__.py | 3 + src/agent/backends/isolated_shell.py | 96 ++++++++++++++++++++++++++++ src/agent/base.py | 14 ++-- 4 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 src/agent/backends/__init__.py create mode 100644 src/agent/backends/isolated_shell.py diff --git a/Dockerfile b/Dockerfile index 32572fc..5468f9d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,11 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app RUN apt update && apt install make -y +ENV AGENT_USER="agent" +RUN useradd --shell /bin/bash agent +ENV WORKSPACE_DIR="/workspace/" +RUN mkdir -p $WORKSPACE_DIR && chown $AGENT_USER:$AGENT_USER $WORKSPACE_DIR + FROM base as builder RUN apt install git -y @@ -24,12 +29,6 @@ COPY src/ /app/src/ COPY Makefile ./ COPY .mk/ ./.mk/ -RUN useradd --shell /bin/bash appuser -USER appuser - -ENV WORKSPACE_DIR="/workspace/" -RUN mkdir -p $WORKSPACE_DIR && chown appuser:appuser $WORKSPACE_DIR - EXPOSE 8000 CMD ["make", "uvicorn-prod"] @@ -49,8 +48,6 @@ ENV PATH="/app/.venv/bin:$PATH" COPY Makefile ./ COPY .mk/ ./.mk/ -ENV WORKSPACE_DIR="/workspace/" - EXPOSE 8000 CMD ["make", "uvicorn-dev"] diff --git a/src/agent/backends/__init__.py b/src/agent/backends/__init__.py new file mode 100644 index 0000000..3bfb87d --- /dev/null +++ b/src/agent/backends/__init__.py @@ -0,0 +1,3 @@ +from src.agent.backends.isolated_shell import IsolatedShellBackend + +__all__ = ["IsolatedShellBackend"] diff --git a/src/agent/backends/isolated_shell.py b/src/agent/backends/isolated_shell.py new file mode 100644 index 0000000..6aa78ea --- /dev/null +++ b/src/agent/backends/isolated_shell.py @@ -0,0 +1,96 @@ +import os +import pwd +import subprocess +from typing import Any + +from deepagents.backends.local_shell import LocalShellBackend, DEFAULT_EXECUTE_TIMEOUT + + +class IsolatedShellBackend(LocalShellBackend): + """LocalShellBackend с изоляцией shell-команд через отдельного пользователя.""" + + def __init__( + self, + user: str, + **kwargs: Any, + ): + super().__init__(**kwargs) + self._user = user + self._uid = pwd.getpwnam(user).pw_uid # type: ignore[attr-defined] + self._gid = pwd.getpwnam(user).pw_gid # type: ignore[attr-defined] + + def execute( + self, + command: str, + *, + timeout: int | None = None, + ) -> Any: + if not command or not isinstance(command, str): + return type(self)._error_response("Command must be a non-empty string.") + + effective_timeout = timeout if timeout is not None else self._default_timeout + if effective_timeout <= 0: + return type(self)._error_response( + f"timeout must be positive, got {effective_timeout}" + ) + + proc: subprocess.Popen[str] | None = None + try: + proc = subprocess.Popen( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=str(self.cwd), + env=self._env, + preexec_fn=lambda: ( + os.setgid(self._gid) or os.setuid(self._uid) # type: ignore[attr-defined] + ), + ) + + stdout, stderr = proc.communicate(timeout=effective_timeout) + + output_parts = [] + if stdout: + output_parts.append(stdout) + if stderr: + stderr_lines = stderr.strip().split("\n") + output_parts.extend(f"[stderr] {line}" for line in stderr_lines) + + output = "\n".join(output_parts) if output_parts else "" + + truncated = False + if len(output) > self._max_output_bytes: + output = output[: self._max_output_bytes] + output += f"\n\n... Output truncated at {self._max_output_bytes} bytes." + truncated = True + + if proc.returncode != 0: + output = f"{output.rstrip()}\n\nExit code: {proc.returncode}" + + return self._make_response(output, proc.returncode, truncated) + + except subprocess.TimeoutExpired: + if proc: + proc.kill() + proc.communicate() + msg = f"Error: Command timed out after {effective_timeout} seconds." + return self._make_response(msg, 124, False) + + except Exception as e: + return self._make_response( + f"Error executing command ({type(e).__name__}): {e}", 1, False + ) + + @staticmethod + def _error_response(message: str): + from deepagents.backends.protocol import ExecuteResponse + + return ExecuteResponse(output=message, exit_code=1, truncated=False) + + @staticmethod + def _make_response(output: str, exit_code: int, truncated: bool): + from deepagents.backends.protocol import ExecuteResponse + + return ExecuteResponse(output=output, exit_code=exit_code, truncated=truncated) diff --git a/src/agent/base.py b/src/agent/base.py index 47b6177..1cdb820 100644 --- a/src/agent/base.py +++ b/src/agent/base.py @@ -1,8 +1,10 @@ import os + from deepagents import create_deep_agent from langchain_openai import ChatOpenAI from langgraph.checkpoint.memory import MemorySaver -from deepagents.backends.local_shell import LocalShellBackend + +from src.agent.backends import IsolatedShellBackend def create_agent(): @@ -13,8 +15,13 @@ def create_agent(): ) workspace_dir = os.environ["WORKSPACE_DIR"] - backend = LocalShellBackend(workspace_dir, - virtual_mode=True) + agent_user = os.environ["AGENT_USER"] + + backend = IsolatedShellBackend( + user=agent_user, + root_dir=workspace_dir, + virtual_mode=True, + ) return create_deep_agent( model=model, @@ -22,4 +29,3 @@ def create_agent(): checkpointer=MemorySaver(), backend=backend, ) -