Reviewed-on: #8
This commit is contained in:
commit
0709f5433b
7 changed files with 139 additions and 10 deletions
|
|
@ -7,3 +7,4 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.env
|
.env
|
||||||
|
workspace/
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
|
workspace/
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
workspace/
|
workspace/
|
||||||
|
|
||||||
|
|
|
||||||
26
Dockerfile
26
Dockerfile
|
|
@ -1,12 +1,18 @@
|
||||||
FROM python:3.14-slim as base
|
FROM python:3.14-slim AS base
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt update && apt install make -y
|
RUN apt update && apt install make -y
|
||||||
|
|
||||||
FROM base as builder
|
ENV AGENT_USER="agent"
|
||||||
|
ENV WORKSPACE_DIR="/workspace/"
|
||||||
|
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
|
||||||
|
|
||||||
RUN apt install git -y
|
RUN apt install git -y
|
||||||
RUN pip install uv
|
RUN pip install uv
|
||||||
|
|
@ -15,9 +21,7 @@ COPY pyproject.toml uv.lock ./
|
||||||
RUN uv sync --frozen --no-install-project --no-dev
|
RUN uv sync --frozen --no-install-project --no-dev
|
||||||
RUN uv pip install git+https://git.lambda.coredump.ru/platform/agent_api.git
|
RUN uv pip install git+https://git.lambda.coredump.ru/platform/agent_api.git
|
||||||
|
|
||||||
FROM base as production
|
FROM base AS production
|
||||||
|
|
||||||
RUN useradd --create-home --shell /bin/bash appuser
|
|
||||||
|
|
||||||
COPY --from=builder /app/.venv /app/.venv
|
COPY --from=builder /app/.venv /app/.venv
|
||||||
ENV PATH="/app/.venv/bin:$PATH"
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
@ -25,14 +29,15 @@ ENV PATH="/app/.venv/bin:$PATH"
|
||||||
COPY src/ /app/src/
|
COPY src/ /app/src/
|
||||||
COPY Makefile ./
|
COPY Makefile ./
|
||||||
COPY .mk/ ./.mk/
|
COPY .mk/ ./.mk/
|
||||||
|
RUN chown root:root /app && chmod 700 /app
|
||||||
USER appuser
|
RUN apt install sudo -y && \
|
||||||
|
echo "agent ALL=(ALL) NOPASSWD: /usr/bin/apt*" >> /etc/sudoers
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["make", "uvicorn-prod"]
|
CMD ["make", "uvicorn-prod"]
|
||||||
|
|
||||||
FROM base as development
|
FROM base AS development
|
||||||
|
|
||||||
RUN pip install uv
|
RUN pip install uv
|
||||||
|
|
||||||
|
|
@ -46,6 +51,9 @@ ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
COPY Makefile ./
|
COPY Makefile ./
|
||||||
COPY .mk/ ./.mk/
|
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
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,14 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/app/src
|
- ./src:/app/src
|
||||||
- ${AGENT_API_PATH}:/agent-api/
|
- ${AGENT_API_PATH}:/agent-api/
|
||||||
|
- ./workspace:/workspace/
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
cap_add: # для работы bwrap
|
||||||
|
- SYS_ADMIN
|
||||||
|
security_opt: # для работы bwrap
|
||||||
|
- seccomp:unconfined
|
||||||
profiles:
|
profiles:
|
||||||
- dev
|
- dev
|
||||||
|
|
|
||||||
3
src/agent/backends/__init__.py
Normal file
3
src/agent/backends/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from src.agent.backends.isolated_shell import IsolatedShellBackend
|
||||||
|
|
||||||
|
__all__ = ["IsolatedShellBackend"]
|
||||||
98
src/agent/backends/isolated_shell.py
Normal file
98
src/agent/backends/isolated_shell.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
import subprocess
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from deepagents.backends.local_shell import LocalShellBackend
|
||||||
|
|
||||||
|
|
||||||
|
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 | None = None
|
||||||
|
try:
|
||||||
|
print(f"Running shell: {command}")
|
||||||
|
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 "<no output>"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
result = self._make_response(output, proc.returncode, truncated)
|
||||||
|
print(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
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)
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from deepagents import create_deep_agent
|
from deepagents import create_deep_agent
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_openai import ChatOpenAI
|
||||||
from langgraph.checkpoint.memory import MemorySaver
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
|
|
||||||
|
from src.agent.backends import IsolatedShellBackend
|
||||||
|
|
||||||
|
|
||||||
def create_agent():
|
def create_agent():
|
||||||
model = ChatOpenAI(
|
model = ChatOpenAI(
|
||||||
|
|
@ -11,9 +14,18 @@ def create_agent():
|
||||||
api_key=os.environ["PROVIDER_API_KEY"],
|
api_key=os.environ["PROVIDER_API_KEY"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
workspace_dir = os.environ["WORKSPACE_DIR"]
|
||||||
|
agent_user = os.environ.get("AGENT_USER", "agent")
|
||||||
|
|
||||||
|
backend = IsolatedShellBackend(
|
||||||
|
user=agent_user,
|
||||||
|
root_dir=workspace_dir,
|
||||||
|
virtual_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
return create_deep_agent(
|
return create_deep_agent(
|
||||||
model=model,
|
model=model,
|
||||||
system_prompt="You are a helpful assistant.",
|
system_prompt="You are a helpful assistant.",
|
||||||
checkpointer=MemorySaver(),
|
checkpointer=MemorySaver(),
|
||||||
|
backend=backend,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue