11 Commits
Author SHA1 Message Date
Bentlybro 9564b63bc2 Pin both Dockerfiles to python:3.12-slim
docker/Dockerfile used 3.14-slim and deploy/Dockerfile used 3.11-slim.
Unify on 3.12 — the version the CI matrix tests against and the local
venv runs — so the image matches what's actually validated. Update the
docs that referenced 3.11 to match.
2026-07-06 13:31:04 +01:00
Bentlybro a38762dd05 Switch gunicorn worker from eventlet to gthread
Cloud Run was 500-ing every async route on the eventlet worker:

  RuntimeError: asyncio.run() cannot be called from a running event loop
  RuntimeError: You cannot use AsyncToSync in the same thread as an
                async event loop - just await the async function directly.

Root cause: standard Flask decorators (@login_required from Flask-Login,
@limiter.limit from Flask-Limiter, and Flask's own async-view wrapper)
call ``flask.current_app.ensure_sync(obj)(*a, **k)``, which uses
``asgiref.AsyncToSync``, which calls ``asyncio.run()``. Under eventlet's
greenlet hub ``asyncio.run()`` raises because the hub *is* the running
loop. Every ``async def`` route wrapped by any of those decorators (i.e.
all of /api/moderate, /auth/*, /dashboard/*) dies at the decorator
boundary before the view body ever executes. Sentry AUTOMOD-3B/3C/3D.

The dev server didn't show this because werkzeug gives each request a
fresh OS thread with no existing event loop, so asgiref can spin up its
own loop fine.

gthread worker + SOCKETIO_ASYNC_MODE=threading:
- Real OS threads for each request → asgiref's AsyncToSync works.
- Flask-SocketIO threading mode supports WebSockets for our
  dashboard-scale realtime traffic.
- Dev (run.py) and prod now use the same async mode, so behaviour
  matches end-to-end.
- 50 threads/worker = plenty of concurrency for HTTP + long-lived
  WebSocket connections.

Dropped eventlet and psycogreen from requirements.txt — they were pinned
specifically for the eventlet worker, neither is used any more.
2026-04-22 14:22:26 +01:00
Bentlybro 478ca18ad8 Phase 0 hardening + batched AI moderation + websocket/perf fixes
Security / correctness
- Privilege escalation: rule/api-key/project-update routes now gate on
  can_manage_members instead of is_member; plain members could previously
  rewrite or disable moderation rules and delete API keys.
- Project.is_owner() AttributeError on non-admin API-user deletion (route
  + template) replaced with user_id comparison.
- Flask-Limiter on /auth/login, /auth/register, /auth/change-password,
  and /api/moderate (per API key id).
- Remove silent SECRET_KEY fallback: docker-compose fails hard if unset;
  production config validator refuses to boot without SECRET_KEY and
  OPENAI_API_KEY.
- Require verified email in Google OAuth auto-link flow.
- User-supplied regex now evaluated via the regex package with a 1s
  timeout, closing the ReDoS vector.
- Cap batched AI chunk count (max 40) and concurrent workers (200 -> 10).
- openai>=2.32 (from 1.55.3 so reasoning_effort kwarg works); add regex,
  Flask-Limiter, gunicorn, eventlet, psycogreen; drop the stdlib-shadowing
  pypi asyncio package.

Production runtime
- New wsgi.py: eventlet.monkey_patch + psycogreen.patch_psycopg so DB
  queries cooperate with the event loop; sets SOCKETIO_ASYNC_MODE=eventlet
  for app startup.
- Dockerfiles run as non-root app user and CMD gunicorn with
  --worker-class eventlet. run.py refuses FLASK_CONFIG=production so a
  misconfigured deploy fails loudly.
- cloudbuild.yaml stops managing env vars/secrets on deploy — it was
  about to clobber secret-backed env vars with plain ones. Runtime config
  is now owned by the Cloud Run service.

SocketIO async-mode fix (was causing full site lockup during API calls)
- async_mode picked via SOCKETIO_ASYNC_MODE env var, default "threading".
  The previous sys.modules auto-detect false-positived because
  flask_socketio / python-engineio import eventlet on load whenever it is
  installed, pinning a non-monkey-patched eventlet server in front of
  blocking stdlib I/O.

Real-time dashboard updates
- New content_received WebSocket event fires when the API accepts a
  submission; UI renders a "Processing…" row immediately and updates it
  in place when moderation_update arrives.
- Fix stale updated_at timestamp in moderation_update payloads.

Batched AI rule evaluation
- Replace the per-rule parallel fan-out (N OpenAI calls) with a single
  batched call evaluating all AI-prompt rules at once. On a 6-rule
  default project this drops safe-content cost from 5-12s to ~1-4s.
- response_format=json_object + max_completion_tokens=4000 so the model
  emits strict JSON and stops at the close-brace.
- OPENAI_REASONING_EFFORT config: minimal/low/medium/high for gpt-5 /
  o-series; accepts "none" on gpt-5.4. Works around the gpt-5-nano case
  where reasoning tokens ate the full output budget and returned empty
  responses.

Client errors
- Malformed / non-UTF-8 JSON bodies now return 400 with a clear message
  about UTF-8 encoding instead of a generic 500.

Logging
- Single INFO line per moderation: the existing
  "Content <id>: decision in Zs" summary.
- Per-stage timing, websocket scheduling, and batched-AI call details
  moved to DEBUG so they are available when diagnosing latency without
  cluttering steady-state logs.
2026-04-21 16:01:18 +01:00
Bentlybro 3f008c0031 Revert "Switch to Gunicorn for production deployment"
This reverts commit 8f08dd39a3.
2025-09-29 11:16:40 +01:00
Bentlybro fa0cd3e726 Revert "Switch Gunicorn worker from eventlet to gevent"
This reverts commit 11e790eec1.
2025-09-29 11:16:33 +01:00
Bentlybro 11e790eec1 Switch Gunicorn worker from eventlet to gevent
Replaces eventlet with gevent in both Dockerfile and requirements.txt for Gunicorn worker class. This may improve compatibility and performance for async workloads.
2025-09-29 11:09:23 +01:00
Bentlybro 8f08dd39a3 Switch to Gunicorn for production deployment
Updated Dockerfile to use Gunicorn with eventlet for production server. Added gunicorn and eventlet to requirements.txt. Modified run.py to use debug mode for local development only, separating production and development server configurations.
2025-09-29 10:46:35 +01:00
Bentlybro 8f64cb8bba Update Dockerfile 2025-09-25 13:38:33 +01:00
Bentlybro 99213fde90 Fix Dockerfile requirements.txt copy path
Changed the COPY command to use './' for requirements.txt to ensure correct file path handling during Docker build.
2025-09-25 13:25:29 +01:00
Bentlybro 65ca7e1da0 Add Cloud SQL instance to Cloud Build deployment
Updated cloudbuild.yaml to include the --add-cloudsql-instances flag and reference the _CLOUD_SQL_INSTANCE variable. Sensitive variables are now documented as required to be set in trigger configuration for improved security.
2025-09-25 12:34:43 +01:00
Bentlybro 2718ae5797 Add GCP deployment configuration files
Introduces Dockerfile, Cloud Build YAML, .gitignore, and README for deploying AutoModerate to Google Cloud Platform. These files support Cloud Run deployment, CI/CD automation, and secure environment variable management.
2025-09-25 12:28:53 +01:00