Overview
TabletopExercise currently produces HTML/PDF/QMD exercise materials that are text-only — no images. Artifacts marked screenshot render as <pre><code> blocks even when a real image would be far more pedagogically effective. This issue tracks adding AI-generated images across three pedagogically distinct categories, inspired by tabletop RPG design principles.
Three image categories
1. Attack vector visuals (what the victim saw)
Make the deception mechanism visible, not narrated. Highest teaching value.
- Phishing email as it appeared in the inbox
- Fake login / credential-harvesting page
- Ransomware note / splash screen
- Fraudulent invoice or purchase order
- Malicious USB drive label / autorun prompt
2. Evidence visuals (what investigators find)
Give players a shared spatial model to reason against.
- Network diagram of compromised hosts
- SIEM alert / log screenshot
- Dark web listing of stolen data
- Network packet capture (visual representation)
- SCADA/ICS interface screen
3. Atmosphere visuals (mood / world-building)
Make the scenario feel inhabited before players explore it. Borrowed directly from TTRPG design.
- NPC portraits
- Location illustrations (hospital, factory floor, data center)
- Cover art for the scenario card
- Period photographs (historical scenarios only)
Provider flexibility (first-class requirement)
No single provider covers every use-case. All providers are selected via IMAGE_PROVIDER env var; API keys are read from env — never hardcoded. New providers can be added by implementing the ImageProvider interface and registering in the provider map — no changes to calling code needed.
| Provider |
Env var |
Notes |
openai (default) |
OPENAI_API_KEY |
DALL-E 3 — best prompt adherence |
gemini |
GEMINI_API_KEY |
Google Imagen (imagen-3.0-generate-002) |
stability |
STABILITY_API_KEY |
Stability AI REST API |
replicate |
REPLICATE_API_KEY |
Runs any Replicate model (default: black-forest-labs/flux-schnell) |
mistral |
MISTRAL_API_KEY |
Mistral image generation endpoint |
anthropic |
ANTHROPIC_API_KEY |
Prompt-enhancement layer: Claude enriches/refines prompts before routing to a generation provider; no second key needed when Claude is already in the loop |
ollama |
none |
Self-hosted at OLLAMA_BASE_URL (default: http://localhost:11434) |
Image subtypes with difficulty guidance
Each subtype maps to a specialised prompt builder. When image_subtype is absent, it is inferred from artifact.type and scenario_type.
| Subtype |
Category |
Appropriate difficulty |
ransomware_note |
Attack vector |
Intermediate+ |
phishing_email |
Attack vector |
All |
fraudulent_invoice |
Attack vector |
All |
usb_device |
Attack vector |
All (strong social-eng teaching) |
scada_interface |
Evidence |
Advanced / Historical |
network_capture |
Evidence |
Advanced |
dark_web_listing |
Evidence |
Intermediate+ |
network_diagram |
Evidence |
All |
period_photograph |
Atmosphere |
Historical only |
portrait |
Atmosphere |
All |
location_illustration |
Atmosphere |
All |
cover_art |
Atmosphere |
All |
Style consistency via VisualStyle
A visual_style block in the exercise data ensures cohesive visuals within a scenario — and across all scenario cards in a malmon family (e.g., all LockBit cards share the same palette).
interface VisualStyle {
art_style?: string; // "photorealistic" | "noir graphic novel" | "hand-drawn ink wash"
color_palette?: string; // "muted sepia" | "high-contrast blue-grey"
mood?: string; // "tense, clinical" | "cold, corporate"
seed?: number; // passed to providers that support deterministic generation
}
The same visual_style object is appended to every prompt as a suffix. Reusing the same block across all scenario cards in a malmon family produces a unified visual identity across the handbook.
Implementation plan
New file: generate-images.ts
- Provider registry pattern (see above)
- Three exported generator functions (one per MCP tool):
export async function generateAttackVectorImage(artifact, style, provider): Promise<string>
export async function generateEvidenceImage(artifact, style, provider): Promise<string>
export async function generateAtmosphereImage(context, style, provider): Promise<string>
- No new npm dependencies — uses Bun's native
fetch for all provider calls
schema.ts changes (backward-compatible)
export const VisualStyleSchema = z.object({
art_style: z.string().optional(),
color_palette: z.string().optional(),
mood: z.string().optional(),
seed: z.number().int().optional(),
});
// ArtifactSchema additions:
image_data: z.string().optional() // base64 data URI
image_subtype: z.enum([...]).optional()
// TabletopExerciseDataSchema additions:
cover_image_data: z.string().optional()
visual_style: VisualStyleSchema.optional()
mcp-server.ts — Tools 8–10
All three tools share the same contract:
- Validate schema → call generator → return
{ updated_data, images_generated, provider_used } or { error }
- Never throw
Tool 8: generate_attack_vector_images — populates artifact.image_data for attack-vector-type artifacts
Tool 9: generate_evidence_images — populates artifact.image_data for evidence artifacts; optionally generates exercise_data.evidence_diagram_data
Tool 10: generate_atmosphere_images — generates cover_image_data + NPC portrait_data; accepts subtypes array to limit scope
generate-pdf.ts rendering
- Cover page: if
cover_image_data → <img> in header
- Artifact blocks: if
artifact.image_data → <img> instead of <pre><code>; facilitator mode shows text content as caption; participant mode shows image only
generate-qmd.ts rendering
- If
artifact.image_data, write [slug].png to outputDir and use  instead of the code block
SKILL.md additions
- New Image Generation section with difficulty guidance per category
- Recommended workflow:
validate_exercise_data → image tools → generate_exercise / generate_exercise_qmd
- How
visual_style provides scenario-wide and cross-scenario consistency
test-mcp.ts — Tests 19–24
All tests avoid real API calls (test the no-key error path):
| Test |
What it covers |
| 19 |
generate_attack_vector_images returns structured error when no API key present |
| 20 |
generate_evidence_images same |
| 21 |
generate_atmosphere_images same |
| 22 |
Invalid exercise_data → schema error returned before provider is checked |
| 23 |
visual_style round-trips through schema validation |
| 24 |
image_subtype accepted and returned in updated_data |
Acceptance criteria
Related
🤖 Generated with Claude Code
Overview
TabletopExercise currently produces HTML/PDF/QMD exercise materials that are text-only — no images. Artifacts marked
screenshotrender as<pre><code>blocks even when a real image would be far more pedagogically effective. This issue tracks adding AI-generated images across three pedagogically distinct categories, inspired by tabletop RPG design principles.Three image categories
1. Attack vector visuals (what the victim saw)
Make the deception mechanism visible, not narrated. Highest teaching value.
2. Evidence visuals (what investigators find)
Give players a shared spatial model to reason against.
3. Atmosphere visuals (mood / world-building)
Make the scenario feel inhabited before players explore it. Borrowed directly from TTRPG design.
Provider flexibility (first-class requirement)
No single provider covers every use-case. All providers are selected via
IMAGE_PROVIDERenv var; API keys are read from env — never hardcoded. New providers can be added by implementing theImageProviderinterface and registering in the provider map — no changes to calling code needed.openai(default)OPENAI_API_KEYgeminiGEMINI_API_KEYimagen-3.0-generate-002)stabilitySTABILITY_API_KEYreplicateREPLICATE_API_KEYblack-forest-labs/flux-schnell)mistralMISTRAL_API_KEYanthropicANTHROPIC_API_KEYollamaOLLAMA_BASE_URL(default:http://localhost:11434)Image subtypes with difficulty guidance
Each subtype maps to a specialised prompt builder. When
image_subtypeis absent, it is inferred fromartifact.typeandscenario_type.ransomware_notephishing_emailfraudulent_invoiceusb_devicescada_interfacenetwork_capturedark_web_listingnetwork_diagramperiod_photographportraitlocation_illustrationcover_artStyle consistency via
VisualStyleA
visual_styleblock in the exercise data ensures cohesive visuals within a scenario — and across all scenario cards in a malmon family (e.g., all LockBit cards share the same palette).The same
visual_styleobject is appended to every prompt as a suffix. Reusing the same block across all scenario cards in a malmon family produces a unified visual identity across the handbook.Implementation plan
New file:
generate-images.tsfetchfor all provider callsschema.tschanges (backward-compatible)mcp-server.ts— Tools 8–10All three tools share the same contract:
{ updated_data, images_generated, provider_used }or{ error }Tool 8:
generate_attack_vector_images— populatesartifact.image_datafor attack-vector-type artifactsTool 9:
generate_evidence_images— populatesartifact.image_datafor evidence artifacts; optionally generatesexercise_data.evidence_diagram_dataTool 10:
generate_atmosphere_images— generatescover_image_data+ NPCportrait_data; acceptssubtypesarray to limit scopegenerate-pdf.tsrenderingcover_image_data→<img>in headerartifact.image_data→<img>instead of<pre><code>; facilitator mode shows text content as caption; participant mode shows image onlygenerate-qmd.tsrenderingartifact.image_data, write[slug].pngtooutputDirand useinstead of the code blockSKILL.mdadditionsvalidate_exercise_data→ image tools →generate_exercise/generate_exercise_qmdvisual_styleprovides scenario-wide and cross-scenario consistencytest-mcp.ts— Tests 19–24All tests avoid real API calls (test the no-key error path):
generate_attack_vector_imagesreturns structured error when no API key presentgenerate_evidence_imagessamegenerate_atmosphere_imagessameexercise_data→ schema error returned before provider is checkedvisual_styleround-trips through schema validationimage_subtypeaccepted and returned inupdated_dataAcceptance criteria
bun run test-mcp.ts→ 75+ assertions, all pass (Tests 19–24 included)OPENAI_API_KEYset:generate_attack_vector_imageson SSRF fixture →images_generated > 0, artifact has valid data URI inimage_datagenerate_exerciseon updated data →facilitator.htmlcontains<imgtagsIMAGE_PROVIDER=gemini GEMINI_API_KEY=...→ same result via ImagenRelated
generate_exercise_qmdtool🤖 Generated with Claude Code