mirror of
https://github.com/open-webui/open-terminal.git
synced 2026-10-19 01:32:05 +00:00
refac
This commit is contained in:
parent
3f7c544835
commit
3103f34ee7
8 changed files with 286 additions and 26 deletions
50
.github/workflows/docker.yml
vendored
Normal file
50
.github/workflows/docker.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Build Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha
|
||||
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
41
Dockerfile
41
Dockerfile
|
|
@ -1,18 +1,41 @@
|
|||
FROM python:3.12-slim
|
||||
FROM python:3.12
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
wget \
|
||||
# Core utilities
|
||||
coreutils findutils grep sed gawk diffutils patch \
|
||||
less file tree bc man-db \
|
||||
# Networking
|
||||
curl wget net-tools iputils-ping dnsutils netcat-openbsd socat telnet \
|
||||
openssh-client rsync \
|
||||
# Editors
|
||||
vim nano \
|
||||
# Version control
|
||||
git \
|
||||
jq \
|
||||
vim-tiny \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
openssh-client \
|
||||
unzip \
|
||||
# Build tools
|
||||
build-essential cmake make \
|
||||
# Scripting & languages
|
||||
perl ruby-full lua5.4 \
|
||||
# Data processing
|
||||
jq xmlstarlet sqlite3 \
|
||||
# Compression
|
||||
zip unzip tar gzip bzip2 xz-utils zstd p7zip-full \
|
||||
# System
|
||||
procps htop lsof strace sysstat \
|
||||
sudo tmux screen \
|
||||
ca-certificates gnupg apt-transport-https \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir \
|
||||
numpy pandas scipy scikit-learn \
|
||||
matplotlib seaborn plotly \
|
||||
jupyter ipython \
|
||||
requests beautifulsoup4 lxml \
|
||||
sqlalchemy psycopg2-binary \
|
||||
pyyaml toml jsonlines \
|
||||
tqdm rich
|
||||
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir .
|
||||
|
||||
|
|
|
|||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Open WebUI Inc. (Timothy Jaeryang Baek)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
72
README.md
72
README.md
|
|
@ -0,0 +1,72 @@
|
|||
# ⬛ Open Terminal
|
||||
|
||||
A lightweight API for remote command execution with streaming support.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Run Locally
|
||||
|
||||
```bash
|
||||
pip install .
|
||||
open-terminal run
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker build -t open-terminal .
|
||||
docker run -p 8000:8000 open-terminal
|
||||
```
|
||||
|
||||
An API key is auto-generated on startup if not provided. To set your own:
|
||||
|
||||
```bash
|
||||
# via CLI flag
|
||||
open-terminal run --api-key my-secret
|
||||
|
||||
# via environment variable
|
||||
docker run -p 8000:8000 -e OPEN_TERMINAL_API_KEY=my-secret open-terminal
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Health check — no auth required.
|
||||
|
||||
### `POST /execute`
|
||||
|
||||
Execute a command and return the result.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/execute \
|
||||
-H "Authorization: Bearer <api-key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "echo hello", "timeout": 30}'
|
||||
```
|
||||
|
||||
```json
|
||||
{"exit_code": 0, "stdout": "hello\n", "stderr": ""}
|
||||
```
|
||||
|
||||
### `POST /execute?stream=true`
|
||||
|
||||
Stream output as JSONL (`application/x-ndjson`):
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/execute?stream=true" \
|
||||
-H "Authorization: Bearer <api-key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "for i in 1 2 3; do echo $i; sleep 1; done"}'
|
||||
```
|
||||
|
||||
```jsonl
|
||||
{"type": "stdout", "data": "1\n"}
|
||||
{"type": "stdout", "data": "2\n"}
|
||||
{"type": "stdout", "data": "3\n"}
|
||||
{"type": "exit", "data": 0}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -8,11 +8,45 @@ def main():
|
|||
pass
|
||||
|
||||
|
||||
BANNER = r"""
|
||||
____ _____ _ _
|
||||
/ __ \ |_ _| (_) | |
|
||||
| | | |_ __ ___ _ __ | | ___ _ __ _ __ ___ _ _ __ __ _| |
|
||||
| | | | '_ \ / _ | '_ \ | |/ _ | '__| '_ ` _ \| | '_ \ / _` | |
|
||||
| |__| | |_) | __| | | | | | __| | | | | | | | | | | | (_| | |
|
||||
\____/| .__/ \___|_| |_| \_/\___|_| |_| |_| |_|_|_| |_|\__,_|_|
|
||||
| |
|
||||
|_|
|
||||
"""
|
||||
|
||||
|
||||
@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):
|
||||
@click.option(
|
||||
"--api-key",
|
||||
default="",
|
||||
envvar="OPEN_TERMINAL_API_KEY",
|
||||
help="Bearer API key (or set OPEN_TERMINAL_API_KEY env var)",
|
||||
)
|
||||
def run(host: str, port: int, api_key: str):
|
||||
"""Start the sandbox API server."""
|
||||
import os
|
||||
import secrets
|
||||
|
||||
generated = not api_key
|
||||
if not api_key:
|
||||
api_key = secrets.token_urlsafe(24)
|
||||
|
||||
os.environ["OPEN_TERMINAL_API_KEY"] = api_key
|
||||
|
||||
click.echo(BANNER)
|
||||
if generated:
|
||||
click.echo("=" * 60)
|
||||
click.echo(f" API Key: {api_key}")
|
||||
click.echo("=" * 60)
|
||||
click.echo()
|
||||
|
||||
uvicorn.run("open_terminal.main:app", host=host, port=port)
|
||||
|
||||
|
||||
|
|
|
|||
3
open_terminal/env.py
Normal file
3
open_terminal/env.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import os
|
||||
|
||||
API_KEY = os.environ.get("OPEN_TERMINAL_API_KEY", "")
|
||||
|
|
@ -2,31 +2,88 @@ import asyncio
|
|||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
app = FastAPI(title="open-terminal")
|
||||
from open_terminal.env import API_KEY
|
||||
|
||||
bearer_scheme = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
async def verify_api_key(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(bearer_scheme),
|
||||
):
|
||||
if not API_KEY:
|
||||
return
|
||||
if not credentials or credentials.credentials != API_KEY:
|
||||
raise HTTPException(status_code=401, detail="Invalid API key")
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Open Terminal",
|
||||
description="Shell command execution API with synchronous and streaming support.",
|
||||
version="0.1.3",
|
||||
dependencies=[Depends(verify_api_key)],
|
||||
)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
class ExecRequest(BaseModel):
|
||||
command: str
|
||||
timeout: Optional[float] = 30.0
|
||||
command: str = Field(
|
||||
...,
|
||||
description="Shell command to execute. Supports chaining (&&, ||, ;), pipes (|), and redirections.",
|
||||
json_schema_extra={"examples": ["echo hello", "ls -la && whoami"]},
|
||||
)
|
||||
timeout: Optional[float] = Field(
|
||||
30.0,
|
||||
description="Max execution time in seconds. Process is killed if exceeded (exit_code: -1). Null to disable.",
|
||||
ge=0,
|
||||
)
|
||||
|
||||
|
||||
class ExecResponse(BaseModel):
|
||||
exit_code: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
exit_code: int = Field(
|
||||
...,
|
||||
description="Process exit code. 0 = success, non-zero = error, -1 = timeout.",
|
||||
)
|
||||
stdout: str = Field(..., description="Captured standard output.")
|
||||
stderr: str = Field(..., description="Captured standard error.")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
@app.get(
|
||||
"/health",
|
||||
summary="Health check",
|
||||
description="Returns service status. No authentication required.",
|
||||
)
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/execute")
|
||||
async def execute(req: ExecRequest, stream: bool = False):
|
||||
@app.post(
|
||||
"/execute",
|
||||
summary="Execute a command",
|
||||
description="Run a shell command and return the result.",
|
||||
response_model=ExecResponse,
|
||||
responses={
|
||||
401: {"description": "Invalid or missing API key."},
|
||||
},
|
||||
)
|
||||
async def execute(
|
||||
req: ExecRequest,
|
||||
stream: bool = Query(
|
||||
False,
|
||||
description="Stream output as JSONL (application/x-ndjson) instead of waiting for completion.",
|
||||
),
|
||||
):
|
||||
if stream:
|
||||
return _stream_response(req)
|
||||
|
||||
|
|
@ -36,9 +93,7 @@ async def execute(req: ExecRequest, stream: bool = False):
|
|||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=req.timeout
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=req.timeout)
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.communicate()
|
||||
|
|
@ -65,7 +120,9 @@ def _stream_response(req: ExecRequest):
|
|||
|
||||
async def read_stream(s, label):
|
||||
async for line in s:
|
||||
yield json.dumps({"type": label, "data": line.decode(errors="replace")}) + "\n"
|
||||
yield json.dumps(
|
||||
{"type": label, "data": line.decode(errors="replace")}
|
||||
) + "\n"
|
||||
|
||||
async for chunk in read_stream(proc.stdout, "stdout"):
|
||||
yield chunk
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "open-terminal"
|
||||
version = "0.1.0"
|
||||
version = "0.1.3"
|
||||
description = "A barebones terminal interaction API"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue