Spaces:
Sleeping
Sleeping
Setup sandbox server
Browse files- Dockerfile +10 -12
- sandbox_server.py +105 -0
Dockerfile
CHANGED
|
@@ -1,29 +1,27 @@
|
|
| 1 |
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
| 2 |
|
| 3 |
-
# Install Dev Mode required packages + useful tools
|
| 4 |
RUN apt-get update && \
|
| 5 |
apt-get install -y \
|
| 6 |
-
bash \
|
| 7 |
-
|
| 8 |
-
wget curl procps \
|
| 9 |
-
htop vim nano \
|
| 10 |
-
jq \
|
| 11 |
-
tmux \
|
| 12 |
build-essential && \
|
| 13 |
rm -rf /var/lib/apt/lists/*
|
| 14 |
|
| 15 |
-
|
|
|
|
| 16 |
RUN useradd -m -u 1000 user
|
| 17 |
USER user
|
| 18 |
|
| 19 |
ENV HOME=/home/user \
|
| 20 |
-
PATH=/home/user/.local/bin:$PATH
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
WORKDIR /app
|
| 24 |
COPY --chown=user . /app
|
| 25 |
|
| 26 |
EXPOSE 7860
|
| 27 |
|
| 28 |
-
|
| 29 |
-
CMD ["python", "-m", "http.server", "7860"]
|
|
|
|
| 1 |
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
| 2 |
|
|
|
|
| 3 |
RUN apt-get update && \
|
| 4 |
apt-get install -y \
|
| 5 |
+
bash git git-lfs wget curl procps \
|
| 6 |
+
htop vim nano jq tmux \
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
build-essential && \
|
| 8 |
rm -rf /var/lib/apt/lists/*
|
| 9 |
|
| 10 |
+
RUN uv pip install --system fastapi uvicorn python-multipart
|
| 11 |
+
|
| 12 |
RUN useradd -m -u 1000 user
|
| 13 |
USER user
|
| 14 |
|
| 15 |
ENV HOME=/home/user \
|
| 16 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 17 |
+
PIP_USER=1 \
|
| 18 |
+
HF_HUB_DISABLE_PROGRESS_BARS=1 \
|
| 19 |
+
TQDM_DISABLE=1 \
|
| 20 |
+
HF_HUB_ENABLE_HF_TRANSFER=1
|
| 21 |
|
| 22 |
WORKDIR /app
|
| 23 |
COPY --chown=user . /app
|
| 24 |
|
| 25 |
EXPOSE 7860
|
| 26 |
|
| 27 |
+
CMD ["python", "sandbox_server.py"]
|
|
|
sandbox_server.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal FastAPI server for sandbox operations."""
|
| 2 |
+
import os, subprocess, pathlib
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from typing import Optional
|
| 6 |
+
import uvicorn
|
| 7 |
+
|
| 8 |
+
app = FastAPI()
|
| 9 |
+
|
| 10 |
+
class BashReq(BaseModel):
|
| 11 |
+
command: str
|
| 12 |
+
work_dir: str = "/app"
|
| 13 |
+
timeout: int = 120
|
| 14 |
+
|
| 15 |
+
class ReadReq(BaseModel):
|
| 16 |
+
path: str
|
| 17 |
+
offset: Optional[int] = None
|
| 18 |
+
limit: Optional[int] = 2000
|
| 19 |
+
|
| 20 |
+
class WriteReq(BaseModel):
|
| 21 |
+
path: str
|
| 22 |
+
content: str
|
| 23 |
+
|
| 24 |
+
class EditReq(BaseModel):
|
| 25 |
+
path: str
|
| 26 |
+
old_str: str
|
| 27 |
+
new_str: str
|
| 28 |
+
replace_all: bool = False
|
| 29 |
+
|
| 30 |
+
class ExistsReq(BaseModel):
|
| 31 |
+
path: str
|
| 32 |
+
|
| 33 |
+
@app.get("/api/health")
|
| 34 |
+
def health():
|
| 35 |
+
return {"status": "ok"}
|
| 36 |
+
|
| 37 |
+
@app.post("/api/bash")
|
| 38 |
+
def bash(req: BashReq):
|
| 39 |
+
try:
|
| 40 |
+
r = subprocess.run(
|
| 41 |
+
req.command, shell=True, capture_output=True, text=True,
|
| 42 |
+
cwd=req.work_dir, timeout=req.timeout,
|
| 43 |
+
)
|
| 44 |
+
output = r.stdout + r.stderr
|
| 45 |
+
if len(output) > 30000:
|
| 46 |
+
output = output[:30000] + "\n... (truncated)"
|
| 47 |
+
return {"success": r.returncode == 0, "output": output, "error": "" if r.returncode == 0 else f"Exit code {r.returncode}"}
|
| 48 |
+
except subprocess.TimeoutExpired:
|
| 49 |
+
return {"success": False, "output": "", "error": f"Timeout after {req.timeout}s"}
|
| 50 |
+
except Exception as e:
|
| 51 |
+
return {"success": False, "output": "", "error": str(e)}
|
| 52 |
+
|
| 53 |
+
@app.post("/api/read")
|
| 54 |
+
def read(req: ReadReq):
|
| 55 |
+
try:
|
| 56 |
+
p = pathlib.Path(req.path)
|
| 57 |
+
if not p.exists():
|
| 58 |
+
return {"success": False, "output": "", "error": f"File not found: {req.path}"}
|
| 59 |
+
if p.is_dir():
|
| 60 |
+
return {"success": False, "output": "", "error": f"Is a directory: {req.path}"}
|
| 61 |
+
lines = p.read_text().splitlines()
|
| 62 |
+
start = (req.offset or 1) - 1
|
| 63 |
+
end = start + (req.limit or len(lines))
|
| 64 |
+
selected = lines[start:end]
|
| 65 |
+
numbered = "\n".join(f"{start + i + 1}\t{line}" for i, line in enumerate(selected))
|
| 66 |
+
return {"success": True, "output": numbered, "error": ""}
|
| 67 |
+
except Exception as e:
|
| 68 |
+
return {"success": False, "output": "", "error": str(e)}
|
| 69 |
+
|
| 70 |
+
@app.post("/api/write")
|
| 71 |
+
def write(req: WriteReq):
|
| 72 |
+
try:
|
| 73 |
+
p = pathlib.Path(req.path)
|
| 74 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 75 |
+
p.write_text(req.content)
|
| 76 |
+
return {"success": True, "output": f"Wrote {len(req.content)} bytes to {req.path}", "error": ""}
|
| 77 |
+
except Exception as e:
|
| 78 |
+
return {"success": False, "output": "", "error": str(e)}
|
| 79 |
+
|
| 80 |
+
@app.post("/api/edit")
|
| 81 |
+
def edit(req: EditReq):
|
| 82 |
+
try:
|
| 83 |
+
p = pathlib.Path(req.path)
|
| 84 |
+
if not p.exists():
|
| 85 |
+
return {"success": False, "output": "", "error": f"File not found: {req.path}"}
|
| 86 |
+
content = p.read_text()
|
| 87 |
+
if req.old_str not in content:
|
| 88 |
+
return {"success": False, "output": "", "error": f"old_str not found in {req.path}"}
|
| 89 |
+
if not req.replace_all and content.count(req.old_str) > 1:
|
| 90 |
+
return {"success": False, "output": "", "error": f"old_str appears {content.count(req.old_str)} times. Use replace_all=true or provide more context."}
|
| 91 |
+
if req.replace_all:
|
| 92 |
+
new_content = content.replace(req.old_str, req.new_str)
|
| 93 |
+
else:
|
| 94 |
+
new_content = content.replace(req.old_str, req.new_str, 1)
|
| 95 |
+
p.write_text(new_content)
|
| 96 |
+
return {"success": True, "output": f"Edited {req.path}", "error": ""}
|
| 97 |
+
except Exception as e:
|
| 98 |
+
return {"success": False, "output": "", "error": str(e)}
|
| 99 |
+
|
| 100 |
+
@app.post("/api/exists")
|
| 101 |
+
def exists(req: ExistsReq):
|
| 102 |
+
return {"success": True, "output": str(pathlib.Path(req.path).exists()).lower(), "error": ""}
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|