feat/37 - #82
Merged
Merged
feat/37#82
Conversation
…ile-timeout feat(backend): enforce 15s compile timeout (#25)
feat(frontend): replace terminal panel with xterm.js interactive terminal (#27)
…rategy feat(backend): implement PtyExecutionStrategy (#29)
…-pty feat(backend): bridge websocket and pty streams (#30)
Fixes for issue #30: - Change gunicorn worker from GeventWebSocketWorker to gevent (flask-sock's simple-websocket was incompatible with gevent-websocket) - Use container.create() + put_archive() instead of bind mount (bind mount fails between sibling containers) - Start reader thread before container.start() to avoid race condition (fast programs finish before reader starts) - Remove read_only=True to allow put_archive to write binary - Remove tmpfs on /sandbox (tmpfs hid the uploaded binary)
The frontend sends the Supabase JWT token via WebSocket subprotocol (bearer.<token>) which is transmitted in the Sec-WebSocket-Protocol header. Without this header being forwarded, the backend cannot authenticate the WebSocket connection and closes it with 'missing_bearer_token'.
Supabase access_tokens use ES256 (ECDSA) algorithm, not HS256. - Add ES256 to allowed algorithms in verify_supabase_jwt() - Add cryptography dependency required for ES256 support
Supabase access_tokens use ES256 (ECDSA) which requires a public key, not a symmetric secret. This fix: - Detects the token algorithm (ES256 vs HS256) - For ES256: fetches the public key from Supabase JWKS endpoint and verifies with it - For HS256: verifies with the JWT secret as before - Caches the JWKS client to avoid repeated fetches
Browsers expect the server to respond with a Sec-WebSocket-Protocol header when the client sends one. simple-websocket's default choose_subprotocol returns None when no subprotocols are configured, causing the browser to fire onerror. Fix: monkey-patch choose_subprotocol to accept the first subprotocol the client sends, so the 101 response includes the expected header.
- Replace PyJWKClient with manual JWKS fetch + EC key construction - Fallback to HS256 if ES256+JWKS fails - Log JWT alg, kid, and any verification errors - Add logging.basicConfig in create_app
feat(backend): bridge websocket and pty streams
The PtyExecutionStrategy main loop was breaking on every ws.receive(timeout=0.05) that returned None (normal timeout), which killed the container before any stdin could be forwarded. This prevented the interactive leia flow from working — the program would block on sys_read, but the backend loop would exit within 50ms, never receiving the user's input. Changing break to continue keeps the loop alive, polling stdout, wall-clock timeout, and incoming WebSocket messages (stdin, stop, ping) as intended.
fix(backend): keep execution loop alive on ws.receive timeout
feat(backend): implement websocket protocol events (#32)
feat(frontend): wire stop action to websocket
- exit event skipped when timed_out=True - container uses read_only=True with binary on tmpfs - container.stop() for graceful shutdown - frontend preserves error state on ws close - tests for timeout flow
gevent-websocket conflicts with the subprotocol monkey-patch used for JWT auth. flask-sock falls back to simple-websocket which correctly handles bearer.<token> subprotocol.
feat(backend): enforce 10s execution timeout (#34)
…-timeout feat(devops): set docker hard stop timeout to 12s
- Set read_only=True and replace put_archive with bind mount ro - Add tmpfs at /tmp for writable temp storage - Remove unused io, os, tarfile imports
… workers Substitui o SlidingWindowRateLimiter in-memory (per-worker) pelo flask-limiter com Redis, garantindo que o limite de 30 runs/min seja compartilhado entre todos os workers do gunicorn. Adiciona serviço redis ao docker-compose.yml e injeta o limiter no blueprint REST e no handler WebSocket.
Adiciona try/except na conexao Redis na inicializacao. Se o Redis nao estiver pronto, faz fallback para memory:// com warning. Adiciona healthcheck no Redis no docker-compose.
Bind mount do tmpdir do Windows falha no Docker Desktop (caminho C:\Users\... nao monta como /sandbox). Agora copia o binario via put_archive() para /tmp dentro do container.
Em vez de put_archive/bind mount, o binario agora e codificado em base64 e passado via comando sh -c. O container decodifica e executa qemu com o binario estatico em /tmp/programa.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #37
O que mudou
Adiciona rate limit de 30 execuções/min por usuário e 120/min por IP usando flask-limiter com backend in-memory (
memory://).Alterações
SlidingWindowRateLimiter— sliding window in-memory (mantido como módulo standalone)GET /api/limits— endpoint público com limites atuais@limiter.limit("{runs_per_minute}/minute", key_func=_user_key)para rate por user_id + IPlimiter.limit(...)como context manager no handler WebSocketLimiterinicializado commemory://, injetado nos blueprints e WebSocketruns_per_minute(30) eruns_per_minute_ip(120)flask-limiter>=3.0.0RUNS_PER_MINUTEeRUNS_PER_MINUTE_IPno backendTestes
test_rate_limiter.py: 10 testes (unidade do custom limiter + integração com flask-limiter)Limitação conhecida
O backend usa 4 workers do gunicorn (
-w 4). O backend in-memory é por worker, então o limite efetivo é30 × 4 = 120execuções/min. Para um limite exato entre workers, seria necessário Redis (descartado por enquanto devido a problemas de conexão).Como testar
docker compose down && docker compose up -d --build/api/compile429 rate_limit_exceeded