const navRoot = document.getElementById("nav-root");
const appRoot = document.getElementById("app");
const toastTemplate = document.getElementById("toast-template");
const API = {
scenarios: "/api/v1/scenarios",
scenario: (key) => `/api/v1/scenarios/${encodeURIComponent(key)}`,
reset: "/api/v1/env/reset",
step: (envId) => `/api/v1/env/${encodeURIComponent(envId)}/step`,
destroy: (envId) => `/api/v1/env/${encodeURIComponent(envId)}`,
};
const state = {
path: normalizePath(window.location.pathname),
scenarios: {},
order: [],
builderScenario: null,
playground: {
envId: null,
scenarioKey: null,
score: 0,
step: 0,
maxSteps: 50,
taskName: "",
description: "",
cwd: "~",
done: false,
},
};
const AUTO_PLAYBOOK = {
llm: {
log_analysis: ["grep 500 /var/log/app.log"],
permission_repair: ["chmod 0755 /home/user/scripts/cleanup.sh"],
process_recovery: ["ps", "systemctl restart postgres", "systemctl status postgres"],
cascading_db_failure: ["ps", "systemctl restart postgres", "df -h", "rm /var/log/nginx/error.log", "systemctl restart app"],
full_incident: ["ps", "df -h", "systemctl restart postgres", "rm /var/log/nginx/error.log", "grep auth /var/log/auth.log"],
},
rl: {
log_analysis: ["cat /var/log/app.log", "grep 500 /var/log/app.log"],
process_recovery: ["systemctl restart postgres", "systemctl status postgres"],
cascading_db_failure: ["systemctl restart postgres", "rm /var/log/nginx/error.log", "systemctl restart app"],
},
};
function normalizePath(pathname) {
const normalized = pathname.replace(/\/+$/, "");
return normalized.length ? normalized : "/";
}
function routeKey(path) {
if (path === "/builder") return "builder";
if (path === "/playground") return "playground";
if (path === "/arena") return "arena";
return "hub";
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function splitLines(text) {
return String(text || "")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
}
function getQueryScenario() {
const params = new URLSearchParams(window.location.search);
return params.get("scenario") || "";
}
function showToast(message) {
if (!toastTemplate) return;
const toast = toastTemplate.content.firstElementChild.cloneNode(true);
toast.textContent = message;
document.body.appendChild(toast);
window.setTimeout(() => toast.remove(), 2200);
}
async function fetchJSON(url, options = {}) {
const config = { ...options };
config.headers = {
...(options.headers || {}),
};
if (config.body && !config.headers["Content-Type"]) {
config.headers["Content-Type"] = "application/json";
}
const response = await fetch(url, config);
if (!response.ok) {
let details = `${response.status} ${response.statusText}`;
try {
const payload = await response.json();
if (payload?.detail) {
details = payload.detail;
}
} catch {
// Fallback to status text.
}
throw new Error(details);
}
return response.json();
}
function difficultyMeta(diff) {
const value = (diff || "").toLowerCase();
if (value === "easy") {
return {
label: "SAFE",
textClass: "text-chaos-green",
bgClass: "bg-chaos-green/10",
borderClass: "border-chaos-green/20",
specsClass: "text-chaos-green",
iconColorClass: "text-chaos-green",
};
}
if (value === "medium") {
return {
label: "MEDIUM",
textClass: "text-chaos-cyan",
bgClass: "bg-chaos-cyan/10",
borderClass: "border-chaos-cyan/20",
specsClass: "text-chaos-cyan",
iconColorClass: "text-chaos-cyan",
};
}
if (value === "hard") {
return {
label: "CRITICAL",
textClass: "text-chaos-red",
bgClass: "bg-chaos-red/10",
borderClass: "border-chaos-red/20",
specsClass: "text-chaos-red",
iconColorClass: "text-chaos-red",
};
}
return {
label: "EXPERT",
textClass: "text-chaos-red",
bgClass: "bg-chaos-red/10",
borderClass: "border-chaos-red/50",
specsClass: "text-chaos-red",
iconColorClass: "text-chaos-red",
};
}
function scenarioEntries() {
return state.order.map((key) => [key, state.scenarios[key]]).filter(([, value]) => Boolean(value));
}
function iconMarkup(key, colorClass) {
if (key === "process_recovery" || key === "cascading_db_failure") {
return ` `;
}
if (key === "disk_space_crisis") {
return ` `;
}
if (key === "security_incident") {
return ` `;
}
if (key === "memory_leak") {
return ` `;
}
if (key === "network_troubleshooting") {
return ` `;
}
return ` `;
}
function renderNav() {
const active = routeKey(state.path);
const navLink = (href, key, label) => {
const activeClass = key === active
? "text-chaos-green border-b-2 border-chaos-green py-5 -mb-[22px]"
: "text-chaos-muted";
return `${label} `;
};
navRoot.innerHTML = `
ChaosLab
${navLink("/", "hub", "Hub")}
${navLink("/builder", "builder", "Builder")}
${navLink("/playground", "playground", "Playground")}
${navLink("/arena", "arena", "Arena")}
`;
}
function buildManifest(detail) {
const lines = [
`scenario: "${detail.name}"`,
`key: "${detail.key}"`,
`difficulty: ${detail.difficulty}`,
`max_steps: ${detail.max_steps}`,
"",
"faults:",
];
detail.faults.forEach((fault, index) => {
lines.push(` - id: "fault-${index}"`);
lines.push(` name: "${fault.name}"`);
lines.push(` type: "${fault.apply_fn}"`);
lines.push(` args: ${JSON.stringify(fault.params)}`);
});
lines.push("", "cascades:");
detail.cascades.forEach((cascade) => {
lines.push(` - condition: "${cascade.condition_fn}"`);
lines.push(` when: ${JSON.stringify(cascade.condition_params)}`);
lines.push(` trigger: "${cascade.effect.apply_fn}"`);
lines.push(` opts: ${JSON.stringify(cascade.effect.params)}`);
});
lines.push("", "objectives:");
detail.objectives.forEach((objective) => {
lines.push(` - check: "${objective.check_fn}"`);
lines.push(` description: "${objective.description}"`);
lines.push(` points: ${objective.points}`);
});
return lines.join("\n");
}
function metrics(detail) {
const faults = detail.faults.length;
const cascades = detail.cascades.length;
const objectives = detail.objectives.length;
const blastRadius = Math.min(99, faults * 3 + cascades * 2 + objectives * 1.2);
const complexityBase = detail.difficulty === "expert" ? 0.9 : detail.difficulty === "hard" ? 0.76 : detail.difficulty === "medium" ? 0.58 : 0.42;
const complexity = Math.min(0.99, complexityBase + faults * 0.01 + cascades * 0.01);
return { faults, cascades, objectives, blastRadius, complexity };
}
function renderHub() {
const cards = scenarioEntries().map(([key, scenario]) => {
const meta = difficultyMeta(scenario.difficulty);
return `
${iconMarkup(key, meta.iconColorClass)}
${meta.label}
${escapeHtml(scenario.name)}
${escapeHtml(scenario.description || "")}
${escapeHtml(String(scenario.objectives_count || 0))} OBJS
MAX ${escapeHtml(String(scenario.max_steps || 50))} STEPS
`;
}).join("");
const featuredKey = state.scenarios.full_incident ? "full_incident" : state.order[0];
const featured = state.scenarios[featuredKey] || null;
const featuredName = featured ? featured.name : "Full Incident Response";
const featuredDifficulty = featured ? difficultyMeta(featured.difficulty).specsClass : "text-chaos-red";
const featuredSteps = featured ? String(featured.max_steps || 100) : "100";
const featuredObjectives = featured ? String(featured.objectives_count || 6) : "6";
appRoot.innerHTML = `
ChaosHub
Central nervous system for system resilience. Browse production-hardened tools and scenarios to stress test your architecture.
Scenarios
Integrations
${cards}
FEATURED SCENARIO
${escapeHtml(featuredName)}
The ultimate challenge. A cascading failure involving database crash, disk overfilling, cron breakdown, and a concurrent security incident. Triage, prioritize, and restore all systems to 100% functionality.
Technical Specs
Complexity Expert
Max Steps ${escapeHtml(featuredSteps)}
Grading Logic ${escapeHtml(featuredObjectives)} Objectives
`;
const integrations = document.getElementById("integrations-tab");
integrations?.addEventListener("click", () => {
showToast("Integrations view is not available yet.");
});
}
function builderScenarioOptionButtons(selectedKey) {
return scenarioEntries().map(([key, meta]) => {
return `${escapeHtml(meta.name)} `;
}).join("");
}
async function renderBuilder() {
const queryScenario = getQueryScenario();
if (!state.builderScenario) {
if (state.scenarios[queryScenario]) {
state.builderScenario = queryScenario;
} else if (state.scenarios.cascading_db_failure) {
state.builderScenario = "cascading_db_failure";
} else {
state.builderScenario = state.order[0];
}
}
const key = state.builderScenario;
const detail = await fetchJSON(API.scenario(key));
const live = metrics(detail);
const faultsMarkup = detail.faults.map((fault) => {
return `${iconMarkup("process_recovery", "w-4 h-4")}
${escapeHtml(fault.name)}
${escapeHtml(fault.apply_fn.replaceAll("_", " "))}
`;
}).join("");
const cascadeMarkup = detail.cascades.map((cascade) => {
const effectLabel = cascade.effect?.name || cascade.effect?.apply_fn || "";
return `${escapeHtml(cascade.condition_fn.replaceAll("_", " "))}
→ ${escapeHtml(effectLabel)}
`;
}).join("");
const objectiveMarkup = detail.objectives.map((objective) => {
return `${escapeHtml(objective.description)}
${Math.round((objective.points || 0) * 100)}% `;
}).join("");
const formatNodeValue = (paramKey, paramValue) => {
if (paramKey === "count") {
const numeric = Number(paramValue);
if (Number.isFinite(numeric) && numeric >= 1000) {
return `${(numeric / 1000).toFixed(1)}K`;
}
}
return String(paramValue);
};
const actionDescription = (actionName) => {
if (actionName === "fill_disk") return "Log flood fills /var/log partition";
if (actionName === "crash_service" || actionName === "app_degraded") return "App can't write logs, starts failing";
return actionName.replaceAll("_", " ");
};
const faultNodes = detail.faults.map((fault, index) => {
const id = `${fault.apply_fn.slice(0, 3).toUpperCase()}-${String(index).padStart(2, "0")}`;
const paramRows = Object.entries(fault.params || {}).map(([paramKey, paramValue]) => {
return `${escapeHtml(paramKey)} ${escapeHtml(formatNodeValue(paramKey, paramValue))}
`;
}).join("");
return `${escapeHtml(fault.name.toUpperCase())} ID: ${escapeHtml(id)}
${iconMarkup("process_recovery", "w-4 h-4")}
${escapeHtml(fault.description || fault.name)}
`;
}).join("");
const cascadeNodes = detail.cascades.map((cascade, index) => {
const cond = cascade.condition_fn.replaceAll("_", " ");
const actionKey = String(cascade.effect.apply_fn || "");
const actionText = cascade.effect.description || actionDescription(actionKey);
const firstParam = Object.entries(cascade.condition_params || {}).map(([k, v]) => `${k}: ${v}`).join(", ");
const actionMeta = Object.entries(cascade.effect.params || {}).map(([k, v]) => `${escapeHtml(`${k}:`)} ${escapeHtml(formatNodeValue(k, v))}
`).join("");
return `
TRIGGER CONDITION#${index + 1} IF ${escapeHtml(cond)}
(${escapeHtml(firstParam)})
ACTION: ${escapeHtml(actionKey.toUpperCase())}
${escapeHtml(actionText)}
${actionMeta}
`;
}).join("");
const objectiveNodes = detail.objectives.map((objective, index) => {
return `
OBJ-${String(index + 1).padStart(2, "0")} ${Math.round((objective.points || 0) * 100)}%
${escapeHtml(objective.description)}
${escapeHtml(objective.check_fn.replaceAll("_", " "))} `;
}).join("");
const scenarioButtonLabel = state.scenarios[key]?.name || detail.name;
const diff = difficultyMeta(detail.difficulty);
appRoot.innerHTML = `
Select Scenario
${escapeHtml(scenarioButtonLabel)}
Injected Faults ${detail.faults.length}
${faultsMarkup}
Cascade Rules ${detail.cascades.length}
${cascadeMarkup}
Objectives ${detail.objectives.length}
${objectiveMarkup}
Fault Nodes ${detail.faults.length} Active
Cascade Logic ${detail.cascades.length} Triggers
Validation Targets ${detail.objectives.length} Criteria
${faultNodes}
${cascadeNodes}
${objectiveNodes}
Active Scenario
${escapeHtml(detail.name)}
${diff.label}
MAX: ${detail.max_steps} STEPS
${escapeHtml(detail.description)}
Live Projections
Fault Nodes ${live.faults}
Cascade Depth ${live.cascades}
Objectives ${live.objectives}
Blast Radius ${live.blastRadius.toFixed(1)}%
Complexity ${live.complexity.toFixed(3)}
Hints
${detail.hints.map((hint) => `› ${escapeHtml(hint)} `).join("")}
Config Manifest
${escapeHtml(buildManifest(detail))}
Execute Experiment
Export YAML Bundle
`;
const trigger = document.getElementById("builder-scenario-trigger");
const menu = document.getElementById("builder-scenario-menu");
trigger?.addEventListener("click", () => {
menu?.classList.toggle("hidden");
});
menu?.querySelectorAll("button[data-scenario-key]").forEach((button) => {
button.addEventListener("click", async () => {
const selected = button.getAttribute("data-scenario-key");
if (!selected) return;
state.builderScenario = selected;
await renderBuilder();
});
});
[["builder-btn-execute", "Experiment queued."], ["builder-btn-llm", "LLM test started."], ["builder-btn-rl", "RL test started."], ["builder-btn-export", "YAML bundle exported."]].forEach(([id, msg]) => {
document.getElementById(id)?.addEventListener("click", () => showToast(msg));
});
}
function getRingDash(score) {
const pct = Math.max(0, Math.min(99, Math.round(score * 100)));
return `${pct}, 100`;
}
function appendTerminalOutput(line) {
const stream = document.getElementById("terminal-stream");
const output = document.getElementById("terminal-output-area");
if (!stream || !output) return;
const row = document.createElement("div");
row.className = "mb-2 whitespace-pre-wrap flex flex-col";
row.innerHTML = `${escapeHtml(line)}
`;
stream.appendChild(row);
output.scrollTop = output.scrollHeight;
}
function appendTerminalSystem(line) {
const stream = document.getElementById("terminal-stream");
const output = document.getElementById("terminal-output-area");
if (!stream || !output) return;
const row = document.createElement("div");
row.className = "mb-2 whitespace-pre-wrap flex flex-col";
row.innerHTML = `${escapeHtml(line)}
`;
stream.appendChild(row);
output.scrollTop = output.scrollHeight;
}
function renderPlaygroundFrame() {
const envLabel = "ENV_0";
const scorePct = Math.max(0, Math.min(99, Math.round(state.playground.score * 100)));
appRoot.innerHTML = `
${escapeHtml(state.playground.taskName || "Log Analysis")}
ID: ${escapeHtml(envLabel)}
${escapeHtml(state.playground.description || "")}
Completion Level
${scorePct} PTS
Step Progress ${state.playground.step} / ${state.playground.maxSteps}
Objectives Meta
Complete actions in the terminal and verify fixes to achieve 100% completion in this sandbox.
root@chaoslab-env_0 ~ (ssh)
OFFLINE
ENCRYPTION: AES-256-GCM
Ask the LLM for help with the current task. Get suggestions for the next command to run.
`;
}
async function destroyPlaygroundEnv() {
if (!state.playground.envId) return;
try {
await fetchJSON(API.destroy(state.playground.envId), { method: "DELETE" });
} catch {
// Ignore cleanup errors.
}
}
function updatePlaygroundWidgets() {
const scorePct = Math.max(0, Math.min(99, Math.round(state.playground.score * 100)));
const envLabel = "ENV_0";
const progressArc = document.getElementById("playground-progress-arc");
if (progressArc) progressArc.setAttribute("stroke-dasharray", getRingDash(state.playground.score));
const progressText = document.getElementById("playground-progress-text");
if (progressText) progressText.textContent = `${scorePct}%`;
const score = document.getElementById("playground-score-points");
if (score) score.textContent = String(scorePct);
const step = document.getElementById("playground-step-progress");
if (step) step.textContent = `${state.playground.step} / ${state.playground.maxSteps}`;
const name = document.getElementById("playground-task-name");
if (name) name.textContent = state.playground.taskName;
const description = document.getElementById("playground-task-desc");
if (description) description.textContent = state.playground.description;
const shell = document.getElementById("playground-shell-label");
if (shell) shell.textContent = `root@chaoslab-${envLabel.toLowerCase()} ~ (ssh)`;
const prompt = document.getElementById("playground-prompt");
if (prompt) prompt.textContent = `root@chaoslab:${state.playground.cwd}$`;
}
async function initializePlayground(scenarioKey) {
await destroyPlaygroundEnv();
const payload = await fetchJSON(API.reset, {
method: "POST",
body: JSON.stringify({ scenario: scenarioKey }),
});
state.playground.envId = payload.env_id;
state.playground.scenarioKey = scenarioKey;
state.playground.score = 0;
state.playground.step = 0;
state.playground.maxSteps = Number(payload.info?.max_steps || 50);
state.playground.done = false;
state.playground.cwd = "~";
state.playground.taskName = payload.info?.task_name || (state.scenarios[scenarioKey]?.name || "Scenario");
state.playground.description = state.scenarios[scenarioKey]?.description || "";
appendTerminalSystem(":: Successfully initialized sandbox [env_0]");
appendTerminalSystem(`:: Task: ${state.playground.taskName}`);
updatePlaygroundWidgets();
}
async function runPlaygroundCommand(command) {
if (!command || !state.playground.envId || state.playground.done) return;
appendTerminalOutput(`root@chaoslab:${state.playground.cwd}$ ${command}`);
const payload = await fetchJSON(API.step(state.playground.envId), {
method: "POST",
body: JSON.stringify({ action: command }),
});
const output = payload.info?.command_output || "";
if (output.trim()) {
output.split(/\r?\n/).forEach((line) => {
if (line.trim()) appendTerminalOutput(line);
});
}
state.playground.score = Number(payload.info?.task_score || 0);
state.playground.step = Number(payload.info?.step || state.playground.step + 1);
state.playground.maxSteps = Number(payload.info?.max_steps || state.playground.maxSteps);
state.playground.cwd = payload.observation?.current_directory || state.playground.cwd;
state.playground.done = Boolean(payload.done);
if (state.playground.done) {
appendTerminalSystem(":: Sandbox run ended.");
}
updatePlaygroundWidgets();
}
async function renderPlayground() {
renderPlaygroundFrame();
const queryScenario = getQueryScenario();
const key = state.scenarios[queryScenario] ? queryScenario : state.order[0];
await initializePlayground(key);
const input = document.getElementById("cli-input");
input?.addEventListener("keydown", async (event) => {
if (event.key !== "Enter") return;
event.preventDefault();
const command = input.value.trim();
if (!command) return;
input.value = "";
try {
await runPlaygroundCommand(command);
} catch (error) {
appendTerminalOutput(`error: ${error.message}`);
}
});
document.getElementById("playground-auto-rl")?.addEventListener("click", async () => {
const commands = AUTO_PLAYBOOK.rl[state.playground.scenarioKey] || AUTO_PLAYBOOK.llm[state.playground.scenarioKey] || [];
for (const command of commands) {
if (state.playground.done) break;
try {
await runPlaygroundCommand(command);
} catch {
break;
}
}
});
}
function resolveArenaCommands(mode, scriptText, scenarioKey) {
const normalized = (mode || "script").toLowerCase();
if (normalized === "script") {
const commands = splitLines(scriptText);
if (commands.length) return commands;
}
if (AUTO_PLAYBOOK[normalized]?.[scenarioKey]?.length) {
return AUTO_PLAYBOOK[normalized][scenarioKey];
}
if (AUTO_PLAYBOOK.llm[scenarioKey]?.length) {
return AUTO_PLAYBOOK.llm[scenarioKey];
}
return ["ps", "df -h", "free -m"];
}
async function runArenaAgent(label, mode, scriptText, scenarioKey) {
const commands = resolveArenaCommands(mode, scriptText, scenarioKey);
const resetPayload = await fetchJSON(API.reset, {
method: "POST",
body: JSON.stringify({ scenario: scenarioKey }),
});
const envId = resetPayload.env_id;
let score = 0;
let step = 0;
let done = false;
try {
for (const command of commands) {
const payload = await fetchJSON(API.step(envId), {
method: "POST",
body: JSON.stringify({ action: command }),
});
score = Number(payload.info?.task_score || score);
step = Number(payload.info?.step || step + 1);
done = Boolean(payload.done);
if (done) break;
}
} finally {
try {
await fetchJSON(API.destroy(envId), { method: "DELETE" });
} catch {
// Ignore cleanup error.
}
}
return { label, mode, score, step, done };
}
function renderArenaResult(resultA, resultB) {
const winner = resultA.score > resultB.score
? resultA.label
: resultB.score > resultA.score
? resultB.label
: resultA.step < resultB.step
? resultA.label
: resultB.step < resultA.step
? resultB.label
: "DRAW";
return `
${winner === "DRAW" ? "Result: Draw" : `Winner: ${winner}`}
Agent
Mode
Score
Steps
Status
${escapeHtml(resultA.label)}
${escapeHtml(resultA.mode)}
${Math.round(resultA.score * 100)}%
${resultA.step}
${resultA.done ? "DONE" : "INCOMPLETE"}
${escapeHtml(resultB.label)}
${escapeHtml(resultB.mode)}
${Math.round(resultB.score * 100)}%
${resultB.step}
${resultB.done ? "DONE" : "INCOMPLETE"}
`;
}
function renderArena() {
const options = scenarioEntries().map(([key, meta]) => {
return `${escapeHtml(meta.name)} `;
}).join("");
appRoot.innerHTML = `
root@chaoslab-terminal / ARENA / AGENT_DUEL_V3.4
Compare user or autonomous scripts against high-stakes environments. Define identical testing parameters and observe how different instruction sets handle cascading service disruptions.
Active Environment Overlay
${options}
READY
A
AGENT_ALPHA
Behavior Matrix Hardcoded Script LLM Autonomous RL Autonomous
B
AGENT_BETA
Behavior Matrix Hardcoded Script LLM Autonomous RL Autonomous
RL AUTONOMOUS MODE
Model: Heuristic Expert
PPO Neural Net — Tabular Q-Learning — Heuristic Expert ✓
`;
document.getElementById("arena-run")?.addEventListener("click", async () => {
const scenarioKey = document.getElementById("arena-scenario")?.value || state.order[0];
const modeA = document.getElementById("arena-mode-a")?.value || "script";
const modeB = document.getElementById("arena-mode-b")?.value || "rl";
const scriptA = document.getElementById("arena-script-a")?.value || "";
const scriptB = document.getElementById("arena-script-b")?.value || "";
const runButton = document.getElementById("arena-run");
const resultNode = document.getElementById("arena-result");
if (runButton) runButton.setAttribute("disabled", "disabled");
if (resultNode) {
resultNode.innerHTML = `Running race...
`;
}
try {
const [resultA, resultB] = await Promise.all([
runArenaAgent("AGENT_ALPHA", modeA, scriptA, scenarioKey),
runArenaAgent("AGENT_BETA", modeB, scriptB, scenarioKey),
]);
if (resultNode) {
resultNode.innerHTML = renderArenaResult(resultA, resultB);
}
} catch (error) {
if (resultNode) {
resultNode.innerHTML = `Race failed: ${escapeHtml(error.message)}
`;
}
} finally {
if (runButton) runButton.removeAttribute("disabled");
}
});
}
function renderError(message) {
appRoot.innerHTML = `Frontend failed to initialize ${escapeHtml(message)}
`;
}
async function hydrateScenarios() {
const payload = await fetchJSON(API.scenarios);
state.scenarios = payload.scenarios || {};
state.order = Object.keys(state.scenarios);
}
async function mount() {
try {
await hydrateScenarios();
renderNav();
if (routeKey(state.path) === "builder") {
await renderBuilder();
return;
}
if (routeKey(state.path) === "playground") {
await renderPlayground();
return;
}
if (routeKey(state.path) === "arena") {
renderArena();
return;
}
renderHub();
} catch (error) {
renderNav();
renderError(error.message || "Unknown error");
}
}
window.addEventListener("pagehide", () => {
destroyPlaygroundEnv();
});
mount();