LixinDu/math-ai-solver
Math AI Solver (execution backend)
A small, authenticated HTTP service that validates and executes Python programs written for the math_for_AI_assistant project, using SymPy, NumPy, and Plotly.
This Space is the execution backend only. It renders no UI and holds no user accounts. The browser interface, login, and model calls live in the main application.
What this service does
Endpoints
POST /solve
Request:
{"code": "import sympy as sp\nprint(sp.factor(12))"}Response (matching src/executor's ExecutionResult fields):
{
"passed": true,
"status": "completed",
"timed_out": false,
"stdout": "12\n",
"stderr_summary": null,
"plotly_json": null
}plotly_json, when present, is a JSON object (the figure), bounded to 512 KiB. Execution failures retain their structured status and error details rather than collapsing into a generic error.
GET /healthz
Returns {"status": "ok"}. Requires no authentication so the platform's health probe can reach it. It exposes no information beyond liveness.
GET /
Describes the service: name and available endpoints. Requires no authentication and exposes no configuration.
This exists so the Space's embedded App view does not display "Not Found". Without a root route, GET / correctly returns 404, which makes the Space page look broken even when the service is healthy. The interactive API docs (/docs, /openapi.json) remain disabled deliberately — the schema would advertise the request shape to unauthenticated callers.
Authentication
POST /solve requires a bearer token:
Authorization: Bearer <SOLVER_TOKEN>SOLVER_TOKEN is a dedicated random value. It is stored as a Space secret (HF Settings → Secrets) and as a Cloudflare Worker secret. It never appears in browser code, request bodies, or logs.
The token is compared with hmac.compare_digest (constant time). Authentication runs before the request body is read or parsed, so an unauthenticated caller cannot make the service buffer and parse a payload.
Distinct HTTP statuses are used so callers can tell the cases apart:
Note the deliberate split: authentication failures, size limits, overload, and internal errors are HTTP errors, while code rejection and execution failure are normal responses carrying structured detail. Callers should not treat a failed computation as a transport error.
An internal failure returns a JSON body ({"detail": ...}) and logs the traceback to the Space logs. The response never echoes exception text, paths, or stack frames.
Telling two different failures apart. Hugging Face's proxy answers with its own opaque page — historically the bare text Internal Server Error — when a request never reaches this application (for example, an unauthenticated request to a private Space). Our handler cannot change that, because we never see the request. What it does change is the other case: a failure inside the service now returns JSON, so a caller can distinguish "the solver broke" from "the edge broke" without reading the logs.
Execution limits
Limits are enforced inside the sandbox per process, not by the host:
A larger host does not raise these caps. RLIMIT_AS bounds virtual address space, not RSS — Python plus NumPy, Plotly, and SymPy map significant virtual memory, so the default is sized for the import stack while still catching runaway allocations.
Concurrency starts at 1. Measure before raising it. Simultaneous heavy requests on a 2 vCPU instance will starve each other and produce spurious CPU/OOM failures that look like bugs in the submitted code.
Isolation: read this before exposing the service
The AST allowlist and the subprocess are useful controls, but they do not establish complete filesystem, network, or secret isolation. Blocking write_html does not prove that all file access is blocked. The allowlist is a denylist of import names and call attributes, not a sandbox boundary.
Bearer authentication controls who may call the service. It does not make submitted code trustworthy.
Treat this service as untrusted-code-adjacent: keep it free of other secrets, do not co-locate data with it, and do not assume a submitted program cannot escape its intended bounds.
Vendored code and upstream pinning
This Space is self-contained. It carries a versioned copy of:
src/validation/code_security.py→math_ai_solver/validation/code_security.pysrc/executor/sandbox.py→math_ai_solver/executor/sandbox.py
Upstream: math_for_AI_assistant
- Commit:
ebab6dc4702c8ae8fce9d069faac259a9f77723f - Branch:
public/teachers - Date: 2026-09-14
UPSTREAM_SHA in math_ai_solver/__init__.py records this, and tests/test_upstream_sync.py verifies the copies still match the recorded content hashes. A security fix in the upstream validator must be ported here. Until a shared package exists, the copies can drift, and a fix that lands only upstream would leave this service more permissive than the application it serves.
Maintainers: the maintainer-side rationale, drift procedure, and re-vendor steps are recorded privately in the source repository (outside this Space, in deploy/docs/). This folder is uploaded verbatim, so internal notes are deliberately kept out of it.
To update: copy the modules, re-record UPSTREAM_SHA and the hashes, and update tests/test_upstream_sync.py. Make it an explicit, reviewed commit.
Local development
cd deploy/hf-space
python -m venv .venv && source .venv/bin/activate
python -m pip install -r requirements.txt -r requirements-dev.txt
python -m pytest tests -qThe suite also runs from the repository root:
python -m pytest deploy/hf-space/tests -qThe package is named math_ai_solver rather than app deliberately: this repository already has a top-level app.py (the Streamlit interface), and a package called app shadows it during pytest collection, which breaks the main project's tests.
Run locally:
SOLVER_TOKEN=local-test-token python -m uvicorn math_ai_solver.main:app --port 7860curl -s -X POST http://127.0.0.1:7860/solve \
-H 'Authorization: Bearer local-test-token' \
-H 'Content-Type: application/json' \
-d '{"code": "import sympy as sp\nprint(sp.factor(12))"}'Deployment
The Space repository is flat at its root: README.md, Dockerfile, requirements.txt, and math_ai_solver/ all sit at the top level. This folder is the staging area for those contents.
Set the secret before going live:
- HF Space → Settings → Secrets →
SOLVER_TOKEN - Cloudflare Worker → Secrets →
SOLVER_TOKEN(same value)
