From 745512aa4bd6cf907ef46ea3ce1c5da5a56f00d0 Mon Sep 17 00:00:00 2001 From: MrKan Date: Wed, 8 Apr 2026 15:00:01 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B8=20=D0=BC=D0=B0=D1=83=D0=BD=D1=82=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=BF=D0=BA=D0=B8=20/workspace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 1 + .gitignore | 2 ++ Dockerfile | 8 ++++++-- docker-compose.yml | 1 + src/agent/base.py | 6 ++++++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index e557a44..2197ab1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ *.pyc __pycache__/ .env +workspace/ diff --git a/.gitignore b/.gitignore index 274911b..ab5238a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +workspace/ + .idea/ # Byte-compiled / optimized / DLL files diff --git a/Dockerfile b/Dockerfile index e090658..32572fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,6 @@ RUN uv pip install git+https://git.lambda.coredump.ru/platform/agent_api.git FROM base as production -RUN useradd --create-home --shell /bin/bash appuser - COPY --from=builder /app/.venv /app/.venv ENV PATH="/app/.venv/bin:$PATH" @@ -26,8 +24,12 @@ 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"] @@ -47,6 +49,8 @@ ENV PATH="/app/.venv/bin:$PATH" COPY Makefile ./ COPY .mk/ ./.mk/ +ENV WORKSPACE_DIR="/workspace/" + EXPOSE 8000 CMD ["make", "uvicorn-dev"] diff --git a/docker-compose.yml b/docker-compose.yml index 32ec3ae..3f57d97 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: volumes: - ./src:/app/src - ${AGENT_API_PATH}:/agent-api/ + - ./workspace:/workspace/ ports: - "8000:8000" env_file: diff --git a/src/agent/base.py b/src/agent/base.py index f635ba8..47b6177 100644 --- a/src/agent/base.py +++ b/src/agent/base.py @@ -2,6 +2,7 @@ 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 def create_agent(): @@ -11,9 +12,14 @@ def create_agent(): api_key=os.environ["PROVIDER_API_KEY"], ) + workspace_dir = os.environ["WORKSPACE_DIR"] + backend = LocalShellBackend(workspace_dir, + virtual_mode=True) + return create_deep_agent( model=model, system_prompt="You are a helpful assistant.", checkpointer=MemorySaver(), + backend=backend, ) From a1235cf255752a532df2a09416c215a3f8db300d Mon Sep 17 00:00:00 2001 From: MrKan Date: Wed, 8 Apr 2026 15:33:05 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20shel?= =?UTF-8?q?l=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=20?= =?UTF-8?q?=D1=8E=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, ) - From 59f6e5bc4e17e600984aa82a255c3b4344956374 Mon Sep 17 00:00:00 2001 From: MrKan Date: Thu, 9 Apr 2026 23:54:20 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=BB=D1=8F=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 21 ++++++++++++++------- docker-compose.yml | 4 ++++ src/agent/backends/isolated_shell.py | 14 ++++++++------ src/agent/base.py | 2 +- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5468f9d..b9bc44e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,18 @@ -FROM python:3.14-slim as base +FROM python:3.14-slim AS base ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 WORKDIR /app -RUN apt update && apt install make -y +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 +RUN useradd --shell /bin/bash $AGENT_USER \ + && mkdir -p $WORKSPACE_DIR /home/$AGENT_USER \ + && chown -R agent:agent $WORKSPACE_DIR /home/$AGENT_USER -FROM base as builder +FROM base AS builder RUN apt install git -y RUN pip install uv @@ -20,7 +21,7 @@ COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-install-project --no-dev RUN uv pip install git+https://git.lambda.coredump.ru/platform/agent_api.git -FROM base as production +FROM base AS production COPY --from=builder /app/.venv /app/.venv ENV PATH="/app/.venv/bin:$PATH" @@ -28,12 +29,15 @@ ENV PATH="/app/.venv/bin:$PATH" COPY src/ /app/src/ COPY Makefile ./ COPY .mk/ ./.mk/ +RUN chown root:root /app && chmod 700 /app +RUN apt install sudo -y && \ + echo "agent ALL=(ALL) NOPASSWD: /usr/bin/apt*" >> /etc/sudoers EXPOSE 8000 CMD ["make", "uvicorn-prod"] -FROM base as development +FROM base AS development RUN pip install uv @@ -47,6 +51,9 @@ ENV PATH="/app/.venv/bin:$PATH" COPY Makefile ./ COPY .mk/ ./.mk/ +RUN chown root:root /app && chmod 700 /app +RUN apt install sudo -y && \ + echo "agent ALL=(ALL) NOPASSWD: /usr/bin/apt*" >> /etc/sudoers EXPOSE 8000 diff --git a/docker-compose.yml b/docker-compose.yml index 3f57d97..d025503 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,5 +23,9 @@ services: - "8000:8000" env_file: - .env + cap_add: # для работы bwrap + - SYS_ADMIN + security_opt: # для работы bwrap + - seccomp:unconfined profiles: - dev diff --git a/src/agent/backends/isolated_shell.py b/src/agent/backends/isolated_shell.py index 6aa78ea..468fb1b 100644 --- a/src/agent/backends/isolated_shell.py +++ b/src/agent/backends/isolated_shell.py @@ -3,7 +3,7 @@ import pwd import subprocess from typing import Any -from deepagents.backends.local_shell import LocalShellBackend, DEFAULT_EXECUTE_TIMEOUT +from deepagents.backends.local_shell import LocalShellBackend class IsolatedShellBackend(LocalShellBackend): @@ -34,8 +34,9 @@ class IsolatedShellBackend(LocalShellBackend): f"timeout must be positive, got {effective_timeout}" ) - proc: subprocess.Popen[str] | None = None + proc: subprocess.Popen | None = None try: + print(f"Running shell: {command}") proc = subprocess.Popen( command, shell=True, @@ -69,12 +70,13 @@ class IsolatedShellBackend(LocalShellBackend): if proc.returncode != 0: output = f"{output.rstrip()}\n\nExit code: {proc.returncode}" - return self._make_response(output, proc.returncode, truncated) + result = self._make_response(output, proc.returncode, truncated) + print(result) + return result except subprocess.TimeoutExpired: - if proc: - proc.kill() - proc.communicate() + proc.kill() + proc.communicate() msg = f"Error: Command timed out after {effective_timeout} seconds." return self._make_response(msg, 124, False) diff --git a/src/agent/base.py b/src/agent/base.py index 1cdb820..fe08072 100644 --- a/src/agent/base.py +++ b/src/agent/base.py @@ -15,7 +15,7 @@ def create_agent(): ) workspace_dir = os.environ["WORKSPACE_DIR"] - agent_user = os.environ["AGENT_USER"] + agent_user = os.environ.get("AGENT_USER", "agent") backend = IsolatedShellBackend( user=agent_user,