commit 3f7c544835f3ce3fa16b05945cad89d41ee6fe09 Author: Timothy Jaeryang Baek Date: Thu Feb 12 13:10:52 2026 -0600 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd9dd9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ + +# venv +.venv/ + +# uv +uv.lock + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..15838aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + wget \ + git \ + jq \ + vim-tiny \ + build-essential \ + ca-certificates \ + openssh-client \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY . . +RUN pip install --no-cache-dir . + +EXPOSE 8000 + +ENTRYPOINT ["open-terminal"] +CMD ["run"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..aec9132 --- /dev/null +++ b/dev.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +uv run uvicorn open_terminal.main:app --reload diff --git a/open_terminal/__init__.py b/open_terminal/__init__.py new file mode 100644 index 0000000..5925cdd --- /dev/null +++ b/open_terminal/__init__.py @@ -0,0 +1 @@ +"""open-terminal — a barebones terminal interaction API.""" diff --git a/open_terminal/cli.py b/open_terminal/cli.py new file mode 100644 index 0000000..0304e73 --- /dev/null +++ b/open_terminal/cli.py @@ -0,0 +1,20 @@ +import click +import uvicorn + + +@click.group() +def main(): + """open-terminal — terminal interaction API""" + pass + + +@main.command() +@click.option("--host", default="0.0.0.0", help="Bind host") +@click.option("--port", default=8000, type=int, help="Bind port") +def run(host: str, port: int): + """Start the sandbox API server.""" + uvicorn.run("open_terminal.main:app", host=host, port=port) + + +if __name__ == "__main__": + main() diff --git a/open_terminal/main.py b/open_terminal/main.py new file mode 100644 index 0000000..758c051 --- /dev/null +++ b/open_terminal/main.py @@ -0,0 +1,78 @@ +import asyncio +import json +from typing import Optional + +from fastapi import FastAPI +from fastapi.responses import StreamingResponse +from pydantic import BaseModel + +app = FastAPI(title="open-terminal") + + +class ExecRequest(BaseModel): + command: str + timeout: Optional[float] = 30.0 + + +class ExecResponse(BaseModel): + exit_code: int + stdout: str + stderr: str + + +@app.get("/health") +async def health(): + return {"status": "ok"} + + +@app.post("/execute") +async def execute(req: ExecRequest, stream: bool = False): + if stream: + return _stream_response(req) + + try: + proc = await asyncio.create_subprocess_shell( + req.command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for( + proc.communicate(), timeout=req.timeout + ) + except asyncio.TimeoutError: + proc.kill() + await proc.communicate() + return ExecResponse( + exit_code=-1, + stdout="", + stderr=f"Command timed out after {req.timeout}s", + ) + + return ExecResponse( + exit_code=proc.returncode or 0, + stdout=stdout.decode(errors="replace"), + stderr=stderr.decode(errors="replace"), + ) + + +def _stream_response(req: ExecRequest): + async def generate(): + proc = await asyncio.create_subprocess_shell( + req.command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + async def read_stream(s, label): + async for line in s: + yield json.dumps({"type": label, "data": line.decode(errors="replace")}) + "\n" + + async for chunk in read_stream(proc.stdout, "stdout"): + yield chunk + async for chunk in read_stream(proc.stderr, "stderr"): + yield chunk + + await proc.wait() + yield json.dumps({"type": "exit", "data": proc.returncode}) + "\n" + + return StreamingResponse(generate(), media_type="application/x-ndjson") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5690433 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "open-terminal" +version = "0.1.0" +description = "A barebones terminal interaction API" +readme = "README.md" +authors = [ + { name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" } +] +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.34.0", + "click>=8.1.0", +] + +[project.scripts] +open-terminal = "open_terminal.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["open_terminal"]