CANION — Kickoff Document
Mission
Create exportable, self-contained training exercises that simulate a “fake internet” end-to-end. Author once, run deterministically, review confidently. Keep the exercise as the single source of truth.
Principles
- Exercise-centric: one document defines world, content, timeline, automation, and runtime state.
- Configuration-driven: managers edit resources; the timeline executes them.
- Deterministic and portable: export/import as JSON + blobs.
- Minimal surfaces: two Mongo collections (
users,exercises). Add append-onlyexercise_eventsonly if necessary.
User Roles
- Platform Admin: manages platform users and any exercise.
- Exercise Admin: owns a single exercise.
- Controller: runs timeline, triggers injects.
- Participant: mapped to a PC (player character) within an exercise.
- PC: actor controlled by a participant.
- NPC: scripted/bot actor.
High-Level Flow
Admin Portal Structure
-
Exercises list with status: Draft, Prepped, Running, Paused, Ended, Archived.
-
Tools inside an exercise:
- Social Manager (draft content, templates, curation)
- Site Manager (clones, custom sites/pages)
- Personnel/Actors (PCs, NPCs, orgs, graph)
- Injects and Timeline (canonical execution surface)
- Logs (system/player/content events)
- Manage Exercise (lifecycle, access, export/import)
Execution Model
- Definition layer: Actors/Orgs, Sites/Pages, Seed Content, Templates.
- Execution layer: Timeline with injects producing atomic actions.
- Evaluation layer: Flags, scoring, logs, AAR.
Stack and Deployment
-
Next.js frontend (Admin + User portals)
-
Node.js API (auth, exercise CRUD, runtime engine, scheduler)
-
MongoDB (users, exercises)
-
Blob storage (media, long logs, exports)
-
Dockerized services:
frontend(Next.js)api(Node.js)mongoblob(e.g., MinIO/S3-compatible)
Data Model Overview
Schema Drafts (MVP, not locked; commented for options)
users (global auth and per-exercise mapping)
// users collection (platform identity)
// Notes:
// - Keep platform-level auth separate from in-world actors.
// - exerciseAccess maps a platform user to role + optional PC actor per exercise.
// - Optional: add MFA, session audit, passwordless later.
{
_id: ObjectId,
username: string, // unique
passwordHash: string, // bcrypt/argon2
roles: ["admin" | "author" | "observer" | "participant"],
exerciseAccess: [
{
exerciseId: ObjectId,
role: "admin" | "controller" | "participant" | "observer",
actorId: string | null // participant→PC binding; null for observers
// Alt design: allow multiple PCs per user if scenario demands multi-seat
}
],
status: "active" | "disabled",
org: string | null, // optional tenant label
createdAt: Date,
updatedAt: Date,
lastLoginAt: Date
}
// Indexes:
// - { username: 1 } unique
// - { "exerciseAccess.exerciseId": 1, "exerciseAccess.role": 1 }exercises (single source of truth)
// exercises collection (authoring + runtime)
// Notes:
// - Authoring sections are editable pre-run; runtime is write-once per event.
// - Use blob keys for large HTML/MD/media to keep doc <16MB.
// - Deterministic IDs (string) for stable export/import.
// ---- Root ----
{
_id: ObjectId,
name: string,
code: string, // short unique handle
version: number, // bump on lock/export
status: "draft" | "prepped" | "running" | "paused" | "ended" | "archived",
visibility: "private" | "shareable",
createdBy: { userId: ObjectId, username: string },
createdAt: Date,
updatedAt: Date,
// ---- Access (exercise-scoped) ----
access: {
admins: [{ userId: ObjectId, username: string }],
controllers: [{ userId: ObjectId, username: string }],
participants: [{ userId: ObjectId, actorId: string | null }],
inviteMode: "closed" | "code" | "open"
// Option: add per-feature permissions if needed
},
// ---- Scenario (authoring) ----
scenario: {
purpose: string, // training intent
synopsis: string, // narrative summary
tags: [string],
rulesOfEngagement: string, // markdown
notes: string
// Option: classification, visibility, legal notices
},
// ---- Orgs & Actors (authoring) ----
orgs: [
{
id: string, // "org_acme"
name: string,
type: "Company" | "Agency" | "NGO" | "Media" | "Criminal" | "Terrorist" | "Other",
summary: string,
attrs: Record<string, any> // freeform extension
}
],
actors: [
{
id: string, // "actor_jdoe"
type: "PC" | "NPC",
displayName: string,
orgId: string | null, // -> orgs.id
profile: {
bio: string,
avatarKey: string | null, // blob
traits: [string] // domain tags for behavior templates
},
accounts: [
{
siteId: string, // -> sites.id
handle: string, // @name / username
role: "owner" | "editor" | "member" | "none",
visibility: "public" | "private" | "unlisted",
credentials: { mode: "auto" | "manual" | "none", username?: string, password?: string }
// Option: per-account bot defaults (frequency caps)
}
]
}
],
graph: {
nodes: [{ id: string, kind: "actor" | "org", ref: string }],
edges: [
{
id: string,
from: string, to: string, // node ids
type: "ally" | "rival" | "employs" | "influences" | "custom",
weight: number | null,
notes: string
}
]
// Option: typed properties (time-bounded relations)
},
// ---- Sites & Content (authoring) ----
sites: [
{
id: string, // "site_google", "site_reddit_1"
type: "Google" | "Reddit" | "Facebook" | "Instagram" | "X" | "YouTube" | "Wiki" | "News" | "Blog" | "Forum" | "Custom",
label: string,
enabled: boolean,
options: Record<string, any>, // site-specific toggles
// Custom site builder (static or dynamic)
pages: [
{
id: string,
path: string, // "/topic/article-1"
kind: "static" | "dynamic",
contentKey: string | null, // blob (HTML/MD) if static
template: string | null, // serialized component tree if dynamic
data: any | null // schema-less for MVP
}
],
// Seed for social/news (kept small; offload heavy sets to blob)
seed: {
posts: [
{
id: string,
authorActorId: string | null,
t: Date,
text: string,
media: [{ key: string, kind: "image" | "video" | "doc" }],
tags: [string],
mentions: [string], // -> actors.id
links: [string],
reactions: [{ byActorId: string, type: string }],
comments: [
{ id: string, byActorId: string, t: Date, text: string, reactions: [{ byActorId: string, type: string }] }
],
visibility: "public" | "followers" | "private",
pinned: boolean
}
],
channels: [{ id: string, ownerActorId: string, title: string, description: string }],
searchHints: [{ query: string, results: [{ type: "post" | "page", siteId?: string, refId: string }] }]
// Option: relevance scoring knobs for search
}
}
],
// ---- Timeline & Injects (execution plan) ----
timeline: {
phases: [
{ id: string, name: string, startMin: number, endMin: number, notes: string }
]
// Absolute schedule derived at start; offsets enable replay
},
injects: [
{
id: string,
name: string,
description: string,
trigger: {
mode: "time" | "manual" | "conditional",
atMin?: number, // minutes from T0
condition?: { type: string, expr: any } // mini rule engine for MVP
},
actions: [
{
id: string,
type:
| "PublishPage"
| "CreatePost"
| "Comment"
| "DM"
| "SearchBoost"
| "ActivateBot"
| "DeactivateBot"
| "PhaseChange"
| "Banner"
| "Custom",
siteId?: string,
actorId?: string,
payload?: any
// Design rule: actions are atomic; chain multiple actions per inject
}
],
owner: { userId: ObjectId, username: string },
tags: [string]
}
],
// ---- Objectives & Flags (evaluation) ----
objectives: [
{
id: string,
title: string,
description: string,
weight: number, // 0..1
flags: [
{
id: string,
code: string, // unique within exercise
description: string,
detection: {
type: "event" | "pathContains" | "manual",
event?: { kind: string, actorId?: string, siteId?: string, contains?: string }
}
}
]
}
],
scoring: { mode: "sum" | "rubric", thresholds: { pass: number, merit: number } | null },
// ---- Automation hooks (bounded) ----
automation: {
aiEnabled: boolean,
provider: "openai" | "bedrock" | "vertex" | "none",
apiKeyRef: string | null, // key vault reference
scripts: [
{
id: string,
name: string,
when: { onInjectId?: string, offsetMin?: number },
do: { type: "GeneratePost" | "Rewrite" | "Summarize" | "Custom", siteId?: string, args?: any },
guardrails: { maxTokens: number, temperature: number }
// Option: per-actor style prompts; keep deterministic defaults
}
]
},
// ---- Runtime (separate from authoring) ----
runtime: {
lockedAt: Date | null,
startedAt: Date | null,
pausedAt: Date | null,
endedAt: Date | null,
clockMin: number, // accumulated minutes
phaseId: string | null,
executedInjects: [{ injectId: string, at: Date, outcome: "ok" | "error", note?: string }],
// Compact ring buffer for control room; roll-ups to blob beyond N events
eventsCompact: [
{ t: Date, kind: string, actorId: string | null, siteId: string | null, refId: string | null, meta: any }
],
eventBlobKeys: [string]
},
// ---- Media / Exports / Audit ----
media: [{ key: string, kind: "image" | "video" | "doc", label: string, tags: [string] }],
exports: [{ version: number, at: Date, key: string, size: number, sha256: string }],
audit: [{ at: Date, byUserId: ObjectId, byUsername: string, change: string }]
}
// Indexes:
// - { status: 1, updatedAt: -1 }
// - { code: 1 } unique
// - { "sites.type": 1, "sites.enabled": 1 }
// - { "actors.id": 1 } sparse
// - { "injects.trigger.mode": 1 }
// - { "objectives.flags.code": 1 } sparseOptional exercise_events (add only if needed)
// High-volume append-only events for analytics/AAR at scale.
// Keep authoring/runtime control in `exercises`.
{
_id: ObjectId,
exerciseId: ObjectId,
t: Date,
kind: string, // "post.created", "search.query", "inject.executed", ...
actorId: string | null,
siteId: string | null,
refId: string | null,
meta: any
}
// Indexes:
// - { exerciseId: 1, t: 1 }
// - { exerciseId: 1, kind: 1, t: -1 }API Surface (indicative)
POST /api/loginGET/POST/PATCH /api/exercisesGET/PATCH /api/exercises/:id(ETag + subpath patching)POST /api/exercises/:id/lock→ status: preppedPOST /api/run/:id/start|pause|endPOST /api/run/:id/injects/:injectId/triggerGET /api/run/:id/events?since=...(SSE)POST /api/exercises/:id/export→ bundle keyPOST /api/exercises/import→ creates new exercise
Validation Gates
- Reference integrity:
actorId,orgId,siteId,injectId. - Seed/content size checks → offload to blob.
- Inject action/site compatibility.
- Objectives/flags resolvable against event shapes.
- Locking prevents authoring mutations during run.
Security and Isolation
- All in-world identities scoped to a single exercise.
- Export bundles with hash, optional media redaction.
- RBAC via
users.rolesandexercises.access. - No cross-exercise reads for participants.
MVP Feature Set
- Platform auth; exercises CRUD; access control.
- Actors/Orgs graph; Sites (Google + two socials); Custom pages builder.
- Seed content; templates; timeline with atomic actions.
- Manual triggers; compact logs; flags; basic scoring.
- User portal “browser” with search and shortcuts.
- Export/import bundle.
Containers and Boot
frontend: Next.js app, served behind reverse proxy.api: Node.js, runs scheduler, SSE, and Mongo/Blob clients.mongo: persistent volume forusers,exercises.blob: S3-compatible, bucket per environment.- Shared network; secrets via env or vault; health checks for API and DB.
Development Notes
- Deterministic string IDs for export stability.
- Patch-style updates to subtrees to minimize concurrent edit conflicts.
- Ring buffer for
runtime.eventsCompact(e.g., 10k events), periodic roll-ups to blob. - Seed content and templates authored in Admin; timeline remains canonical for execution.