Files
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

64 lines
2.1 KiB
YAML

steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'-f', 'deploy/Dockerfile',
'-t', 'gcr.io/$PROJECT_ID/automoderate:$BRANCH_NAME-$SHORT_SHA',
'.'
]
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/automoderate:$BRANCH_NAME-$SHORT_SHA']
# Deploy container image to Cloud Run.
#
# This step intentionally does NOT manage environment variables or secrets.
# Runtime config is owned by the Cloud Run service itself (console or
# `gcloud run services update`) so that:
# 1. Secret-backed env vars (SECRET_KEY, DATABASE_URL, OPENAI_API_KEY,
# ADMIN_PASSWORD, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET) set via
# `--set-secrets` survive every deploy. `--set-env-vars` here would
# convert them into plain env vars and lose the secret binding.
# 2. Per-environment values (FLASK_CONFIG=development for the dev service,
# FLASK_CONFIG=production for the prod service) are configured once on
# the service and persist across image-only deploys.
# If a brand-new service is being stood up, configure its env vars and
# secrets manually via the console before the first cloudbuild run, or
# extend this step with `--update-env-vars` / `--set-secrets`.
- name: 'gcr.io/cloud-builders/gcloud'
args:
- 'run'
- 'deploy'
- 'automoderate-${_ENV}'
- '--image'
- 'gcr.io/$PROJECT_ID/automoderate:$BRANCH_NAME-$SHORT_SHA'
- '--region'
- '${_REGION}'
- '--platform'
- 'managed'
- '--allow-unauthenticated'
- '--port'
- '6217'
- '--memory'
- '${_MEMORY}'
- '--cpu'
- '${_CPU}'
- '--max-instances'
- '${_MAX_INSTANCES}'
- '--add-cloudsql-instances'
- '${_CLOUD_SQL_INSTANCE}'
substitutions:
_ENV: 'dev'
_REGION: 'us-central1'
_MEMORY: '1Gi'
_CPU: '1'
_MAX_INSTANCES: '10'
# _CLOUD_SQL_INSTANCE must be set in the trigger configuration
# (format: project:region:instance-name).
options:
logging: CLOUD_LOGGING_ONLY