Skip to content

Training Center

The Training Center is a parallel, simulated investigation environment for onboarding agents. Trainers run data-driven scenarios with trainees; trainees investigate simulated dockets with objectives, evidence unlocks, and live trainer interaction. It shares the Flask API, the Discord-OAuth browser session, and the Supabase project of the production system — there is no second API, no second database, and no second authentication system — but simulated state is fully separate from production cases.

The training frontend is the same Vite bundle as the dashboard, booted when the hostname is training.eccdps.org (or any path beginning /training).

Trainers and trainees

  • Trainer: an agent with agents.is_trainer = true, or any supervisor rank (Senior Agent +). Trainer access is checked on every request against the live agents row (_is_trainer in training_runtime.py). The is_trainer flag is manageable through the admin Agents tab (admin_agents, audited).
  • Trainee: an agent whose status is exactly onboarding. The trainee selector (/training/trainees) returns only status = onboarding agents — no role/rank filters.

Training sessions

A session pairs one trainer with one trainee. Sessions are parent containers: a session can begin with zero scenarios, and scenarios (simulated dockets) are activated into the session as training progresses.

Session lifecycle

text
pending → active ⇄ paused → completed
    │                        │
    └──────► aborted ◄───────┘
StatusMeaningWho transitions
pendingCreated, not started
activeInvestigation phase runningtrainer: start, resume, restart
pausedTimer stopped; trainee stays locked in the session shelltrainer: pause
completedEnded or graded (score/result set)trainer: end, evaluate
abortedPrematurely ended; trainee sees the no-active statetrainer: abort

Every lifecycle transition is a trainer control (_trainer_control: browser session + trainer role + the caller must be the session's assigned trainer). The trainee never starts, pauses, or ends a session — trainees get 403 on all trainer-control routes.

Scenario instances

Scenario definitions live in training_scenarios (title, difficulty, category, prologue, and a JSON definition holding evidence items, objectives, and condition trees). When a trainer activates a scenario into a session, the API creates a training_session_scenarios child row with its own engine state, and the child becomes the session's current scenario pointer. Activating a new scenario completes any currently active one. The trainee sees a waiting screen while the parent is active but no child is active.

Session state and engine

The scenario engine (training_engine.py) is pure evaluation logic driven by the scenario definition:

text
condition schema:
  { "type": "action",    "event": "OPEN_REPORT", "target": "reporter" }
  { "type": "objective", "id": "review_report" }
  { "type": "evidence",  "key": "cctv" }
  { "type": "any" / "all", "items": [ ... ] }
  { "type": "trainer" }        // true only for trainer overrides

Trainee actions (whitelisted in TRAINEE_ACTION_TYPES — open docket/report/ profile/timeline/evidence, note/evidence submission, status changes, investigation start/submit) are recorded as training_events rows with a client-generated client_id for idempotency, then the engine evaluates objective completion and evidence unlocks against the scenario instance's state. Engine outcomes are persisted and broadcast live.

Trainer Control Center

TrainerControlCenter.tsx provides:

  • Parent lifecycle: begin (start), pause, resume, restart, reset, end, abort.
  • Scenario catalog loading and activation.
  • Objective complete/reset and evidence release/lock overrides.
  • Information injections (supplemental report, witness statement, admin info, custom message, simulated evidence).
  • Private notes and note release (trainee notes are visible to the trainer for monitoring; trainer notes start private).
  • Grading: score (0–100), pass/fail, private comments, released feedback.
  • Live activity feed, presence (trainee online), and troubleshooting/recovery controls (Reconnect / Full Reconciliation).

All controls call existing API endpoints; the UI never writes Supabase or forces a status directly.

Synchronization and realtime behavior

The architecture is:

text
API/database mutation → persistent state/event → best-effort Realtime broadcast
→ recipient receives event → authoritative GET/reconciliation
  • The API writes the authoritative row, records the persistent event, then broadcasts over the Realtime REST endpoint to the private channel training:session:<uuid> (both parties) and personal channels training:user:<agent_id> (lifecycle events).
  • Broadcast failure is logged and never fails the already-persisted request.
  • The browser joins private channels with a short-lived JWT minted by /training/realtime-token (role=authenticated, sub=<discord_id>, default TTL 600 s) and refreshes it before expiry (setAuth).
  • Channel authorization is RLS on realtime.messages backed by training_session_members (see Row Level Security).
  • The frontend hook (src/training/realtime.ts) reports idle/connecting/connected/reconnecting/disconnected and never pretends sync is active. Detail pages perform an authoritative GET after connect and a debounced GET after event bursts, so a missed broadcast cannot corrupt UI state.

Authorization

Route classGate
All /training/*Browser session only; the bot API key is rejected (403)
Trainer routesTrainer role (is_trainer flag or supervisor rank)
Trainer controlsMust be the session's assigned trainer
Trainee routesMust be the session's trainee; only their own active/paused/completed sessions
Trainee detail on pending/aborted409 NO_ACTIVE_SESSION (trainee cannot re-enter an aborted or never-started session by manipulating the id)
Realtime joinAPI-minted JWT + RLS membership policies

Trainee frontend visibility is equally locked: TraineeShell exposes no trainer/admin navigation and routes the trainee into their own active/paused session or the no-active landing.

Relevant database policies

  • Training tables are RLS-enabled with the service role as the only data path; browser JWTs receive no normal table access.
  • training_session_members has a narrow authenticated SELECT policy for a user's own membership rows (the basis for Realtime authorization).
  • Realtime private-channel policies on realtime.messages gate session/user channel access; the Realtime server's claims-less authorization-probe INSERT is permitted without granting table reads (migrations 026/027).
  • training_events carries a unique (session_id, client_id) index for idempotent action recording.