Skip to content

Services

The API's domain logic lives in service modules under dps-code-api/. Each module owns a bounded set of rules and a data contract; route handlers in app.py remain thin adapters over them. The general conventions:

  • Services never import app; they depend on db_access (data + serialization), authz (gates), and environment (config) only.
  • Every service documents what it owns and what it must not do directly.
  • Authoritative multi-step mutations are delegated to the named RPCs — services do not recreate them with sequences of table writes.
  • Canonical events are recorded through event_service (application path) or by the transactional RPCs (same-transaction path).

reports_service.py — report domain operations

  • Owns: the report data contract — REPORT_ACTIONS (validate/invalidate/ investigate/contact_reporter/claim with their required/new statuses), PATCHABLE_FIELDS, BOT_ONLY_PATCHABLE_FIELDS, the status sets, and the server-side report list (rpc_list_reports).
  • Owns: the shared operations perform_report_action and conclude_investigation — the single application paths for report state transitions.
  • May modify: reports, timeline, pending_actions, contact_messages, events — but only through rpc_report_action, rpc_create_report, and friends.
  • Must not: implement its own status transitions with bare table writes.
  • Database interaction: pre-flight reads (db_get_report_row), then the authoritative RPC; maps RPC error tags to HTTP responses.

punishment_service.py — punishment domain contract

  • Owns: PUNISHMENT_STATUSES, PUNISHMENT_ID_RE (AA0AA0), EDITABLE_PUNISHMENT_FIELDS, PROTECTED_PUNISHMENT_FIELDS, PUNISHMENT_REQUIRED_CREATE_FIELDS, expires_at normalization, and the server-side punishment list.
  • May modify: punishments and discipline_queue — through rpc_punishment_create/update/revoke and the discipline RPCs.
  • Must not: accept client-supplied punishment IDs, issuer identity, or timestamps (protected fields → 400).
  • Database interaction: db_get_punishment_row reads + RPC mutations + batched display enrichment (issuer names, deliveries, profiles).

agents_service.py — agent data contract

  • Owns: AGENT_REQUIRED_CREATE_FIELDS, AGENT_PATCHABLE_FIELDS, VALID_RANKS. Pure constants, no logic.

event_service.py — canonical event interface

  • Owns: the EVENT_TYPES taxonomy and the application-level writers record_event / record_report_event (thin wrappers over rpc_write_event).
  • Must not: be bypassed by ad-hoc event inserts from route code — route-level event recording goes through this module or through the transactional RPCs.
  • Behavior: best-effort — a failed event write is logged and never fails the request that produced the activity. (The transactional RPC path is the strict one: events inside the mutation's transaction are mandatory.)

admin_service.py — admin helpers

  • Owns: _admin_identity() (session-derived (agent_id, agent_name, correlation_id) for audit rows) and health-history recording (_record_and_load_health_history, best-effort).

health_service.py — admin health formatting

  • Owns: the transformation of raw rpc_admin_health metrics into the status-site payload: 10 sections (overall, database, api, actions, queue, agents, evidence, auth, audit, events), each rated ok/warn/error, overall = worst section. Pure transformation — no request context, no database access.

oauth_service.py — Discord OAuth helpers

  • Owns: _discord_token_exchange (proxy-first, direct fallback), _validated_return_to (allowlisted origins only), discord_json_request, DiscordAPIError.
  • Must not: be called with secrets in logs; error bodies are logged, never the client secret.

contact_threads.py — contact thread data

  • Owns: db_get_contact_messages and serialize_contact_message. Read-only helpers; mutations go through rpc_contact_reply / rpc_contact_respond / rpc_close_contact_thread.

action_queue.py — report action queue

  • Owns: stale-action requeue (rpc_requeue_stale_actions, 10-minute timeout, 3-attempt cap), atomic claim (rpc_claim_action), queue listing, _to_unix for Discord timestamps.
  • May modify: pending_actions — through the queue RPCs.
  • Must not: perform the bot's Discord-side work; it only hands work to the bot and records outcomes.

discipline_queue.py — discipline delivery queue

  • Owns: the delivery queue helpers: stale requeue (10 min / 3 attempts), atomic claim with the live punishment embedded, completion error mapping, and the delivery list. Mirrors action_queue.py.
  • Must not: touch punishment rows directly; enqueues are transactional with the punishment RPCs.

training_engine.py — scenario evaluation

  • Owns: pure condition evaluation: condition_met (recursive action/ objective/evidence/any/all/trainer trees), evaluate_objectives, evaluate_evidence_unlocks, default_session_state, validate_action_type.
  • Must not: touch the database or Flask — it is a pure function of the scenario definition and state.

training_service.py — training serialization + reads

  • Owns: training serializers (_serialize_scenario, _serialize_note_row, _serialize_injection_row, _serialize_training_event), role/elapsed-time helpers, and request-scoped single-row reads.
  • May modify: nothing — reads only.

training_runtime.py — training session orchestration

  • Owns: the session state machine: _update_session_row, scenario instance activation, _record_training_event (persist + broadcast), _apply_engine (persist engine outcomes), _session_payload (the 4-parallel- read detail builder), _session_list_item, and the gates (_training_login_agent, _is_trainer, _trainer_control, _training_session_or_error).
  • May modify: training tables (sessions, session scenarios, events) — the API is the only writer.
  • Must not: trust client-supplied roles, statuses, or actor identity; the actor is always derived from the session.
  • Realtime: _rt_broadcast / _broadcast_session / _broadcast_user are best-effort; broadcast failure never fails the persisted mutation.

authz.py — authentication/authorization

  • Owns: rank/clearance constants, verify_api_key, active_agent_from_session, has_permission, supervisor/reassign/Director checks, is_assigned_agent, authorize_dashboard, supervisor_report_denied, _is_admin_user, require_admin, _admin_capabilities.
  • Must not: be bypassed by routes; the DB RPCs independently re-check the critical guards.

db_access.py — low-level data access + serialization

  • Owns: the request-scoped supabase proxy, table-name constants, db_get_* readers, Discord profile normalization/upsert/read, serializers (serialize_report/note/evidence/timeline/agent/punishment/profile), batch name maps, and identifier scrubbing (_build_identifier_replacements, _scrub_user_ids).
  • Must not: contain business rules (status machines, clearance) — that is the services' job.

environment.py — configuration

  • Owns: EnvironmentConfig (production/demo), per-request environment resolution, environment-bound session cookies, request-scoped Supabase clients, optional ingress HMAC verification.
  • Must not: leak secret values; configuration is never logged.

erlc_relay.py — ER:LC integration client

  • Owns: relay selection, failover ordering, the per-relay circuit breaker, and fetch_player (the ER:LC player lookup). Routes only call fetch_player and never see relay URLs, tokens, or connection details.

How services interact with the database

Two patterns, used deliberately:

  1. Transactional RPCs for operations with multiple writes or race requirements (report actions, notes, contact, queue claims/completions, deletions, agent updates, punishments, deliveries, events). One call; the database owns the transaction.
  2. Direct table reads/writes for simple, single-row, low-risk operations (reads everywhere; training-table writes; updated_at bumps). When a direct write would duplicate an existing RPC, use the RPC.

The rule that keeps them consistent: application code should not recreate authoritative multi-step database operations when an atomic database operation already exists.