From 7a0a57f21fd3357bfd4a50f9d71ae28f8b946945 Mon Sep 17 00:00:00 2001 From: mataiodoxion <226399730+mataiodoxion@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:35:57 -0700 Subject: [PATCH 1/6] wip: move code runner to submodule (not tested fully) --- api/python_exec_api.py | 62 +++++++++++++++++++---------------------- docker-compose.yml | 38 ++++++++++++++----------- runner/Dockerfile | 16 +++++++++++ runner/requirements.txt | 1 + runner/runner.py | 41 +++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 51 deletions(-) create mode 100644 runner/Dockerfile create mode 100644 runner/requirements.txt create mode 100644 runner/runner.py diff --git a/api/python_exec_api.py b/api/python_exec_api.py index 7ab74f5..beb3f98 100644 --- a/api/python_exec_api.py +++ b/api/python_exec_api.py @@ -1,41 +1,35 @@ -# /api/python_exec_api.py -from flask import Blueprint, request, jsonify +from flask import Blueprint, request from flask_restful import Api, Resource -import subprocess, tempfile, os +import requests python_exec_api = Blueprint('python_exec_api', __name__, url_prefix='/run') + api = Api(python_exec_api) +RUNNER_URL = "http://code_runner:8591/python" + class PythonExec(Resource): def post(self): - """Executes submitted Python code safely in a short-lived subprocess.""" - data = request.get_json() - code = data.get("code", "") - - if not code.strip(): - return {"output": "⚠️ No code provided."}, 400 - - with tempfile.NamedTemporaryFile(delete=False, suffix=".py") as tmp: - tmp.write(code.encode()) - tmp.flush() - - try: - result = subprocess.run( - ["python3", tmp.name], - capture_output=True, - text=True, - timeout=5, - cwd="/tmp", # Force working directory to /tmp - env={"HOME": "/tmp", "PATH": "/usr/bin:/usr/local/bin"} # Restricted environment - ) - output = result.stdout + result.stderr - except subprocess.TimeoutExpired: - output = "⏱️ Execution timed out (5 s limit)." - except Exception as e: - output = f"Error running code: {str(e)}" - finally: - os.unlink(tmp.name) - - return {"output": output} - -api.add_resource(PythonExec, "/python") \ No newline at end of file + data = request.get_json(silent=True) or {} + + try: + response = requests.post( + RUNNER_URL, + json=data, + timeout=10 + ) + + return response.json(), response.status_code + + except requests.Timeout: + return { + "output": "⏱️ Runner timed out." + }, 504 + + except requests.RequestException as e: + return { + "output": f"❌ Could not connect to code runner: {str(e)}" + }, 502 + + +api.add_resource(PythonExec, "/python") diff --git a/docker-compose.yml b/docker-compose.yml index 365688c..f98775b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,24 @@ version: '3' services: - web: - image: flask_open - build: . - env_file: - - .env # This file is optional; defaults will be used if it does not exist - ports: - - "8587:8587" - volumes: - - ./instance:/app/instance - restart: unless-stopped + web: + image: flask_open + build: . + env_file: + - .env # This file is optional; defaults will be used if it does not exist + ports: + - "8587:8587" + volumes: + - ./instance:/app/instance + restart: unless-stopped - socketio: - image: socket_open - build: ./socket - ports: - - "8500:8500" - - restart: unless-stopped \ No newline at end of file + socketio: + image: socket_open + build: ./socket + ports: + - "8500:8500" + restart: unless-stopped + + code_runner: + image: flask_runner + build: ./runner + restart: unless-stopped diff --git a/runner/Dockerfile b/runner/Dockerfile new file mode 100644 index 0000000..2cb1bc2 --- /dev/null +++ b/runner/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install python deps first for better layer caching +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r /app/requirements.txt + +# Copy service code +COPY . /app + +EXPOSE 8591 + +# Run the websocket service +CMD ["python", "runner.py"] diff --git a/runner/requirements.txt b/runner/requirements.txt new file mode 100644 index 0000000..7e10602 --- /dev/null +++ b/runner/requirements.txt @@ -0,0 +1 @@ +flask diff --git a/runner/runner.py b/runner/runner.py new file mode 100644 index 0000000..e1a7649 --- /dev/null +++ b/runner/runner.py @@ -0,0 +1,41 @@ +from flask import Flask, jsonify, request +import subprocess, tempfile, os + +runner = Flask(__name__) + +@runner.post("/python") +def run_python(): + data = request.get_json() + code = data.get("code", "") + + if not code.strip(): + return {"output": "⚠️ No code provided."}, 400 + + with tempfile.NamedTemporaryFile(delete=False, suffix=".py") as tmp: + tmp.write(code.encode()) + tmp.flush() + + try: + result = subprocess.run( + ["python3", tmp.name], + capture_output=True, + text=True, + timeout=5, + cwd="/tmp", # Force working directory to /tmp + env={"HOME": "/tmp", "PATH": "/usr/bin:/usr/local/bin"} # Restricted environment + ) + output = result.stdout + result.stderr + except subprocess.TimeoutExpired: + output = "⏱️ Execution timed out (5 s limit)." + except Exception as e: + output = f"Error running code: {str(e)}" + finally: + os.unlink(tmp.name) + + return {"output": output} + +if __name__ == "__main__": + host = "0.0.0.0" + port = 8591 + print(f"** Server running: http://localhost:{port}") # Pretty link + runner.run(debug=True, host=host, port=port, use_reloader=False) From 8d0811cd0319a0012c21027ea040210587ecb0e5 Mon Sep 17 00:00:00 2001 From: osteostriga <226399730+mataiodoxion@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:27:18 -0700 Subject: [PATCH 2/6] wip: add local/prod switch for code runner as per Mr. Mort, we don't need the security layer in local development --- api/python_exec_api.py | 68 ++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/api/python_exec_api.py b/api/python_exec_api.py index beb3f98..9d409b6 100644 --- a/api/python_exec_api.py +++ b/api/python_exec_api.py @@ -1,35 +1,73 @@ -from flask import Blueprint, request +from flask import Blueprint, Flask, request from flask_restful import Api, Resource -import requests +import subprocess, tempfile, os, requests python_exec_api = Blueprint('python_exec_api', __name__, url_prefix='/run') api = Api(python_exec_api) +# todo: don't hardcode RUNNER_URL = "http://code_runner:8591/python" class PythonExec(Resource): def post(self): data = request.get_json(silent=True) or {} + code = data.get("code", "") + + if not code.strip(): + return {"output": "⚠️ No code provided."}, 400 + + is_production = os.environ.get("IS_PRODUCTION", "false").lower() == "true" + + if is_production: + return _execute_remote(data) + # might have to update this in future; could be vuln + # skipping verbose check + else: + return _execute_local(code) + + +def _execute_local(code): + with tempfile.NamedTemporaryFile(delete=False, suffix=".py") as tmp: + tmp.write(code.encode()) + tmp.flush() try: - response = requests.post( - RUNNER_URL, - json=data, - timeout=10 + result = subprocess.run( + ["python3", tmp.name], + capture_output=True, + text=True, + timeout=5, + cwd="/tmp", # Force working directory to /tmp + env={"HOME": "/tmp", "PATH": "/usr/bin:/usr/local/bin"} # Restricted environment ) + output = result.stdout + result.stderr + except subprocess.TimeoutExpired: + output = "⏱️ Execution timed out (5 s limit)." + except Exception as e: + output = f"Error running code: {str(e)}" + finally: + os.unlink(tmp.name) + + return {"output": output} + +def _execute_remote(data): + try: + response = requests.post( + RUNNER_URL, + json=data, + timeout=10 + ) - return response.json(), response.status_code + return response.json(), response.status_code - except requests.Timeout: - return { - "output": "⏱️ Runner timed out." - }, 504 + except requests.Timeout: + return {"output": "⏱️ Runner timed out."}, 504 - except requests.RequestException as e: - return { - "output": f"❌ Could not connect to code runner: {str(e)}" - }, 502 + except requests.RequestException as e: + return { + "output": f"❌ Could not connect to code runner: {str(e)}" + }, 502 api.add_resource(PythonExec, "/python") From 09f379134dace2997a7b49b47f6064f2276f1c12 Mon Sep 17 00:00:00 2001 From: mataiodoxion <226399730+mataiodoxion@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:52:08 -0700 Subject: [PATCH 3/6] remove emojis from outputs --- api/python_exec_api.py | 6 +++--- runner/runner.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/python_exec_api.py b/api/python_exec_api.py index 9d409b6..b95b5e4 100644 --- a/api/python_exec_api.py +++ b/api/python_exec_api.py @@ -43,7 +43,7 @@ def _execute_local(code): ) output = result.stdout + result.stderr except subprocess.TimeoutExpired: - output = "⏱️ Execution timed out (5 s limit)." + output = "Execution timed out (5 s limit)." except Exception as e: output = f"Error running code: {str(e)}" finally: @@ -62,11 +62,11 @@ def _execute_remote(data): return response.json(), response.status_code except requests.Timeout: - return {"output": "⏱️ Runner timed out."}, 504 + return {"output": "Runner timed out."}, 504 except requests.RequestException as e: return { - "output": f"❌ Could not connect to code runner: {str(e)}" + "output": f"Could not connect to code runner: {str(e)}" }, 502 diff --git a/runner/runner.py b/runner/runner.py index e1a7649..a9d2f96 100644 --- a/runner/runner.py +++ b/runner/runner.py @@ -9,7 +9,7 @@ def run_python(): code = data.get("code", "") if not code.strip(): - return {"output": "⚠️ No code provided."}, 400 + return {"output": "No code provided."}, 400 with tempfile.NamedTemporaryFile(delete=False, suffix=".py") as tmp: tmp.write(code.encode()) @@ -26,7 +26,7 @@ def run_python(): ) output = result.stdout + result.stderr except subprocess.TimeoutExpired: - output = "⏱️ Execution timed out (5 s limit)." + output = "Execution timed out (5 s limit)." except Exception as e: output = f"Error running code: {str(e)}" finally: From 42a93af6c110941ec46ccdcbffa8507363924532 Mon Sep 17 00:00:00 2001 From: mataiodoxion <226399730+mataiodoxion@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:50:10 -0700 Subject: [PATCH 4/6] testing; drop all caps for runner container --- api/python_exec_api.py | 2 ++ docker-compose.yml | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/python_exec_api.py b/api/python_exec_api.py index b95b5e4..17574ef 100644 --- a/api/python_exec_api.py +++ b/api/python_exec_api.py @@ -20,10 +20,12 @@ def post(self): is_production = os.environ.get("IS_PRODUCTION", "false").lower() == "true" if is_production: + print("running locally...") return _execute_remote(data) # might have to update this in future; could be vuln # skipping verbose check else: + print("running remotely...") return _execute_local(code) diff --git a/docker-compose.yml b/docker-compose.yml index f98775b..a671355 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: restart: unless-stopped code_runner: - image: flask_runner - build: ./runner - restart: unless-stopped + image: flask_runner + build: ./runner + cap_drop: + - ALL + restart: unless-stopped From 4238d4f7082939c49e0b3b0ef12078b9423016cf Mon Sep 17 00:00:00 2001 From: osteostriga <226399730+mataiodoxion@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:02:26 -0700 Subject: [PATCH 5/6] fix: containerization of coder runner; adjustments to cached docker image? --- api/python_exec_api.py | 2 -- docker-compose.yml | 1 - runner/Dockerfile | 6 +++--- runner/requirements.txt | 1 + 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/api/python_exec_api.py b/api/python_exec_api.py index 17574ef..b95b5e4 100644 --- a/api/python_exec_api.py +++ b/api/python_exec_api.py @@ -20,12 +20,10 @@ def post(self): is_production = os.environ.get("IS_PRODUCTION", "false").lower() == "true" if is_production: - print("running locally...") return _execute_remote(data) # might have to update this in future; could be vuln # skipping verbose check else: - print("running remotely...") return _execute_local(code) diff --git a/docker-compose.yml b/docker-compose.yml index a671355..22a7a21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ version: '3' services: web: - image: flask_open build: . env_file: - .env # This file is optional; defaults will be used if it does not exist diff --git a/runner/Dockerfile b/runner/Dockerfile index 2cb1bc2..d14dba3 100644 --- a/runner/Dockerfile +++ b/runner/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.12-slim WORKDIR /app -# Install python deps first for better layer caching +# Install Python deps first for better layer caching COPY requirements.txt /app/requirements.txt RUN pip install --no-cache-dir --upgrade pip \ && pip install --no-cache-dir -r /app/requirements.txt @@ -12,5 +12,5 @@ COPY . /app EXPOSE 8591 -# Run the websocket service -CMD ["python", "runner.py"] +# Run Flask with Gunicorn +CMD ["gunicorn", "--bind", "0.0.0.0:8591", "--workers", "2", "runner:runner"] diff --git a/runner/requirements.txt b/runner/requirements.txt index 7e10602..e4a286c 100644 --- a/runner/requirements.txt +++ b/runner/requirements.txt @@ -1 +1,2 @@ flask +gunicorn From 625682e21dbb125eb1446f392bd98c7349b0b388 Mon Sep 17 00:00:00 2001 From: osteostriga <226399730+mataiodoxion@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:10:31 -0700 Subject: [PATCH 6/6] add image back to docker-compose --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 22a7a21..a671355 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ version: '3' services: web: + image: flask_open build: . env_file: - .env # This file is optional; defaults will be used if it does not exist