diff --git a/.gitignore b/.gitignore
index 852c086..affc2ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,16 +26,7 @@ CybORG/Emulator/Logs/
CybORG/Emulator/logs/
CybORG/Emulator/Config/emulatorconfig.ini
docs/_build/
+site/
dist
-CybORG/Shared/Config/config.ini
-
-CybORG/Emulator/AWS/Config/awsconfig.ini
-
-*.swp
-*.pstats
-
-.ipynb_checkpoints/
-docs/
-
.ruff_cache/
\ No newline at end of file
diff --git a/DATASHEET.md b/DATASHEET.md
new file mode 100644
index 0000000..681a210
--- /dev/null
+++ b/DATASHEET.md
@@ -0,0 +1,82 @@
+# Datasheet & Responsible Use — NetForge RL
+
+Following *Datasheets for Datasets* (Gebru et al.) adapted for a simulated
+benchmark environment. This accompanies a NeurIPS Datasets & Benchmarks submission.
+
+## Motivation
+
+- **Purpose.** NetForge is a research gym and benchmark for autonomous cyber
+ defense: train, evaluate, break, and compare Red/Blue agents. The simulator
+ is the engine; NetForge Arena is the standardized evaluation contract
+ (scenarios, seeds, hidden tests, capability probes, agent cards).
+- **Gap addressed.** Existing cyber-RL environments are largely single-agent or
+ fixed-topology, and they rarely freeze a benchmark protocol. NetForge provides
+ procedural topologies, belief-state graph observations, a train/dev/hidden
+ split, scenario packs, risk-sensitive metrics, and a verified JAX throughput
+ backend.
+
+## Composition
+
+- **Instances.** Episodes are procedurally generated network topologies (100 hosts,
+ 3–5 subnets, optional OT subnet) with per-host OS/CVE/service/credential profiles.
+- **Generation.** `NetworkGenerator(seed)` is deterministic per seed. Training draws
+ from arbitrary seeds; evaluation (`evaluation_mode=True`) draws from a disjoint
+ held-out pool (seed offset 1000) never seen during training.
+- **No personal data.** All hosts, IPs, credentials, and logs are synthetic. CVE
+ identifiers (e.g. MS17-010, CVE-2019-0708) reference public catalog entries used
+ only as abstract vulnerability labels; no exploit code is included.
+
+## Collection / Construction
+
+- **Dynamics.** Two backends share one action taxonomy. The full-fidelity PettingZoo
+ env (`environment/parallel_env.py`) carries action durations, an event queue, SIEM
+ telemetry, and command-list deltas. The vectorized JAX kernel
+ (`backends/jax/vector_env.py`) implements a reduced transition core and is verified
+ equivalent to its NumPy reference (`backends/reference.py`) by a trajectory-level
+ parity test that gates CI (`tests/parity/test_jax_reference_equivalence.py`). The
+ JAX kernel is a scalable subset, not a bit-equivalent replica of the full env.
+- **Rewards.** Per-scenario decompositions; see `benchmarks/env_spec.py` for the full
+ table. Per-step rewards are small; the OT-safety scenarios (`ot_stuxnet`, and the
+ kinetic branch of `ransomware`) add a large ±10,000 terminal signal when a PLC is
+ physically destroyed, so catastrophic physical outcomes dominate the return.
+ `BaseScenario.normalized_reward` exposes a `tanh`-squashed value in `[-1, 1]` for
+ cross-scenario comparison.
+- **Determinism.** Deterministic under a seed. A per-episode RNG on `GlobalNetworkState`
+ drives stochastic actions (exploit rolls, phishing, PLC overload); the SIEM logger,
+ event templates, topology engine, physics, and green agent are all reseeded on
+ `reset(seed)`. See `tests/parity/test_golden_trajectory.py`.
+
+## Recommended Uses
+
+- Benchmarking cooperative/competitive MARL algorithms under partial observability.
+- Evaluating LLM agents as SOC operators (`benchmarks/llm_eval.py`).
+- Studying generalization via the train/held-out topology split
+ (`benchmarks/run_benchmark.py --gap`).
+
+## Limitations
+
+- **It is a simulation.** Dynamics are abstracted and have **not** been validated
+ against real network telemetry or live red-team engagements. Results do not
+ transfer to operational systems without further study.
+- **Abstracted exploits.** Exploit success is modeled via CVSS-weighted probabilities
+ and vulnerability flags, not real exploitation. CVE labels are not a fidelity claim.
+- **No real C2 integration.** Live-fire orchestration against real frameworks is
+ intentionally out of scope and gated behind an unimplemented isolation policy.
+
+## Responsible Use (dual-use statement)
+
+This environment trains both attacker and defender policies. It is released for
+**defensive research and benchmarking**. The simulator contains no operational
+exploit code, no malware, and no capability to act on real systems. Trained Red
+policies operate only inside the simulator's abstract action space and confer no
+real-world offensive capability. Users must not attempt to connect the environment
+to real or production infrastructure. We encourage use aligned with coordinated
+vulnerability disclosure norms.
+
+## Maintenance
+
+- **Versioning.** Environment changes that alter dynamics are reflected in the
+ parity reference and gated by CI; the golden-trajectory fingerprint test detects
+ unintended reward/termination drift.
+- **Reproducibility.** Pin the package version and seeds; `benchmarks/` scripts emit
+ timestamped JSON results with mean ± CI95.
diff --git a/README.md b/README.md
index 5200f16..e8824c1 100644
--- a/README.md
+++ b/README.md
@@ -1,137 +1,125 @@
-
-
-
-
-
+
+
+# NetForge
+
+**Cybersecurity gym for reinforcement learning**
+
+Red attacks a network. Blue contains it from SIEM, not from an oracle.
+PettingZoo (1 Red + 3 Blue) or Gymnasium (one Blue vs scripted Red).
+
+**Python 3.12+** · **PettingZoo** · **Gymnasium** · **Not on PyPI**
+
+[Docs](https://reforcemind.github.io/NetForge_RL/) ·
+[Start](https://reforcemind.github.io/NetForge_RL/getting-started.html) ·
+[Python](https://reforcemind.github.io/NetForge_RL/python-quickstart.html) ·
+[Cyber](https://reforcemind.github.io/NetForge_RL/cybersec/overview.html) ·
+[Contributing](CONTRIBUTING.md)
+
+
+
+
-NetForge RL
-
-Multi-agent cybersecurity environment for RL research. Red team vs Blue team on generated networks.
-
----
-
-## Why NetForge
-
-- **Standardized API** — PettingZoo `ParallelEnv`, Gymnasium spaces, conformance-tested.
- A `gymnasium.Env` single-agent facade (`NetForgeSingleAgentEnv`) drops straight into
- Stable-Baselines3 or CleanRL.
-- **Partial observability that means something** — Blue reads a filtered, optionally
- *delayed* SIEM feed; Red must recon a host before it can exploit it.
-- **Real telemetry** — actions emit real Windows/Sysmon event XML, encoded into
- observations by an NLP pipeline. Point an OpenAI, Anthropic, Google, or vLLM client at
- the raw logs and let it play SOC analyst.
-- **MITRE ATT&CK aligned** — actions carry ATT&CK technique IDs, and CVE identifiers are
- used as abstract vulnerability labels (no real exploit code); every episode reports which
- ATT&CK techniques Red actually exercised.
-- **Reproducible to the bit** — a fixed seed replays observations, SIEM embeddings, infos,
- and rewards identically. Test-guaranteed, not just claimed.
-- **Tunable difficulty** — named `easy` / `medium` / `hard` presets plus a frozen held-out
- evaluation split, so results are comparable across runs.
-- **Diagnostic probes + capability cards** — 6 targeted probes (memory, attention,
- temporal, precision, safety, generalization) score *what* a policy can and can't do,
- summarized as a per-policy radar chart.
-- **Deception as a mechanic** — decoys and honeytokens with a `deception_efficacy` metric
- quantifying how much of Red's effort was wasted on traps.
-- **Optional graph observations** — an experimental wrapper exposes hosts as nodes and
- reachability as edges (fog-of-war aware); the benchmarked default is the fixed-shape array.
-- **Self-play & Elo** — a population tournament rates every red and blue policy on one
- shared Elo ladder.
-- **Fast** — the JAX backend reaches **~2.5×10⁵ env-steps/s (~1.0M agent-steps/s)** at batch
- 4096 on CPU. It runs a reduced transition core with an in-kernel scalar alert (not the full
- SIEM text pipeline), so it is a throughput surrogate, not a replica of the Python engine.
-- **Trained, not just scripted, baselines** — a JIT-fused IPPO trainer whose entire
- rollout runs on-device; a committed run learns mean reward 0.06 → 0.71 on `ransomware`.
-
-## Install
+## 📰 News
-```bash
-pip install 'netforge_rl[jax] @ git+https://github.com/reforcemind/NetForge_RL'
-```
+**🗓️ September 2026**
+
+- 🛡️ **20** — Gym ids: PettingZoo `netforge/ransomware-v4` (1 Red + 3 Blue) and Gymnasium `NetForge/Blue-v4`. Blue trains on SIEM belief graphs, not the true map.
+- 📟 **20** — `netforge` CLI: `run`, `evaluate`, `questions`, HTML replay. Packs for hospital, cloud, APT, IoT, OT.
+- 🏗️ **20** — Arena train / dev / hidden splits. Typed `EnvConfig`. Sphinx docs, same kit as [FlowEdge](https://github.com/reforcemind/FlowEdge).
+
+🏷️ [Changelog](docs/changelog.md) · 📘 [Docs](https://reforcemind.github.io/NetForge_RL/)
+
+***
+
+## The problem
+
+A defender does not get the true compromise map. The SOC sees Sysmon-like logs
+that can be late, dropped, or noisy. Red starts blind and only sees hosts it has
+discovered. Exploits and isolates take ticks, not one step.
+
+A gym that hands Blue an oracle, pays for isolating the whole net, or scores
+mean reward against one scripted Red is training the wrong job.
-Extras: `jax` (vectorized backend), `render` (visualization), `finetune` (LLM PEFT),
-`rllib` (Ray multi-agent training). See the
-[install guide](https://reforcemind.github.io/NetForge_RL/quickstart/) for the full list.
+## What NetForge does
-## Quick start
+Each tick, agents pick `[action_type, host_index]`. Actions run, conflicts
+resolve, SIEM fires, Blue obs update from the buffer.
+
+**Red** — discover, exploit CVEs, escalate, exfil, or hit a PLC.
+**Blue** — isolate, ACL, restore, decoys (`169.254.x.x`). Quiet Red may never
+show up in logs.
+**Kinetic** — `OverloadPLC` can end the episode for Blue.
```python
-from netforge_rl.environment import make_env
+import netforge_rl
+import gymnasium as gym
+from pettingzoo import make
-env = make_env('medium', scenario_type='ransomware', seed=0)
+env = make('parallel', 'netforge/ransomware-v4', max_ticks=80)
obs, infos = env.reset(seed=0)
-while env.agents:
- actions = {a: env.action_space(a).sample() for a in env.agents}
- obs, rewards, term, trunc, infos = env.step(actions)
- if all(term.values()) or all(trunc.values()):
- break
+blue = gym.make('NetForge/Blue-v4', max_ticks=80)
```
-### JAX vectorized (4096 environments)
+
+
+
-```python
-import jax
-from netforge_rl.backends.jax import VectorEnvSpec
-from netforge_rl.bridges.jaxmarl import JaxMARLEnv, random_action_dict
-
-env = JaxMARLEnv(spec=VectorEnvSpec(n_hosts=100, n_red=1, n_blue=3), batch_size=4096)
-key = jax.random.PRNGKey(0)
-obs, state = env.reset(key)
-obs, state, reward, done, info = env.step(key, state, random_action_dict(env, key))
-```
+## Why this, not the usual stack
-### LLM as a Blue agent
+
+
+
-```python
-from netforge_rl.semantic import state_to_text, parse_action
+Blue never sees the true map. Logs can lag. Isolate-everything wrecks SLA.
+Eval uses a Red population, not one campaign. Trainers (MAPPO / QMIX / …) stay
+in your repo.
-prompt = state_to_text(env.to_envstate(), agent_id='blue_dmz')
-# send prompt to your model, then:
-action_idx = parse_action(model_reply, 'blue_dmz', sorted(env.global_state.all_hosts))
-```
+## First run
+
+Not on PyPI. Python 3.12+.
-See the [Quick Start Guide](https://reforcemind.github.io/NetForge_RL/quickstart/) for
-difficulty presets, baselines, diagnostics, and the Gymnasium single-agent wrapper.
+```bash
+python -m pip install 'netforge-rl @ git+https://github.com/reforcemind/NetForge_RL'
+netforge run hospital_ransomware --replay replay.html
+```
-## The five scenarios
+From a clone:
-| Scenario | Red objective | Blue objective | Terminal condition |
-|---|---|---|---|
-| `ransomware` | Compromise Corporate + Secure | Contain, restore, avoid downtime | all Corporate/Secure compromised, or PLC kinetic |
-| `apt_espionage` | Stealthy persistence + exfiltration | Detect and isolate every foothold | every infected host isolated |
-| `cloud_hybrid` | Breach the Secure enclave | Protect the Secure subnet SLA | every Secure host compromised |
-| `iot_grid` | Take the grid controllers | Keep controllers healthy | all controllers compromised |
-| `ot_stuxnet` | Drive a PLC to kinetic destruction | Prevent physical damage | any PLC kinetic destruction |
+```bash
+git clone https://github.com/reforcemind/NetForge_RL.git
+cd NetForge_RL
+pip install -e ".[dev]"
+netforge run ransomware --seed 0 --max-ticks 40
+```
-## Package layout
+```bash
+python -m netforge --help
+```
-| Package | Purpose |
+| Piece | |
|---|---|
-| `environment/` | PettingZoo `NetForgeRLEnv`, difficulty presets, curriculum, Gymnasium facade |
-| `scenarios/` | 5 reward/objective families sharing one `BaseScenario` |
-| `actions/red`, `actions/blue` | ATT&CK-aligned capabilities + technique mapping |
-| `siem/`, `nlp/` | Sysmon/Windows log synthesis, NLP encoding, OCSF-style export |
-| `backends/jax/` | Vectorized `vmap`/`jit` transition kernel + NumPy reference |
-| `baselines/` | Random, heuristic, kill-chain red, JAX IPPO |
-| `diagnostics/` | Capability probes, oracle information-asymmetry, capability cards |
-| `bridges/` | RLlib, JaxMARL, CleanRL, DLPack adapters |
-| `semantic/` | LLM SOC agents, prompt grammars, fine-tuning recipes |
+| Agents | `red_operator`, `blue_dmz`, `blue_internal`, `blue_restricted` |
+| Action | `MultiDiscrete([32, 100])`, mask `int8[132]` |
+| Blue obs | vector + SIEM embedding + belief graph |
+| Families | ransomware, APT, cloud, IoT, OT/Stuxnet |
+| Trainers | your code. Not bundled. |
+
+Gate: `scripts/verify_all.sh` or `scripts/verify_local.ps1`. GitHub Actions is ruff + pytest + Sphinx. `docs.yml` deploys the site to GitHub Pages on `main`.
## Citation
```bibtex
@misc{jankowski2026netforgerlmultiagentsimulation,
- title={NetForge RL: A Multi-Agent Simulation Environment for Cyber Defense with Durative Actions},
+ title={NetForge RL: A Multi-Agent Simulation Environment for Cyber Defense with Durative Actions},
author={Igor Jankowski},
year={2026},
eprint={2604.09523},
archivePrefix={arXiv},
primaryClass={cs.LG},
- url={https://arxiv.org/abs/2604.09523},
+ url={https://arxiv.org/abs/2604.09523},
}
```
-## License
-
-MIT — see [LICENSE](LICENSE).
+MIT, with CybORG / DSTG notices in [LICENSE](LICENSE).
diff --git a/docs/DATASHEET.md b/docs/DATASHEET.md
index b3ad620..1822975 100644
--- a/docs/DATASHEET.md
+++ b/docs/DATASHEET.md
@@ -1,65 +1,19 @@
-# Technical Datasheet
-
-## Environment Composition
-- **Topology Size**: 100 host array slots. Active topologies range from 15-30 nodes. Unused nodes use `169.254.0.0/16` addresses, serving as dynamic decoys.
-- **State Representation**: `GlobalNetworkState` contains host attributes, firewall ACLs, topologies, and visibility masks. Categorical properties are encoded against static codebooks (`netforge_rl/core/functional.py`).
-- **Scenarios**: `ransomware`, `apt_espionage`, `cloud_hybrid`, `iot_grid`, `ot_stuxnet`.
-
-## Observation & Action Spaces
-- **Agents**: `red_operator`, `blue_dmz`, `blue_internal`, `blue_restricted` (PettingZoo parallel).
-- **Action space**: `MultiDiscrete([32, 100])` = `[action_type_id, target_host_index]`, with a
- 132-d binary action mask (32 action types + 100 targets).
-- **Observation** (per-agent `Dict`): `obs` (256-d), `siem_embedding` (128-d), `action_mask`
- (132-d), `adj_matrix`, `delta_t`; Blue agents also receive `blue_comm` (100-d shared channel).
-- The full, machine-readable spec is generated by `python -m benchmarks.env_spec --json`.
-
-## Difficulty & Splits
-- **Presets**: `easy` / `medium` / `hard` via `netforge_rl.environment.presets`, varying
- network size, DHCP churn, dynamic-topology rates, and SIEM latency. See the docs page on
- *Difficulty & Splits*.
-- **Held-out split**: `evaluation_mode=True` draws from a disjoint seed pool (offset 1000);
- `EVAL_SEEDS` is a frozen 20-seed evaluation suite.
-
-## Metrics and Reward Signals
-- Reward signals are defined per-scenario via `BaseScenario`; rewards are `tanh`-normalized.
- Per-scenario reward decompositions are published in `env_spec` and on each scenario's
- `REWARD_WEIGHTS`.
-- Per-episode metrics: MTTC, SLA uptime, total exfiltration, detection rate, and active-host
- compromise/isolation counts (padding excluded).
-
-## Data Generation
-- **Source**: `netforge_rl/topologies/network_generator.py`.
-- **Determinism**: Byte-identical replay under seed — observations, SIEM embeddings, infos, and
- rewards. Every stochastic source (topology, actions, exploit hypervisor, SIEM logger and
- templates, green-agent noise) is reseeded on `reset(seed)`; timestamps use a fixed epoch, not
- wall-clock. See the *Reproducibility* docs page.
-- **Splits**: `evaluation_mode=True` draws from a disjoint held-out seed pool never seen during
- training.
-- **Limitations**: Synthetic generation; no real network captures.
-
-## Diagnostics
-- Six capability probes (memory, attention, temporal, precision, safety, generalization) that
- score a policy in `[0, 1]` on isolated skills, plus an oracle information-asymmetry measure
- and a `capability_card` aggregator (JSON + radar chart).
-
-## Research Features
-- **Graph observations**: node/edge/edge-attr arrays (`core/graph_obs.py`) with fog-of-war
- masking, mapping directly to PyTorch Geometric / jraph.
-- **Deception**: decoys + honeytokens with `deception_hits` / `deception_efficacy` metrics.
-- **SOC export**: full-episode SIEM stream to OCSF-style JSON (`siem/export.py`,
- `record_siem=True`).
-- **Self-play**: population Elo tournament over red/blue policy pools
- (`benchmarks/self_play.py`).
-- **JAX telemetry**: an in-kernel numeric SIEM signal for the vectorized backend
- (`JaxMARLEnv(telemetry_obs=True)`).
-- **Trained baselines**: JIT-fused JAX IPPO trainer with committed learning curves +
- checkpoints (`benchmarks/train_curve.py`, `jax_ppo.save_params` / `load_params`).
-- **Gymnasium single-agent env**: `NetForgeSingleAgentEnv` (passes `check_env`) for
- Stable-Baselines3 / CleanRL.
-- **ATT&CK coverage**: red actions map to MITRE ATT&CK techniques; `attack_coverage` reported
- per episode (`actions/attack_map.py`).
-
-## Supported Use Cases
-- MARL policy training (PPO, IPPO, MAPPO/R-MAPPO) via PettingZoo, RLlib, JaxMARL, CleanRL bridges.
-- LLM zero-shot SOC evaluation and PEFT fine-tuning.
-- Capability diagnosis and generalization studies.
+# Datasheet
+
+| | |
+|---|---|
+| Slots | 100 hosts; 15–30 live; padding `169.254.0.0/16` (decoys) |
+| State | `GlobalNetworkState` + codebooks in `core/functional.py` |
+| Families | `ransomware`, `apt_espionage`, `cloud_hybrid`, `iot_grid`, `ot_stuxnet` |
+| Agents | `red_operator`, `blue_dmz`, `blue_internal`, `blue_restricted` |
+| Action | `MultiDiscrete([32, 100])`, mask `int8[132]` |
+| Obs | `obs` 256, `siem_embedding` 128, `action_mask` 132, `adj_matrix`, `delta_t`; Blue `blue_comm` 100 |
+| Spec | `python -m benchmarks.env_spec --json` |
+| Difficulty | `easy` / `medium` / `hard`; `evaluation_mode` seed offset 1000; `EVAL_SEEDS` |
+| Reward | per-scenario `REWARD_WEIGHTS`, `tanh`-normalized |
+| Metrics | MTTC, SLA, exfil, detection, compromise/isolation (padding excluded) |
+| Replay | seeded obs / SIEM / infos / rewards; fixed epoch, not wall-clock |
+| Limits | synthetic; no real captures |
+
+Also: belief graphs, decoys, OCSF export, Elo self-play, JAX telemetry, IPPO
+checkpoints, `NetForge/Blue-v4`, ATT&CK `attack_coverage`.
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..68d898c
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,12 @@
+SPHINXOPTS ?= -W --keep-going
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+.PHONY: help Makefile
+
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..9b127a0
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,15 @@
+# Docs map
+
+| | |
+|---|---|
+| First episode | `README.md`, {doc}`getting-started` |
+| Env ids | {doc}`python-quickstart` |
+| Red / Blue | {doc}`cybersec/overview` |
+| Package | `netforge_rl/` |
+| Figures | `docs/_static/figures/*.svg` + `*.excalidraw` |
+
+```bash
+python docs/_static/figures/build.py
+pip install -e ".[docs]"
+sphinx-build -b html -W --keep-going docs docs/_build/html
+```
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
new file mode 100644
index 0000000..1aa87e0
--- /dev/null
+++ b/docs/_static/custom.css
@@ -0,0 +1,523 @@
+@import url('https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,600;6..72,700&family=Libre+Franklin:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
+
+:root {
+ --fe-wine: #7b2733;
+ --fe-wine-deep: #5e1f2b;
+ --fe-ink: #2b2521;
+ --fe-shadow: 0 16px 40px rgba(43, 31, 20, 0.16);
+ --fe-shadow-soft: 0 8px 22px rgba(43, 31, 20, 0.1);
+ --fe-radius: 16px;
+}
+
+html[data-theme="dark"],
+body[data-theme="dark"],
+html.fe-theme-dark {
+ --fe-wine: #dd94a0;
+ --fe-wine-deep: #e3a6b0;
+ --fe-ink: #ece0cb;
+ --fe-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
+ --fe-shadow-soft: 0 8px 22px rgba(0, 0, 0, 0.28);
+}
+
+@media (prefers-color-scheme: dark) {
+ html:not([data-theme="light"]):not(.fe-theme-dark) {
+ --fe-wine: #dd94a0;
+ --fe-wine-deep: #e3a6b0;
+ --fe-ink: #ece0cb;
+ --fe-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
+ --fe-shadow-soft: 0 8px 22px rgba(0, 0, 0, 0.28);
+ }
+}
+
+body {
+ --font-stack: 'Libre Franklin', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
+ --font-stack--monospace: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
+ --font-stack--headings: 'Newsreader', Georgia, 'Times New Roman', serif;
+}
+
+.content {
+ background:
+ radial-gradient(900px 380px at 12% -10%, rgba(123, 39, 51, 0.09), transparent 62%),
+ radial-gradient(720px 320px at 92% 0%, rgba(196, 146, 64, 0.08), transparent 58%);
+}
+
+.fe-home .content {
+ background:
+ radial-gradient(1100px 520px at 18% -18%, rgba(123, 39, 51, 0.14), transparent 58%),
+ radial-gradient(900px 420px at 88% -8%, rgba(196, 146, 64, 0.12), transparent 52%),
+ var(--color-background-primary);
+}
+
+h1, h2, h3, h4, h5, h6, .sidebar-brand-text {
+ font-family: var(--font-stack--headings);
+ font-weight: 600;
+ letter-spacing: -0.01em;
+}
+h1 { font-size: 2.8rem; line-height: 1.06; font-weight: 700; margin-top: 0.35rem; margin-bottom: 0.85rem; }
+h2 {
+ font-size: 1.75rem;
+ margin-top: 2.6rem;
+ padding-bottom: 0.35rem;
+ border-bottom: 1px solid var(--color-background-border);
+}
+h3 { font-size: 1.3rem; }
+
+article { line-height: 1.72; }
+
+.content-icon-container {
+ z-index: 20;
+}
+
+.theme-toggle-container {
+ display: none !important;
+}
+
+/* —— Site nav —— */
+.fe-topnav {
+ position: relative;
+ top: auto;
+ z-index: 8;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 0.55rem 0.8rem;
+ margin: 0 -0.4rem 1.35rem;
+ padding: 0.55rem 0.4rem 0.7rem;
+ background: var(--color-background-primary);
+ border-bottom: 1px solid var(--color-background-border);
+ box-shadow: 0 10px 24px color-mix(in srgb, var(--color-background-primary) 80%, transparent);
+}
+.fe-topnav-inner {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ padding: 0.1rem 0;
+}
+.fe-topnav-link {
+ flex: 0 0 auto;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.34rem 0.7rem;
+ border-radius: 999px;
+ border: 1px solid var(--color-background-border);
+ background: var(--color-background-secondary);
+ color: var(--color-foreground-secondary);
+ font-size: 0.8rem;
+ font-weight: 600;
+ letter-spacing: 0.01em;
+ text-decoration: none !important;
+ transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
+}
+.fe-topnav-link:hover {
+ color: var(--color-brand-primary);
+ border-color: color-mix(in srgb, var(--color-brand-primary) 45%, var(--color-background-border));
+ transform: translateY(-1px);
+}
+.fe-topnav-link.is-current {
+ background: var(--fe-wine);
+ border-color: var(--fe-wine);
+ color: #f7efdc;
+}
+html[data-theme="dark"] .fe-topnav-link.is-current,
+body[data-theme="dark"] .fe-topnav-link.is-current,
+html.fe-theme-dark .fe-topnav-link.is-current {
+ color: #211b19;
+}
+html[data-theme="dark"] .fe-chips li,
+body[data-theme="dark"] .fe-chips li,
+html.fe-theme-dark .fe-chips li {
+ color: #f0b4bc;
+ background: #3a2c2a;
+ border-color: #5a4440;
+}
+html[data-theme="dark"] .fe-metric,
+html[data-theme="dark"] .fe-jump-card,
+html[data-theme="dark"] .fe-card,
+html[data-theme="dark"] .fe-explorer,
+body[data-theme="dark"] .fe-metric,
+body[data-theme="dark"] .fe-jump-card,
+body[data-theme="dark"] .fe-card,
+body[data-theme="dark"] .fe-explorer,
+html.fe-theme-dark .fe-metric,
+html.fe-theme-dark .fe-jump-card,
+html.fe-theme-dark .fe-card,
+html.fe-theme-dark .fe-explorer {
+ background: #322824;
+ border-color: #4a3c36;
+}
+html[data-theme="dark"] .fe-metric-value,
+html[data-theme="dark"] .fe-jump-kicker,
+body[data-theme="dark"] .fe-metric-value,
+body[data-theme="dark"] .fe-jump-kicker,
+html.fe-theme-dark .fe-metric-value,
+html.fe-theme-dark .fe-jump-kicker {
+ color: #f0b4bc;
+}
+html[data-theme="dark"] .fe-jump-card strong,
+body[data-theme="dark"] .fe-jump-card strong,
+html.fe-theme-dark .fe-jump-card strong {
+ color: #f3e6d2;
+}
+html[data-theme="dark"] .fe-jump-card span:last-child,
+html[data-theme="dark"] .fe-metric-label,
+body[data-theme="dark"] .fe-jump-card span:last-child,
+body[data-theme="dark"] .fe-metric-label,
+html.fe-theme-dark .fe-jump-card span:last-child,
+html.fe-theme-dark .fe-metric-label {
+ color: #cbbba4;
+}
+
+@media (prefers-color-scheme: dark) {
+ html:not([data-theme="light"]) .fe-metric,
+ html:not([data-theme="light"]) .fe-jump-card,
+ html:not([data-theme="light"]) .fe-card,
+ html:not([data-theme="light"]) .fe-explorer,
+ body:not([data-theme="light"]) .fe-metric,
+ body:not([data-theme="light"]) .fe-jump-card,
+ body:not([data-theme="light"]) .fe-card,
+ body:not([data-theme="light"]) .fe-explorer {
+ background: #322824;
+ border-color: #4a3c36;
+ }
+}
+
+.content h1 {
+ margin-top: 0.35rem;
+}
+
+/* —— Hero —— */
+.fe-hero-img,
+.fe-fig,
+img.fe-fig,
+.figure img {
+ width: 100%;
+ max-width: 960px;
+ display: block;
+ border-radius: var(--fe-radius);
+ border: 1px solid var(--color-background-border);
+ box-shadow: var(--fe-shadow);
+ margin: 0.4rem auto 1.6rem;
+}
+.fe-hero-img {
+ box-shadow: 0 18px 48px rgba(43, 31, 20, 0.28);
+}
+
+.fe-lede {
+ font-size: 1.22rem;
+ line-height: 1.6;
+ color: var(--color-foreground-secondary);
+ max-width: 46rem;
+ clear: both;
+}
+
+.fe-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+ list-style: none;
+ margin: 0.2rem 0 1.35rem;
+ padding: 0;
+}
+.fe-chips li {
+ font-size: 0.78rem;
+ font-weight: 650;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--color-brand-primary);
+ background: color-mix(in srgb, var(--color-brand-primary) 10%, var(--color-background-secondary));
+ border: 1px solid color-mix(in srgb, var(--color-brand-primary) 22%, var(--color-background-border));
+ border-radius: 999px;
+ padding: 0.32rem 0.72rem;
+ animation: fe-chip-in 0.5s ease both;
+}
+.fe-chips li:nth-child(2) { animation-delay: 0.06s; }
+.fe-chips li:nth-child(3) { animation-delay: 0.12s; }
+.fe-chips li:nth-child(4) { animation-delay: 0.18s; }
+
+.fe-metrics {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 0.8rem;
+ margin: 0.4rem 0 1.8rem;
+}
+.fe-metric {
+ background: var(--color-background-secondary);
+ border: 1px solid var(--color-background-border);
+ border-radius: 14px;
+ padding: 0.95rem 1.05rem 1.05rem;
+ box-shadow: var(--fe-shadow-soft);
+ position: relative;
+ overflow: hidden;
+ animation: fe-chip-in 0.55s ease both;
+}
+.fe-metric:nth-child(2) { animation-delay: 0.07s; }
+.fe-metric:nth-child(3) { animation-delay: 0.14s; }
+.fe-metric::before {
+ content: "";
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 4px;
+ background: var(--fe-wine);
+}
+.fe-metric-value {
+ display: block;
+ font-family: var(--font-stack--headings);
+ font-size: 1.45rem;
+ font-weight: 700;
+ color: var(--color-brand-primary);
+ letter-spacing: -0.02em;
+ line-height: 1.15;
+}
+.fe-metric-label {
+ display: block;
+ margin-top: 0.28rem;
+ font-size: 0.82rem;
+ color: var(--color-foreground-muted);
+}
+
+/* —— Jump cards —— */
+.fe-jump {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
+ gap: 0.85rem;
+ margin: 0.2rem 0 2rem;
+}
+.fe-jump-card {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ min-height: 8.4rem;
+ padding: 1rem 1.05rem 1.1rem;
+ border-radius: 14px;
+ border: 1px solid var(--color-background-border);
+ background: var(--color-background-secondary);
+ box-shadow: var(--fe-shadow-soft);
+ text-decoration: none !important;
+ color: inherit;
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
+ animation: fe-chip-in 0.55s ease both;
+}
+.fe-jump-card:nth-child(1) { animation-delay: 0.04s; }
+.fe-jump-card:nth-child(2) { animation-delay: 0.08s; }
+.fe-jump-card:nth-child(3) { animation-delay: 0.12s; }
+.fe-jump-card:nth-child(4) { animation-delay: 0.16s; }
+.fe-jump-card:nth-child(5) { animation-delay: 0.2s; }
+.fe-jump-card:nth-child(6) { animation-delay: 0.24s; }
+.fe-jump-card:hover {
+ transform: translateY(-4px);
+ border-color: color-mix(in srgb, var(--color-brand-primary) 40%, var(--color-background-border));
+ box-shadow: 0 16px 32px rgba(123, 39, 51, 0.14);
+}
+.fe-jump-kicker {
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--color-brand-primary);
+}
+.fe-jump-card strong {
+ font-family: var(--font-stack--headings);
+ font-size: 1.18rem;
+ font-weight: 650;
+ color: var(--color-foreground-primary);
+}
+.fe-jump-card span:last-child {
+ font-size: 0.9rem;
+ color: var(--color-foreground-muted);
+ line-height: 1.45;
+}
+
+/* —— News timeline —— */
+.fe-news {
+ list-style: none;
+ margin: 0.6rem 0 1.4rem;
+ padding: 0;
+}
+.fe-news-item {
+ position: relative;
+ padding: 0.85rem 0 0.95rem 1.25rem;
+ border-left: 2px solid color-mix(in srgb, var(--color-brand-primary) 35%, var(--color-background-border));
+}
+.fe-news-item::before {
+ content: "";
+ position: absolute;
+ left: -5px;
+ top: 1.15rem;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--fe-wine);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-brand-primary) 16%, transparent);
+}
+.fe-news-date {
+ display: inline-block;
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--color-brand-primary);
+ margin-bottom: 0.2rem;
+}
+.fe-news-item p { margin: 0.15rem 0 0; }
+
+.fe-inline-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.55rem 1rem;
+ margin: 0.3rem 0 1.6rem;
+ font-size: 0.95rem;
+}
+
+/* —— Existing card grids —— */
+.fe-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
+ gap: 0.9rem;
+ margin: 1.5rem 0;
+}
+.fe-card {
+ background: var(--color-background-secondary);
+ border: 1px solid var(--color-background-border);
+ border-radius: 12px;
+ padding: 0.9rem 1.1rem;
+ box-shadow: var(--fe-shadow-soft);
+}
+.fe-card h4 {
+ font-family: var(--font-stack--headings);
+ color: var(--color-brand-primary);
+ margin: 0 0 0.5rem;
+ font-size: 1.06rem;
+}
+.fe-card ul { list-style: none; margin: 0; padding: 0; }
+.fe-card li {
+ font-size: 0.92rem;
+ padding: 0.16rem 0 0.16rem 1.35rem;
+ position: relative;
+ color: var(--color-foreground-muted);
+}
+.fe-card li.done { color: var(--color-foreground-primary); }
+.fe-card li::before { content: "\25CB"; position: absolute; left: 0; color: var(--color-foreground-muted); }
+.fe-card li.done::before { content: "\2713"; color: var(--color-brand-primary); font-weight: 700; }
+
+.fe-explorer-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
+ gap: 0.9rem;
+ margin: 1.4rem 0;
+}
+.fe-explorer {
+ background: var(--color-background-secondary);
+ border: 1px solid var(--color-background-border);
+ border-radius: 12px;
+ padding: 0;
+ overflow: hidden;
+ box-shadow: var(--fe-shadow-soft);
+}
+.fe-explorer summary {
+ cursor: pointer;
+ color: var(--color-brand-primary);
+ font-family: var(--font-stack--headings);
+ font-size: 1.08rem;
+ font-weight: 600;
+ padding: 0.85rem 1rem;
+}
+.fe-explorer summary:hover { background: var(--color-background-hover); }
+.fe-explorer[open] summary { border-bottom: 1px solid var(--color-background-border); }
+.fe-explorer p { margin: 0; padding: 0.55rem 1rem; font-size: 0.92rem; }
+.fe-explorer p + p { padding-top: 0; }
+.fe-explorer code { font-size: 0.88em; }
+
+/* —— Sidebar —— */
+.sidebar-logo-container { margin: 0.7rem 0 0.4rem; }
+.sidebar-logo {
+ max-width: 100%;
+ border-radius: 10px;
+ border: 1px solid var(--color-background-border);
+ box-shadow: 0 8px 26px rgba(43, 31, 20, 0.22);
+ transition: transform 0.35s ease;
+}
+.sidebar-brand:hover .sidebar-logo { transform: translateY(-2px) rotate(-1.5deg); }
+.sidebar-brand-text { font-size: 1.4rem; letter-spacing: 0; }
+
+.sidebar-tree { line-height: 1.5; }
+.sidebar-tree .reference { font-size: 0.92rem; }
+.sidebar-tree .caption,
+.sidebar-tree .caption-text,
+.sidebar-tree p.caption {
+ font-family: var(--font-stack) !important;
+ font-size: 0.68rem !important;
+ font-weight: 700 !important;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--color-brand-primary) !important;
+ margin-top: 1.15rem;
+}
+.sidebar-tree li.current-page > .reference,
+.sidebar-tree .current-page > a {
+ font-weight: 650;
+}
+
+.highlight { border-radius: 10px; border: 1px solid var(--color-background-border); }
+code.literal { border-radius: 5px; padding: 0.1em 0.4em; border: 1px solid var(--color-background-border); }
+
+a.reference { text-decoration: none; border-bottom: 1px solid transparent; }
+a.reference:hover { border-bottom-color: currentColor; }
+
+blockquote {
+ border-left: 3px solid var(--color-brand-primary);
+ font-style: italic;
+ color: var(--color-foreground-secondary);
+}
+
+article table {
+ border-collapse: collapse;
+ overflow: hidden;
+ border-radius: 12px;
+ box-shadow: var(--fe-shadow-soft);
+}
+article table th {
+ background: color-mix(in srgb, var(--color-brand-primary) 10%, var(--color-background-secondary));
+}
+
+.related-pages {
+ margin-top: 2.4rem;
+}
+
+/* —— Motion —— */
+.fe-reveal {
+ opacity: 0;
+ transform: translateY(14px);
+ transition: opacity 0.55s ease, transform 0.55s ease;
+}
+.fe-reveal.is-in {
+ opacity: 1;
+ transform: none;
+}
+
+@keyframes fe-hero-in {
+ from { opacity: 0; transform: translateY(10px) scale(0.985); }
+ to { opacity: 1; transform: none; }
+}
+@keyframes fe-chip-in {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: none; }
+}
+@keyframes fe-nav-in {
+ from { opacity: 0; transform: translateY(-8px); }
+ to { opacity: 1; transform: none; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .fe-hero-img,
+ .fe-chips li,
+ .fe-topnav,
+ .fe-reveal,
+ .fe-jump-card,
+ .sidebar-logo {
+ animation: none !important;
+ transition: none !important;
+ opacity: 1 !important;
+ transform: none !important;
+ }
+}
diff --git a/docs/_static/custom.js b/docs/_static/custom.js
new file mode 100644
index 0000000..4f84c90
--- /dev/null
+++ b/docs/_static/custom.js
@@ -0,0 +1,76 @@
+(() => {
+ const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+ function currentPath() {
+ try {
+ return decodeURIComponent(window.location.pathname);
+ } catch {
+ return window.location.pathname;
+ }
+ }
+
+ function markHome() {
+ const path = currentPath();
+ if (
+ /\/(index\.html)?$/.test(path) ||
+ path.endsWith("/NetForge_RL") ||
+ path.endsWith("/NetForge_RL/")
+ ) {
+ document.documentElement.classList.add("fe-home");
+ }
+ }
+
+ function markNav() {
+ document.querySelectorAll(".fe-topnav-link").forEach((a) => {
+ try {
+ const href = new URL(a.href, window.location.href);
+ if (href.pathname === window.location.pathname) a.classList.add("is-current");
+ } catch {
+ /* ignore */
+ }
+ });
+ }
+
+ function reveal() {
+ const nodes = document.querySelectorAll(
+ ".fe-news-item, .fe-fig, .fe-card, article table, .fe-explorer"
+ );
+ if (reduceMotion || !("IntersectionObserver" in window)) {
+ nodes.forEach((el) => el.classList.add("is-in"));
+ return;
+ }
+ const io = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (!entry.isIntersecting) return;
+ entry.target.classList.add("is-in");
+ io.unobserve(entry.target);
+ });
+ },
+ { rootMargin: "0px 0px -6% 0px", threshold: 0.05 }
+ );
+ nodes.forEach((el) => {
+ el.classList.add("fe-reveal");
+ const top = el.getBoundingClientRect().top;
+ if (top < window.innerHeight * 0.94) {
+ el.classList.add("is-in");
+ } else {
+ io.observe(el);
+ }
+ });
+ }
+
+ function ready(fn) {
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", fn, { once: true });
+ } else {
+ fn();
+ }
+ }
+
+ ready(() => {
+ markHome();
+ markNav();
+ reveal();
+ });
+})();
diff --git a/docs/_static/figures/arena.excalidraw b/docs/_static/figures/arena.excalidraw
new file mode 100644
index 0000000..8f1521b
--- /dev/null
+++ b/docs/_static/figures/arena.excalidraw
@@ -0,0 +1,390 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {
+ "id": "t0103",
+ "type": "text",
+ "x": 28,
+ "y": 12,
+ "width": 280,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 400,
+ "version": 1,
+ "versionNonce": 403,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "eval splits",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "eval splits",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0104",
+ "type": "text",
+ "x": 280,
+ "y": 44,
+ "width": 620,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 401,
+ "version": 1,
+ "versionNonce": 404,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "train / dev / hidden - same metrics",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "train / dev / hidden - same metrics",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0105",
+ "type": "rectangle",
+ "x": 36,
+ "y": 78,
+ "width": 350,
+ "height": 180,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FFD166",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 446,
+ "version": 1,
+ "versionNonce": 453,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0106",
+ "type": "text",
+ "x": 36,
+ "y": 110,
+ "width": 350,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 447,
+ "version": 1,
+ "versionNonce": 450,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "train",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "train",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0107",
+ "type": "text",
+ "x": 36,
+ "y": 160,
+ "width": 350,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 448,
+ "version": 1,
+ "versionNonce": 451,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "public\nprocedural packs\nlearn here",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "public\nprocedural packs\nlearn here",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0108",
+ "type": "rectangle",
+ "x": 414,
+ "y": 78,
+ "width": 350,
+ "height": 180,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#2EC4B6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 824,
+ "version": 1,
+ "versionNonce": 831,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0109",
+ "type": "text",
+ "x": 414,
+ "y": 110,
+ "width": 350,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 825,
+ "version": 1,
+ "versionNonce": 828,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "dev",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "dev",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0110",
+ "type": "text",
+ "x": 414,
+ "y": 160,
+ "width": 350,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 826,
+ "version": 1,
+ "versionNonce": 829,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "public\nheld-out offset\npick a checkpoint",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "public\nheld-out offset\npick a checkpoint",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0111",
+ "type": "rectangle",
+ "x": 792,
+ "y": 78,
+ "width": 350,
+ "height": 180,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FF8FAB",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1202,
+ "version": 1,
+ "versionNonce": 1209,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0112",
+ "type": "text",
+ "x": 792,
+ "y": 110,
+ "width": 350,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1203,
+ "version": 1,
+ "versionNonce": 1206,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "hidden",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "hidden",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0113",
+ "type": "text",
+ "x": 792,
+ "y": 160,
+ "width": 350,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1204,
+ "version": 1,
+ "versionNonce": 1207,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "organizer\nreserved + OOD\nfinal check",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "organizer\nreserved + OOD\nfinal check",
+ "autoResize": true,
+ "lineHeight": 1.25
+ }
+ ],
+ "appState": {
+ "gridSize": null,
+ "viewBackgroundColor": "#FFF6DC",
+ "currentItemFontFamily": 2,
+ "width": 1180,
+ "height": 340
+ },
+ "files": {}
+}
\ No newline at end of file
diff --git a/docs/_static/figures/arena.svg b/docs/_static/figures/arena.svg
new file mode 100644
index 0000000..e9517a6
--- /dev/null
+++ b/docs/_static/figures/arena.svg
@@ -0,0 +1,12 @@
+
+eval splits
+
+
+
+
+eval splits
+train / dev / hidden - same metrics train public procedural packs learn here
+dev public held-out offset pick a checkpoint
+hidden organizer reserved + OOD final check
+mission, SLA, false positives, kinetic - not return alone
+
diff --git a/docs/_static/figures/belief.excalidraw b/docs/_static/figures/belief.excalidraw
new file mode 100644
index 0000000..7cd85e2
--- /dev/null
+++ b/docs/_static/figures/belief.excalidraw
@@ -0,0 +1,225 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {
+ "id": "t0114",
+ "type": "text",
+ "x": 28,
+ "y": 12,
+ "width": 400,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 500,
+ "version": 1,
+ "versionNonce": 503,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "SIEM vs full state",
+ "fontSize": 26,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "SIEM vs full state",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0115",
+ "type": "rectangle",
+ "x": 36,
+ "y": 78,
+ "width": 420,
+ "height": 160,
+ "angle": -0.002,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FFD166",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 501,
+ "version": 1,
+ "versionNonce": 508,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0116",
+ "type": "text",
+ "x": 36,
+ "y": 120,
+ "width": 420,
+ "height": 80,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 502,
+ "version": 1,
+ "versionNonce": 505,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "SIEM belief\nwhat a SOC would see",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "SIEM belief\nwhat a SOC would see",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0117",
+ "type": "arrow",
+ "x": 456,
+ "y": 158,
+ "width": 78,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 503,
+ "version": 1,
+ "versionNonce": 512,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 78,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0118",
+ "type": "rectangle",
+ "x": 534,
+ "y": 78,
+ "width": 410,
+ "height": 160,
+ "angle": 0.002,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FF8FAB",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 504,
+ "version": 1,
+ "versionNonce": 511,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0119",
+ "type": "text",
+ "x": 534,
+ "y": 120,
+ "width": 410,
+ "height": 80,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 505,
+ "version": 1,
+ "versionNonce": 508,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "oracle graph\ntrue compromise, debug only",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "oracle graph\ntrue compromise, debug only",
+ "autoResize": true,
+ "lineHeight": 1.25
+ }
+ ],
+ "appState": {
+ "gridSize": null,
+ "viewBackgroundColor": "#FFF6DC",
+ "currentItemFontFamily": 2,
+ "width": 980,
+ "height": 300
+ },
+ "files": {}
+}
\ No newline at end of file
diff --git a/docs/_static/figures/belief.svg b/docs/_static/figures/belief.svg
new file mode 100644
index 0000000..9c2f5e6
--- /dev/null
+++ b/docs/_static/figures/belief.svg
@@ -0,0 +1,12 @@
+
+SIEM vs full state
+
+
+
+
+SIEM vs full state
+Blue trains on logs - full map is diagnostics SIEM belief what a SOC would see partial, delayed, noisy default for Blue
+
+oracle graph true compromise debug / replay not a Blue obs
+netforge questions belief-vs-oracle
+
diff --git a/docs/_static/figures/build.py b/docs/_static/figures/build.py
new file mode 100644
index 0000000..6163fa5
--- /dev/null
+++ b/docs/_static/figures/build.py
@@ -0,0 +1,830 @@
+"""Sticky-note SVGs + Excalidraw sources (FlowEdge visual language)."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+OUT = Path(__file__).resolve().parent
+
+INK = '#2B2118'
+TITLE = '#1B4B6B'
+CANVAS = '#FFF6DC'
+EDGE = '#E8D7A8'
+SHADOW = '#E8C98A'
+YEL = '#FFD166'
+TEAL = '#2EC4B6'
+PINK = '#FF8FAB'
+ORANGE = '#FF9F43'
+PURPLE = '#7B6CF6'
+GREEN = '#6BCB77'
+CREAM = '#FFF1C2'
+BLUE = '#8EC5FF'
+RED = '#FF6B6B'
+FONT = 'Trebuchet MS, Segoe UI, sans-serif'
+
+
+def header(title: str, w: int, h: int, mid: str | None = None) -> str:
+ extra = ''
+ if mid:
+ extra = (
+ f'{mid} '
+ )
+ return f'''
+{title}
+
+
+
+
+{title}
+{extra}'''
+
+
+def _slug(title: str) -> str:
+ return ''.join(ch if ch.isalnum() else '-' for ch in title.lower()).strip('-')[:24]
+
+
+def close() -> str:
+ return ' \n'
+
+
+def card(
+ x: float, y: float, w: float, h: float, fill: str, rot: float, inner: str
+) -> str:
+ cx = x + w / 2
+ cy = y + h / 2
+ return (
+ f''
+ f' '
+ f' '
+ f'{inner} \n'
+ )
+
+
+def xml_escape(text: str) -> str:
+ return text.replace('&', '&').replace('<', '<').replace('>', '>')
+
+
+def t(
+ x: float,
+ y: float,
+ size: int,
+ weight: str,
+ fill: str,
+ text: str,
+ anchor: str = 'middle',
+) -> str:
+ return (
+ f'{xml_escape(text)} '
+ )
+
+
+def chip(
+ x: float, y: float, w: float, h: float, fill: str, rot: float, label: str
+) -> str:
+ cx = x + w / 2
+ cy = y + h / 2
+ return (
+ f''
+ f' '
+ f'{t(cx, y + 24, 14, "800", INK, label)} \n'
+ )
+
+
+def arrow(
+ x1: float, y1: float, x2: float, y2: float, title: str, qy: float | None = None
+) -> str:
+ mid = (x1 + x2) / 2
+ q = y1 if qy is None else qy
+ return (
+ f' \n'
+ )
+
+
+def write(name: str, body: str) -> None:
+ path = OUT / name
+ path.write_text(body, encoding='utf-8')
+ print('wrote', path.name, path.stat().st_size)
+
+
+# ---------------------------------------------------------------------------
+# Excalidraw
+# ---------------------------------------------------------------------------
+
+_EID = 0
+
+
+def eid(prefix: str = 'e') -> str:
+ global _EID
+ _EID += 1
+ return f'{prefix}{_EID:04d}'
+
+
+def e_rect(x, y, w, h, bg, angle=0.0, seed=1):
+ return {
+ 'id': eid('r'),
+ 'type': 'rectangle',
+ 'x': x,
+ 'y': y,
+ 'width': w,
+ 'height': h,
+ 'angle': angle,
+ 'strokeColor': INK,
+ 'backgroundColor': bg,
+ 'fillStyle': 'solid',
+ 'strokeWidth': 2,
+ 'strokeStyle': 'solid',
+ 'roughness': 1,
+ 'opacity': 100,
+ 'groupIds': [],
+ 'frameId': None,
+ 'roundness': {'type': 3},
+ 'seed': seed,
+ 'version': 1,
+ 'versionNonce': seed + 7,
+ 'isDeleted': False,
+ 'boundElements': None,
+ 'updated': 1,
+ 'link': None,
+ 'locked': False,
+ }
+
+
+def e_text(x, y, w, h, text, color=INK, size=20, align='center', seed=2):
+ return {
+ 'id': eid('t'),
+ 'type': 'text',
+ 'x': x,
+ 'y': y,
+ 'width': w,
+ 'height': h,
+ 'angle': 0,
+ 'strokeColor': color,
+ 'backgroundColor': 'transparent',
+ 'fillStyle': 'solid',
+ 'strokeWidth': 1,
+ 'strokeStyle': 'solid',
+ 'roughness': 0,
+ 'opacity': 100,
+ 'groupIds': [],
+ 'frameId': None,
+ 'roundness': None,
+ 'seed': seed,
+ 'version': 1,
+ 'versionNonce': seed + 3,
+ 'isDeleted': False,
+ 'boundElements': None,
+ 'updated': 1,
+ 'link': None,
+ 'locked': False,
+ 'text': text,
+ 'fontSize': size,
+ 'fontFamily': 2,
+ 'textAlign': align,
+ 'verticalAlign': 'middle',
+ 'containerId': None,
+ 'originalText': text,
+ 'autoResize': True,
+ 'lineHeight': 1.25,
+ }
+
+
+def e_arrow(x, y, w, h, seed=3):
+ return {
+ 'id': eid('a'),
+ 'type': 'arrow',
+ 'x': x,
+ 'y': y,
+ 'width': w,
+ 'height': h,
+ 'angle': 0,
+ 'strokeColor': TITLE,
+ 'backgroundColor': 'transparent',
+ 'fillStyle': 'solid',
+ 'strokeWidth': 2,
+ 'strokeStyle': 'solid',
+ 'roughness': 1,
+ 'opacity': 100,
+ 'groupIds': [],
+ 'frameId': None,
+ 'roundness': {'type': 2},
+ 'seed': seed,
+ 'version': 1,
+ 'versionNonce': seed + 9,
+ 'isDeleted': False,
+ 'boundElements': None,
+ 'updated': 1,
+ 'link': None,
+ 'locked': False,
+ 'startBinding': None,
+ 'endBinding': None,
+ 'lastCommittedPoint': None,
+ 'startArrowhead': None,
+ 'endArrowhead': 'arrow',
+ 'points': [[0, 0], [w, h]],
+ }
+
+
+def scene(elements: list, w: int = 1180, h: int = 400) -> dict:
+ return {
+ 'type': 'excalidraw',
+ 'version': 2,
+ 'source': 'https://excalidraw.com',
+ 'elements': elements,
+ 'appState': {
+ 'gridSize': None,
+ 'viewBackgroundColor': CANVAS,
+ 'currentItemFontFamily': 2,
+ 'width': w,
+ 'height': h,
+ },
+ 'files': {},
+ }
+
+
+def write_ex(name: str, elements: list, w: int = 1180, h: int = 400) -> None:
+ path = OUT / name
+ path.write_text(json.dumps(scene(elements, w, h), indent=2), encoding='utf-8')
+ print('wrote', path.name, path.stat().st_size)
+
+
+# ---------------------------------------------------------------------------
+# Diagrams
+# ---------------------------------------------------------------------------
+
+
+def mark_svg() -> str:
+ return f'''
+NetForge
+
+
+
+
+
+
+
+
+'''
+
+
+def purpose():
+ title = 'the match'
+ parts = [
+ header(title, 1180, 400, 'Red attacks - Blue defends - SIEM in the middle')
+ ]
+ parts.append(
+ card(
+ 36,
+ 78,
+ 350,
+ 220,
+ RED,
+ -0.08,
+ t(211, 156, 24, '800', TITLE, 'Red')
+ + t(211, 188, 16, '600', INK, 'discover, exploit')
+ + t(211, 208, 16, '600', INK, 'escalate, exfil')
+ + t(211, 228, 16, '600', INK, 'fog of war')
+ + t(211, 266, 14, '600', TITLE, 'starts blind'),
+ )
+ )
+ parts.append(
+ card(
+ 414,
+ 78,
+ 350,
+ 220,
+ TEAL,
+ 0.24,
+ t(589, 156, 24, '800', TITLE, 'Network')
+ + t(589, 188, 16, '600', INK, 'hosts, subnets')
+ + t(589, 208, 16, '600', INK, 'firewalls, decoys')
+ + t(589, 228, 16, '600', INK, 'durative actions')
+ + t(589, 266, 14, '600', TITLE, 'the gym world'),
+ )
+ )
+ parts.append(
+ card(
+ 792,
+ 78,
+ 350,
+ 220,
+ BLUE,
+ 0.10,
+ t(967, 156, 24, '800', TITLE, 'Blue')
+ + t(967, 188, 16, '600', INK, 'isolate, restore')
+ + t(967, 208, 16, '600', INK, 'decoys, EDR')
+ + t(967, 228, 16, '600', INK, 'reads SIEM only')
+ + t(967, 266, 14, '600', TITLE, 'no oracle'),
+ )
+ )
+ parts.append(chip(70, 312, 148, 36, YEL, 0.9, 'ransomware'))
+ parts.append(chip(470, 312, 174, 36, ORANGE, -1.4, 'APT / cloud / OT'))
+ parts.append(chip(848, 312, 174, 36, PINK, -1.4, '1 Red + 3 Blue'))
+ parts.append(
+ t(590, 378, 15, '800', TITLE, 'PettingZoo parallel -> or Gymnasium Blue-v4')
+ )
+ parts.append(close())
+ write('purpose.svg', ''.join(parts))
+ write_ex(
+ 'purpose.excalidraw',
+ [
+ e_text(28, 12, 280, 32, 'the match', TITLE, 28, 'left', 11),
+ e_text(
+ 240,
+ 44,
+ 700,
+ 24,
+ 'Red attacks - Blue defends - SIEM in the middle',
+ TITLE,
+ 18,
+ 'center',
+ 12,
+ ),
+ e_rect(36, 78, 350, 220, RED, -0.01, 21),
+ e_text(36, 150, 350, 40, 'Red', TITLE, 28, 'center', 22),
+ e_text(
+ 36,
+ 190,
+ 350,
+ 80,
+ 'discover, exploit\nescalate, exfil\nstarts blind',
+ INK,
+ 18,
+ 'center',
+ 23,
+ ),
+ e_rect(414, 78, 350, 220, TEAL, 0.004, 31),
+ e_text(414, 150, 350, 40, 'Network', TITLE, 28, 'center', 32),
+ e_text(
+ 414,
+ 190,
+ 350,
+ 80,
+ 'hosts, subnets\nfirewalls, decoys\ndurative actions',
+ INK,
+ 18,
+ 'center',
+ 33,
+ ),
+ e_rect(792, 78, 350, 220, BLUE, 0.002, 41),
+ e_text(792, 150, 350, 40, 'Blue', TITLE, 28, 'center', 42),
+ e_text(
+ 792,
+ 190,
+ 350,
+ 80,
+ 'isolate, restore\nreads SIEM only\nno oracle',
+ INK,
+ 18,
+ 'center',
+ 43,
+ ),
+ ],
+ 1180,
+ 400,
+ )
+
+
+def workflow():
+ title = 'one tick'
+ parts = [header(title, 980, 270)]
+ boxes = [
+ (24, 78, 155, 100, YEL, -0.13, 'reset', 'seeded net'),
+ (210, 78, 165, 100, TEAL, -0.07, 'observe', 'SIEM + mask'),
+ (406, 78, 185, 100, PURPLE, -0.15, 'act', 'Red / Blue'),
+ (622, 78, 155, 100, ORANGE, -0.25, 'resolve', 'conflicts'),
+ (808, 78, 148, 100, GREEN, -0.17, 'log', 'Sysmon-like'),
+ ]
+ for x, y, w, h, fill, rot, a, b in boxes:
+ cx = x + w / 2
+ parts.append(
+ card(
+ x,
+ y,
+ w,
+ h,
+ fill,
+ rot,
+ t(cx, 118, 16, '800', INK, a) + t(cx, 140, 14, '600', INK, b),
+ )
+ )
+ parts.append(arrow(179, 128, 210, 128, title, 133))
+ parts.append(arrow(375, 128, 406, 128, title, 126))
+ parts.append(arrow(591, 128, 622, 128, title, 128))
+ parts.append(arrow(777, 128, 808, 128, title, 129))
+ parts.append(
+ chip(
+ 220,
+ 200,
+ 527,
+ 36,
+ BLUE,
+ -1.5,
+ 'exploits and isolates take time - logs can be late',
+ )
+ )
+ parts.append(close())
+ write('workflow.svg', ''.join(parts))
+ els = [e_text(28, 12, 280, 32, 'one tick', TITLE, 28, 'left', 61)]
+ xs = [24, 210, 406, 622, 808]
+ fills = [YEL, TEAL, PURPLE, ORANGE, GREEN]
+ labels = [
+ ('reset', 'seeded net'),
+ ('observe', 'SIEM + mask'),
+ ('act', 'Red / Blue'),
+ ('resolve', 'conflicts'),
+ ('log', 'Sysmon-like'),
+ ]
+ ws = [155, 165, 185, 155, 148]
+ for i, (x, fill, (a, b), w) in enumerate(zip(xs, fills, labels, ws)):
+ els += [
+ e_rect(x, 78, w, 100, fill, 0, 70 + i),
+ e_text(x, 100, w, 28, a, INK, 20, 'center', 80 + i),
+ e_text(x, 132, w, 24, b, INK, 16, 'center', 90 + i),
+ ]
+ if i:
+ els.append(
+ e_arrow(
+ xs[i - 1] + ws[i - 1], 128, x - (xs[i - 1] + ws[i - 1]), 0, 100 + i
+ )
+ )
+ els += [
+ e_rect(220, 200, 527, 36, BLUE, -0.026, 110),
+ e_text(
+ 220,
+ 202,
+ 527,
+ 32,
+ 'exploits and isolates take time - logs can be late',
+ INK,
+ 16,
+ 'center',
+ 111,
+ ),
+ ]
+ write_ex('workflow.excalidraw', els, 980, 270)
+
+
+def why_this():
+ title = 'what Blue actually sees'
+ rows = [
+ (
+ YEL,
+ 'true compromise map',
+ 'simulator state',
+ TEAL,
+ 'SIEM belief graph',
+ 'alerts + inventory',
+ ),
+ (
+ PINK,
+ 'instant telemetry',
+ 'every exploit is obvious',
+ GREEN,
+ 'delayed / noisy logs',
+ 'log_latency, drop, decoys',
+ ),
+ (
+ ORANGE,
+ 'one giant isolate',
+ 'kill the network to win',
+ PURPLE,
+ 'SLA vs security',
+ 'keep hosts online',
+ ),
+ (
+ RED,
+ 'scripted campaign only',
+ 'one attacker forever',
+ TEAL,
+ 'Red population',
+ 'random / heuristic / kill-chain',
+ ),
+ (
+ PINK,
+ 'discrete instant acts',
+ 'done in one step',
+ GREEN,
+ 'durative actions',
+ 'ticks until complete',
+ ),
+ ]
+ parts = [header(title, 980, 570)]
+ y = 72
+ for i, (lc, lt, ls, rc, rt, rs) in enumerate(rows):
+ parts.append(
+ card(
+ 36,
+ y,
+ 380,
+ 70,
+ lc,
+ 0.04 if i % 2 == 0 else -0.12,
+ t(226, y + 25, 16, '800', INK, lt) + t(226, y + 47, 14, '600', INK, ls),
+ )
+ )
+ parts.append(arrow(428, y + 35, 534, y + 35, title, y + 32))
+ parts.append(
+ card(
+ 544,
+ y,
+ 400,
+ 70,
+ rc,
+ -0.08 if i % 2 == 0 else 0.06,
+ t(744, y + 25, 16, '800', INK, rt) + t(744, y + 47, 14, '600', INK, rs),
+ )
+ )
+ y += 78
+ parts.append(chip(36, 472, 180, 36, CREAM, -1.4, 'SOC view'))
+ parts.append(chip(248, 472, 200, 36, CREAM, -0.8, 'not god-mode'))
+ parts.append(chip(532, 472, 226, 36, YEL, -1.2, 'keep the lights on'))
+ parts.append(
+ t(
+ 490,
+ 540,
+ 14,
+ '800',
+ TITLE,
+ 'Blue trains on logs - Red trains on recon - the rest is hidden',
+ )
+ )
+ parts.append(close())
+ write('why-this.svg', ''.join(parts))
+ els = [e_text(28, 12, 520, 32, 'what Blue actually sees', TITLE, 26, 'left', 200)]
+ y = 72
+ for i, (lc, lt, ls, rc, rt, rs) in enumerate(rows):
+ els += [
+ e_rect(36, y, 380, 70, lc, 0, 210 + i),
+ e_text(36, y + 8, 380, 28, lt, INK, 18, 'center', 220 + i),
+ e_text(36, y + 36, 380, 24, ls, INK, 14, 'center', 230 + i),
+ e_arrow(416, y + 35, 128, 0, 240 + i),
+ e_rect(544, y, 400, 70, rc, 0, 250 + i),
+ e_text(544, y + 8, 400, 28, rt, INK, 18, 'center', 260 + i),
+ e_text(544, y + 36, 400, 24, rs, INK, 14, 'center', 270 + i),
+ ]
+ y += 78
+ write_ex('why-this.excalidraw', els, 980, 570)
+
+
+def status():
+ title = 'in the gym'
+ parts = [header(title, 980, 520)]
+
+ def pill(
+ x: float, y: float, w: float, fill: str, mark: str, name: str, rot: float
+ ) -> None:
+ parts.append(card(x, y, w, 56, fill, rot, ''))
+ parts.append(
+ f' '
+ )
+ parts.append(t(x + 18, y + 33, 13, '800', INK, mark))
+ parts.append(t(x + w / 2 + 10, y + 32, 13, '800', TITLE, name))
+
+ parts.append(t(36, 68, 16, '800', TITLE, 'APIs', 'start'))
+ pill(36, 80, 220, GREEN, '+', 'PettingZoo', 0.11)
+ pill(268, 80, 250, GREEN, '+', 'Gymnasium Blue-v4', 0.15)
+ pill(530, 80, 200, GREEN, '+', 'AEC wrapper', 0.08)
+
+ parts.append(t(36, 160, 16, '800', TITLE, 'World', 'start'))
+ pill(36, 172, 200, GREEN, '+', 'SIEM logs', 0.25)
+ pill(248, 172, 210, GREEN, '+', 'belief graphs', 0.06)
+ pill(470, 172, 200, GREEN, '+', 'decoys / OT', -0.05)
+ pill(682, 172, 190, TEAL, '+', 'HTML replay', -0.20)
+
+ parts.append(t(36, 252, 16, '800', TITLE, 'Agents', 'start'))
+ pill(36, 264, 200, GREEN, '+', 'heuristics', 0.14)
+ pill(248, 264, 220, GREEN, '+', 'kill-chain Red', 0.02)
+ pill(480, 264, 210, TEAL, '+', 'scripted Blue', -0.11)
+
+ parts.append(t(36, 344, 16, '800', TITLE, 'Your code', 'start'))
+ pill(36, 356, 160, CREAM, 'x', 'MAPPO', 0.11)
+ pill(208, 356, 150, CREAM, 'x', 'QMIX', -0.21)
+ pill(370, 356, 190, CREAM, 'x', 'GNN trainer', -0.18)
+ pill(572, 356, 200, CREAM, 'x', 'LLM agent', -0.08)
+
+ parts.append(
+ t(
+ 36,
+ 448,
+ 14,
+ '800',
+ TITLE,
+ 'entry: gym.make / pettingzoo.make - netforge run / evaluate',
+ 'start',
+ )
+ )
+ parts.append(
+ t(
+ 36,
+ 478,
+ 14,
+ '800',
+ TITLE,
+ '+ ships in this repo x train elsewhere, run here',
+ 'start',
+ )
+ )
+ parts.append(close())
+ write('status.svg', ''.join(parts))
+ els = [e_text(28, 12, 280, 32, 'in the gym', TITLE, 26, 'left', 300)]
+ rows_e = [
+ (
+ 80,
+ 'APIs',
+ [
+ (36, 220, 'PettingZoo', GREEN),
+ (268, 250, 'Gymnasium Blue-v4', GREEN),
+ (530, 200, 'AEC wrapper', GREEN),
+ ],
+ ),
+ (
+ 172,
+ 'World',
+ [
+ (36, 200, 'SIEM logs', GREEN),
+ (248, 210, 'belief graphs', GREEN),
+ (470, 200, 'decoys / OT', GREEN),
+ (682, 190, 'HTML replay', TEAL),
+ ],
+ ),
+ (
+ 264,
+ 'Agents',
+ [
+ (36, 200, 'heuristics', GREEN),
+ (248, 220, 'kill-chain Red', GREEN),
+ (480, 210, 'scripted Blue', TEAL),
+ ],
+ ),
+ (
+ 356,
+ 'Your code',
+ [
+ (36, 160, 'MAPPO', CREAM),
+ (208, 150, 'QMIX', CREAM),
+ (370, 190, 'GNN trainer', CREAM),
+ (572, 200, 'LLM agent', CREAM),
+ ],
+ ),
+ ]
+ for y, lab, items in rows_e:
+ els.append(e_text(36, y - 28, 200, 24, lab, TITLE, 18, 'left', y))
+ for x, w, name, fill in items:
+ els += [
+ e_rect(x, y, w, 56, fill, 0, y + x),
+ e_text(x, y + 12, w, 32, name, TITLE, 16, 'center', y + x + 1),
+ ]
+ write_ex('status.excalidraw', els, 980, 520)
+
+
+def arena():
+ title = 'eval splits'
+ parts = [header(title, 1180, 340, 'train / dev / hidden - same metrics')]
+ cols = [
+ (36, YEL, 'train', 'public', 'procedural packs', 'learn here'),
+ (414, TEAL, 'dev', 'public', 'held-out offset', 'pick a checkpoint'),
+ (792, PINK, 'hidden', 'organizer', 'reserved + OOD', 'final check'),
+ ]
+ for x, fill, name, who, how, use in cols:
+ cx = x + 175
+ parts.append(
+ card(
+ x,
+ 78,
+ 350,
+ 180,
+ fill,
+ 0.12 if fill == YEL else (-0.1 if fill == TEAL else 0.06),
+ t(cx, 130, 28, '800', TITLE, name)
+ + t(cx, 168, 16, '600', INK, who)
+ + t(cx, 192, 16, '600', INK, how)
+ + t(cx, 228, 14, '600', TITLE, use),
+ )
+ )
+ parts.append(
+ chip(
+ 200,
+ 274,
+ 780,
+ 36,
+ BLUE,
+ -0.7,
+ 'mission, SLA, false positives, kinetic - not return alone',
+ )
+ )
+ parts.append(close())
+ write('arena.svg', ''.join(parts))
+ els = [
+ e_text(28, 12, 280, 32, 'eval splits', TITLE, 28, 'left', 400),
+ e_text(
+ 280,
+ 44,
+ 620,
+ 24,
+ 'train / dev / hidden - same metrics',
+ TITLE,
+ 18,
+ 'center',
+ 401,
+ ),
+ ]
+ for x, fill, name, who, how, use in cols:
+ els += [
+ e_rect(x, 78, 350, 180, fill, 0, 410 + x),
+ e_text(x, 110, 350, 40, name, TITLE, 28, 'center', 411 + x),
+ e_text(x, 160, 350, 70, f'{who}\n{how}\n{use}', INK, 18, 'center', 412 + x),
+ ]
+ write_ex('arena.excalidraw', els, 1180, 340)
+
+
+def belief():
+ title = 'SIEM vs full state'
+ parts = [
+ header(title, 980, 300, 'Blue trains on logs - full map is diagnostics')
+ ]
+ parts.append(
+ card(
+ 36,
+ 78,
+ 420,
+ 160,
+ YEL,
+ -0.12,
+ t(246, 130, 22, '800', TITLE, 'SIEM belief')
+ + t(246, 162, 16, '600', INK, 'what a SOC would see')
+ + t(246, 186, 16, '600', INK, 'partial, delayed, noisy')
+ + t(246, 214, 14, '600', TITLE, 'default for Blue'),
+ )
+ )
+ parts.append(arrow(468, 158, 524, 158, title, 150))
+ parts.append(
+ card(
+ 534,
+ 78,
+ 410,
+ 160,
+ PINK,
+ 0.14,
+ t(739, 130, 22, '800', TITLE, 'oracle graph')
+ + t(739, 162, 16, '600', INK, 'true compromise')
+ + t(739, 186, 16, '600', INK, 'debug / replay')
+ + t(739, 214, 14, '600', TITLE, 'not a Blue obs'),
+ )
+ )
+ parts.append(
+ chip(220, 250, 540, 36, TEAL, -1.1, 'netforge questions belief-vs-oracle')
+ )
+ parts.append(close())
+ write('belief.svg', ''.join(parts))
+ write_ex(
+ 'belief.excalidraw',
+ [
+ e_text(28, 12, 400, 32, 'SIEM vs full state', TITLE, 26, 'left', 500),
+ e_rect(36, 78, 420, 160, YEL, -0.002, 501),
+ e_text(
+ 36,
+ 120,
+ 420,
+ 80,
+ 'SIEM belief\nwhat a SOC would see',
+ INK,
+ 20,
+ 'center',
+ 502,
+ ),
+ e_arrow(456, 158, 78, 0, 503),
+ e_rect(534, 78, 410, 160, PINK, 0.002, 504),
+ e_text(
+ 534,
+ 120,
+ 410,
+ 80,
+ 'oracle graph\ntrue compromise, debug only',
+ INK,
+ 20,
+ 'center',
+ 505,
+ ),
+ ],
+ 980,
+ 300,
+ )
+
+
+def main() -> None:
+ (OUT / 'mark.svg').write_text(mark_svg(), encoding='utf-8')
+ print('wrote mark.svg')
+ purpose()
+ workflow()
+ why_this()
+ status()
+ arena()
+ belief()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/docs/_static/figures/mark.svg b/docs/_static/figures/mark.svg
new file mode 100644
index 0000000..20457b5
--- /dev/null
+++ b/docs/_static/figures/mark.svg
@@ -0,0 +1,10 @@
+
+NetForge
+
+
+
+
+
+
+
+
diff --git a/docs/_static/figures/purpose.excalidraw b/docs/_static/figures/purpose.excalidraw
new file mode 100644
index 0000000..f613397
--- /dev/null
+++ b/docs/_static/figures/purpose.excalidraw
@@ -0,0 +1,390 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {
+ "id": "t0001",
+ "type": "text",
+ "x": 28,
+ "y": 12,
+ "width": 280,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 11,
+ "version": 1,
+ "versionNonce": 14,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "the match",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "the match",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0002",
+ "type": "text",
+ "x": 240,
+ "y": 44,
+ "width": 700,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 12,
+ "version": 1,
+ "versionNonce": 15,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Red attacks - Blue defends - SIEM in the middle",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Red attacks - Blue defends - SIEM in the middle",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0003",
+ "type": "rectangle",
+ "x": 36,
+ "y": 78,
+ "width": 350,
+ "height": 220,
+ "angle": -0.01,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FF6B6B",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 21,
+ "version": 1,
+ "versionNonce": 28,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0004",
+ "type": "text",
+ "x": 36,
+ "y": 150,
+ "width": 350,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 22,
+ "version": 1,
+ "versionNonce": 25,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Red",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Red",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0005",
+ "type": "text",
+ "x": 36,
+ "y": 190,
+ "width": 350,
+ "height": 80,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 23,
+ "version": 1,
+ "versionNonce": 26,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "discover, exploit\nescalate, exfil\nstarts blind",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "discover, exploit\nescalate, exfil\nstarts blind",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0006",
+ "type": "rectangle",
+ "x": 414,
+ "y": 78,
+ "width": 350,
+ "height": 220,
+ "angle": 0.004,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#2EC4B6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 31,
+ "version": 1,
+ "versionNonce": 38,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0007",
+ "type": "text",
+ "x": 414,
+ "y": 150,
+ "width": 350,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 32,
+ "version": 1,
+ "versionNonce": 35,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Network",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Network",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0008",
+ "type": "text",
+ "x": 414,
+ "y": 190,
+ "width": 350,
+ "height": 80,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 33,
+ "version": 1,
+ "versionNonce": 36,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "hosts, subnets\nfirewalls, decoys\ndurative actions",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "hosts, subnets\nfirewalls, decoys\ndurative actions",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0009",
+ "type": "rectangle",
+ "x": 792,
+ "y": 78,
+ "width": 350,
+ "height": 220,
+ "angle": 0.002,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#8EC5FF",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 41,
+ "version": 1,
+ "versionNonce": 48,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0010",
+ "type": "text",
+ "x": 792,
+ "y": 150,
+ "width": 350,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 42,
+ "version": 1,
+ "versionNonce": 45,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Blue",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Blue",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0011",
+ "type": "text",
+ "x": 792,
+ "y": 190,
+ "width": 350,
+ "height": 80,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 43,
+ "version": 1,
+ "versionNonce": 46,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "isolate, restore\nreads SIEM only\nno oracle",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "isolate, restore\nreads SIEM only\nno oracle",
+ "autoResize": true,
+ "lineHeight": 1.25
+ }
+ ],
+ "appState": {
+ "gridSize": null,
+ "viewBackgroundColor": "#FFF6DC",
+ "currentItemFontFamily": 2,
+ "width": 1180,
+ "height": 400
+ },
+ "files": {}
+}
\ No newline at end of file
diff --git a/docs/_static/figures/purpose.svg b/docs/_static/figures/purpose.svg
new file mode 100644
index 0000000..ada515a
--- /dev/null
+++ b/docs/_static/figures/purpose.svg
@@ -0,0 +1,14 @@
+
+the match
+
+
+
+
+the match
+Red attacks - Blue defends - SIEM in the middle Red discover, exploit escalate, exfil fog of war starts blind
+Network hosts, subnets firewalls, decoys durative actions the gym world
+Blue isolate, restore decoys, EDR reads SIEM only no oracle
+ransomware
+APT / cloud / OT
+1 Red + 3 Blue
+PettingZoo parallel -> or Gymnasium Blue-v4
diff --git a/docs/_static/figures/status.excalidraw b/docs/_static/figures/status.excalidraw
new file mode 100644
index 0000000..2aa0dd9
--- /dev/null
+++ b/docs/_static/figures/status.excalidraw
@@ -0,0 +1,1105 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {
+ "id": "t0070",
+ "type": "text",
+ "x": 28,
+ "y": 12,
+ "width": 280,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 300,
+ "version": 1,
+ "versionNonce": 303,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "in the gym",
+ "fontSize": 26,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "in the gym",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0071",
+ "type": "text",
+ "x": 36,
+ "y": 52,
+ "width": 200,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 80,
+ "version": 1,
+ "versionNonce": 83,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "APIs",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "APIs",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0072",
+ "type": "rectangle",
+ "x": 36,
+ "y": 80,
+ "width": 220,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 116,
+ "version": 1,
+ "versionNonce": 123,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0073",
+ "type": "text",
+ "x": 36,
+ "y": 92,
+ "width": 220,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 117,
+ "version": 1,
+ "versionNonce": 120,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "PettingZoo",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "PettingZoo",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0074",
+ "type": "rectangle",
+ "x": 268,
+ "y": 80,
+ "width": 250,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 348,
+ "version": 1,
+ "versionNonce": 355,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0075",
+ "type": "text",
+ "x": 268,
+ "y": 92,
+ "width": 250,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 349,
+ "version": 1,
+ "versionNonce": 352,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Gymnasium Blue-v4",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Gymnasium Blue-v4",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0076",
+ "type": "rectangle",
+ "x": 530,
+ "y": 80,
+ "width": 200,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 610,
+ "version": 1,
+ "versionNonce": 617,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0077",
+ "type": "text",
+ "x": 530,
+ "y": 92,
+ "width": 200,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 611,
+ "version": 1,
+ "versionNonce": 614,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "AEC wrapper",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "AEC wrapper",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0078",
+ "type": "text",
+ "x": 36,
+ "y": 144,
+ "width": 200,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 172,
+ "version": 1,
+ "versionNonce": 175,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "World",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "World",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0079",
+ "type": "rectangle",
+ "x": 36,
+ "y": 172,
+ "width": 200,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 208,
+ "version": 1,
+ "versionNonce": 215,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0080",
+ "type": "text",
+ "x": 36,
+ "y": 184,
+ "width": 200,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 209,
+ "version": 1,
+ "versionNonce": 212,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "SIEM logs",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "SIEM logs",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0081",
+ "type": "rectangle",
+ "x": 248,
+ "y": 172,
+ "width": 210,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 420,
+ "version": 1,
+ "versionNonce": 427,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0082",
+ "type": "text",
+ "x": 248,
+ "y": 184,
+ "width": 210,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 421,
+ "version": 1,
+ "versionNonce": 424,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "belief graphs",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "belief graphs",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0083",
+ "type": "rectangle",
+ "x": 470,
+ "y": 172,
+ "width": 200,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 642,
+ "version": 1,
+ "versionNonce": 649,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0084",
+ "type": "text",
+ "x": 470,
+ "y": 184,
+ "width": 200,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 643,
+ "version": 1,
+ "versionNonce": 646,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "decoys / OT",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "decoys / OT",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0085",
+ "type": "rectangle",
+ "x": 682,
+ "y": 172,
+ "width": 190,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#2EC4B6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 854,
+ "version": 1,
+ "versionNonce": 861,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0086",
+ "type": "text",
+ "x": 682,
+ "y": 184,
+ "width": 190,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 855,
+ "version": 1,
+ "versionNonce": 858,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "HTML replay",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "HTML replay",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0087",
+ "type": "text",
+ "x": 36,
+ "y": 236,
+ "width": 200,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 264,
+ "version": 1,
+ "versionNonce": 267,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Agents",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Agents",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0088",
+ "type": "rectangle",
+ "x": 36,
+ "y": 264,
+ "width": 200,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 300,
+ "version": 1,
+ "versionNonce": 307,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0089",
+ "type": "text",
+ "x": 36,
+ "y": 276,
+ "width": 200,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 301,
+ "version": 1,
+ "versionNonce": 304,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "heuristics",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "heuristics",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0090",
+ "type": "rectangle",
+ "x": 248,
+ "y": 264,
+ "width": 220,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 512,
+ "version": 1,
+ "versionNonce": 519,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0091",
+ "type": "text",
+ "x": 248,
+ "y": 276,
+ "width": 220,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 513,
+ "version": 1,
+ "versionNonce": 516,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "kill-chain Red",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "kill-chain Red",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0092",
+ "type": "rectangle",
+ "x": 480,
+ "y": 264,
+ "width": 210,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#2EC4B6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 744,
+ "version": 1,
+ "versionNonce": 751,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0093",
+ "type": "text",
+ "x": 480,
+ "y": 276,
+ "width": 210,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 745,
+ "version": 1,
+ "versionNonce": 748,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "scripted Blue",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "scripted Blue",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0094",
+ "type": "text",
+ "x": 36,
+ "y": 328,
+ "width": 200,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 356,
+ "version": 1,
+ "versionNonce": 359,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Your code",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Your code",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0095",
+ "type": "rectangle",
+ "x": 36,
+ "y": 356,
+ "width": 160,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FFF1C2",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 392,
+ "version": 1,
+ "versionNonce": 399,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0096",
+ "type": "text",
+ "x": 36,
+ "y": 368,
+ "width": 160,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 393,
+ "version": 1,
+ "versionNonce": 396,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "MAPPO",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "MAPPO",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0097",
+ "type": "rectangle",
+ "x": 208,
+ "y": 356,
+ "width": 150,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FFF1C2",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 564,
+ "version": 1,
+ "versionNonce": 571,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0098",
+ "type": "text",
+ "x": 208,
+ "y": 368,
+ "width": 150,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 565,
+ "version": 1,
+ "versionNonce": 568,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "QMIX",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "QMIX",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0099",
+ "type": "rectangle",
+ "x": 370,
+ "y": 356,
+ "width": 190,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FFF1C2",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 726,
+ "version": 1,
+ "versionNonce": 733,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0100",
+ "type": "text",
+ "x": 370,
+ "y": 368,
+ "width": 190,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 727,
+ "version": 1,
+ "versionNonce": 730,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "GNN trainer",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "GNN trainer",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0101",
+ "type": "rectangle",
+ "x": 572,
+ "y": 356,
+ "width": 200,
+ "height": 56,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FFF1C2",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 928,
+ "version": 1,
+ "versionNonce": 935,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0102",
+ "type": "text",
+ "x": 572,
+ "y": 368,
+ "width": 200,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 929,
+ "version": 1,
+ "versionNonce": 932,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "LLM agent",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "LLM agent",
+ "autoResize": true,
+ "lineHeight": 1.25
+ }
+ ],
+ "appState": {
+ "gridSize": null,
+ "viewBackgroundColor": "#FFF6DC",
+ "currentItemFontFamily": 2,
+ "width": 980,
+ "height": 520
+ },
+ "files": {}
+}
\ No newline at end of file
diff --git a/docs/_static/figures/status.svg b/docs/_static/figures/status.svg
new file mode 100644
index 0000000..febacbf
--- /dev/null
+++ b/docs/_static/figures/status.svg
@@ -0,0 +1,22 @@
+
+in the gym
+
+
+
+
+in the gym
+APIs
++ PettingZoo
++ Gymnasium Blue-v4
++ AEC wrapper World
++ SIEM logs
++ belief graphs
++ decoys / OT
++ HTML replay Agents
++ heuristics
++ kill-chain Red
++ scripted Blue Your code
+x MAPPO
+x QMIX
+x GNN trainer
+x LLM agent entry: gym.make / pettingzoo.make - netforge run / evaluate + ships in this repo x train elsewhere, run here
diff --git a/docs/_static/figures/why-this.excalidraw b/docs/_static/figures/why-this.excalidraw
new file mode 100644
index 0000000..b76964c
--- /dev/null
+++ b/docs/_static/figures/why-this.excalidraw
@@ -0,0 +1,1281 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {
+ "id": "t0034",
+ "type": "text",
+ "x": 28,
+ "y": 12,
+ "width": 520,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 200,
+ "version": 1,
+ "versionNonce": 203,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "what Blue actually sees",
+ "fontSize": 26,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "what Blue actually sees",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0035",
+ "type": "rectangle",
+ "x": 36,
+ "y": 72,
+ "width": 380,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FFD166",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 210,
+ "version": 1,
+ "versionNonce": 217,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0036",
+ "type": "text",
+ "x": 36,
+ "y": 80,
+ "width": 380,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 220,
+ "version": 1,
+ "versionNonce": 223,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "true compromise map",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "true compromise map",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0037",
+ "type": "text",
+ "x": 36,
+ "y": 108,
+ "width": 380,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 230,
+ "version": 1,
+ "versionNonce": 233,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "simulator state",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "simulator state",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0038",
+ "type": "arrow",
+ "x": 416,
+ "y": 107,
+ "width": 128,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 240,
+ "version": 1,
+ "versionNonce": 249,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 128,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0039",
+ "type": "rectangle",
+ "x": 544,
+ "y": 72,
+ "width": 400,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#2EC4B6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 250,
+ "version": 1,
+ "versionNonce": 257,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0040",
+ "type": "text",
+ "x": 544,
+ "y": 80,
+ "width": 400,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 260,
+ "version": 1,
+ "versionNonce": 263,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "SIEM belief graph",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "SIEM belief graph",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0041",
+ "type": "text",
+ "x": 544,
+ "y": 108,
+ "width": 400,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 270,
+ "version": 1,
+ "versionNonce": 273,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "alerts + inventory",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "alerts + inventory",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0042",
+ "type": "rectangle",
+ "x": 36,
+ "y": 150,
+ "width": 380,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FF8FAB",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 211,
+ "version": 1,
+ "versionNonce": 218,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0043",
+ "type": "text",
+ "x": 36,
+ "y": 158,
+ "width": 380,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 221,
+ "version": 1,
+ "versionNonce": 224,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "instant telemetry",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "instant telemetry",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0044",
+ "type": "text",
+ "x": 36,
+ "y": 186,
+ "width": 380,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 231,
+ "version": 1,
+ "versionNonce": 234,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "every exploit is obvious",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "every exploit is obvious",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0045",
+ "type": "arrow",
+ "x": 416,
+ "y": 185,
+ "width": 128,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 241,
+ "version": 1,
+ "versionNonce": 250,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 128,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0046",
+ "type": "rectangle",
+ "x": 544,
+ "y": 150,
+ "width": 400,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 251,
+ "version": 1,
+ "versionNonce": 258,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0047",
+ "type": "text",
+ "x": 544,
+ "y": 158,
+ "width": 400,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 261,
+ "version": 1,
+ "versionNonce": 264,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "delayed / noisy logs",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "delayed / noisy logs",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0048",
+ "type": "text",
+ "x": 544,
+ "y": 186,
+ "width": 400,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 271,
+ "version": 1,
+ "versionNonce": 274,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "log_latency, drop, decoys",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "log_latency, drop, decoys",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0049",
+ "type": "rectangle",
+ "x": 36,
+ "y": 228,
+ "width": 380,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FF9F43",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 212,
+ "version": 1,
+ "versionNonce": 219,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0050",
+ "type": "text",
+ "x": 36,
+ "y": 236,
+ "width": 380,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 222,
+ "version": 1,
+ "versionNonce": 225,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "one giant isolate",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "one giant isolate",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0051",
+ "type": "text",
+ "x": 36,
+ "y": 264,
+ "width": 380,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 232,
+ "version": 1,
+ "versionNonce": 235,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "kill the network to win",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "kill the network to win",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0052",
+ "type": "arrow",
+ "x": 416,
+ "y": 263,
+ "width": 128,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 242,
+ "version": 1,
+ "versionNonce": 251,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 128,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0053",
+ "type": "rectangle",
+ "x": 544,
+ "y": 228,
+ "width": 400,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#7B6CF6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 252,
+ "version": 1,
+ "versionNonce": 259,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0054",
+ "type": "text",
+ "x": 544,
+ "y": 236,
+ "width": 400,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 262,
+ "version": 1,
+ "versionNonce": 265,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "SLA vs security",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "SLA vs security",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0055",
+ "type": "text",
+ "x": 544,
+ "y": 264,
+ "width": 400,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 272,
+ "version": 1,
+ "versionNonce": 275,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "keep hosts online",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "keep hosts online",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0056",
+ "type": "rectangle",
+ "x": 36,
+ "y": 306,
+ "width": 380,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FF6B6B",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 213,
+ "version": 1,
+ "versionNonce": 220,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0057",
+ "type": "text",
+ "x": 36,
+ "y": 314,
+ "width": 380,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 223,
+ "version": 1,
+ "versionNonce": 226,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "scripted campaign only",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "scripted campaign only",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0058",
+ "type": "text",
+ "x": 36,
+ "y": 342,
+ "width": 380,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 233,
+ "version": 1,
+ "versionNonce": 236,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "one attacker forever",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "one attacker forever",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0059",
+ "type": "arrow",
+ "x": 416,
+ "y": 341,
+ "width": 128,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 243,
+ "version": 1,
+ "versionNonce": 252,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 128,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0060",
+ "type": "rectangle",
+ "x": 544,
+ "y": 306,
+ "width": 400,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#2EC4B6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 253,
+ "version": 1,
+ "versionNonce": 260,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0061",
+ "type": "text",
+ "x": 544,
+ "y": 314,
+ "width": 400,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 263,
+ "version": 1,
+ "versionNonce": 266,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Red population",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Red population",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0062",
+ "type": "text",
+ "x": 544,
+ "y": 342,
+ "width": 400,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 273,
+ "version": 1,
+ "versionNonce": 276,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "random / heuristic / kill-chain",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "random / heuristic / kill-chain",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0063",
+ "type": "rectangle",
+ "x": 36,
+ "y": 384,
+ "width": 380,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FF8FAB",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 214,
+ "version": 1,
+ "versionNonce": 221,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0064",
+ "type": "text",
+ "x": 36,
+ "y": 392,
+ "width": 380,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 224,
+ "version": 1,
+ "versionNonce": 227,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "discrete instant acts",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "discrete instant acts",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0065",
+ "type": "text",
+ "x": 36,
+ "y": 420,
+ "width": 380,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 234,
+ "version": 1,
+ "versionNonce": 237,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "done in one step",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "done in one step",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0066",
+ "type": "arrow",
+ "x": 416,
+ "y": 419,
+ "width": 128,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 244,
+ "version": 1,
+ "versionNonce": 253,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 128,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0067",
+ "type": "rectangle",
+ "x": 544,
+ "y": 384,
+ "width": 400,
+ "height": 70,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 254,
+ "version": 1,
+ "versionNonce": 261,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0068",
+ "type": "text",
+ "x": 544,
+ "y": 392,
+ "width": 400,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 264,
+ "version": 1,
+ "versionNonce": 267,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "durative actions",
+ "fontSize": 18,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "durative actions",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0069",
+ "type": "text",
+ "x": 544,
+ "y": 420,
+ "width": 400,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 274,
+ "version": 1,
+ "versionNonce": 277,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "ticks until complete",
+ "fontSize": 14,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "ticks until complete",
+ "autoResize": true,
+ "lineHeight": 1.25
+ }
+ ],
+ "appState": {
+ "gridSize": null,
+ "viewBackgroundColor": "#FFF6DC",
+ "currentItemFontFamily": 2,
+ "width": 980,
+ "height": 570
+ },
+ "files": {}
+}
\ No newline at end of file
diff --git a/docs/_static/figures/why-this.svg b/docs/_static/figures/why-this.svg
new file mode 100644
index 0000000..7abe2b8
--- /dev/null
+++ b/docs/_static/figures/why-this.svg
@@ -0,0 +1,26 @@
+
+what Blue actually sees
+
+
+
+
+what Blue actually sees
+true compromise map simulator state
+
+SIEM belief graph alerts + inventory
+instant telemetry every exploit is obvious
+
+delayed / noisy logs log_latency, drop, decoys
+one giant isolate kill the network to win
+
+SLA vs security keep hosts online
+scripted campaign only one attacker forever
+
+Red population random / heuristic / kill-chain
+discrete instant acts done in one step
+
+durative actions ticks until complete
+SOC view
+not god-mode
+keep the lights on
+Blue trains on logs - Red trains on recon - the rest is hidden
diff --git a/docs/_static/figures/workflow.excalidraw b/docs/_static/figures/workflow.excalidraw
new file mode 100644
index 0000000..c856b0f
--- /dev/null
+++ b/docs/_static/figures/workflow.excalidraw
@@ -0,0 +1,797 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {
+ "id": "t0012",
+ "type": "text",
+ "x": 28,
+ "y": 12,
+ "width": 280,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 61,
+ "version": 1,
+ "versionNonce": 64,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "one tick",
+ "fontSize": 28,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "one tick",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0013",
+ "type": "rectangle",
+ "x": 24,
+ "y": 78,
+ "width": 155,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FFD166",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 70,
+ "version": 1,
+ "versionNonce": 77,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0014",
+ "type": "text",
+ "x": 24,
+ "y": 100,
+ "width": 155,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 80,
+ "version": 1,
+ "versionNonce": 83,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "reset",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "reset",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0015",
+ "type": "text",
+ "x": 24,
+ "y": 132,
+ "width": 155,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 90,
+ "version": 1,
+ "versionNonce": 93,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "seeded net",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "seeded net",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "r0016",
+ "type": "rectangle",
+ "x": 210,
+ "y": 78,
+ "width": 165,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#2EC4B6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 71,
+ "version": 1,
+ "versionNonce": 78,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0017",
+ "type": "text",
+ "x": 210,
+ "y": 100,
+ "width": 165,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 81,
+ "version": 1,
+ "versionNonce": 84,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "observe",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "observe",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0018",
+ "type": "text",
+ "x": 210,
+ "y": 132,
+ "width": 165,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 91,
+ "version": 1,
+ "versionNonce": 94,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "SIEM + mask",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "SIEM + mask",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0019",
+ "type": "arrow",
+ "x": 179,
+ "y": 128,
+ "width": 31,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 101,
+ "version": 1,
+ "versionNonce": 110,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 31,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0020",
+ "type": "rectangle",
+ "x": 406,
+ "y": 78,
+ "width": 185,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#7B6CF6",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 72,
+ "version": 1,
+ "versionNonce": 79,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0021",
+ "type": "text",
+ "x": 406,
+ "y": 100,
+ "width": 185,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 82,
+ "version": 1,
+ "versionNonce": 85,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "act",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "act",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0022",
+ "type": "text",
+ "x": 406,
+ "y": 132,
+ "width": 185,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 92,
+ "version": 1,
+ "versionNonce": 95,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Red / Blue",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Red / Blue",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0023",
+ "type": "arrow",
+ "x": 375,
+ "y": 128,
+ "width": 31,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 102,
+ "version": 1,
+ "versionNonce": 111,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 31,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0024",
+ "type": "rectangle",
+ "x": 622,
+ "y": 78,
+ "width": 155,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#FF9F43",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 73,
+ "version": 1,
+ "versionNonce": 80,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0025",
+ "type": "text",
+ "x": 622,
+ "y": 100,
+ "width": 155,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 83,
+ "version": 1,
+ "versionNonce": 86,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "resolve",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "resolve",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0026",
+ "type": "text",
+ "x": 622,
+ "y": 132,
+ "width": 155,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 93,
+ "version": 1,
+ "versionNonce": 96,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "conflicts",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "conflicts",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0027",
+ "type": "arrow",
+ "x": 591,
+ "y": 128,
+ "width": 31,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 103,
+ "version": 1,
+ "versionNonce": 112,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 31,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0028",
+ "type": "rectangle",
+ "x": 808,
+ "y": 78,
+ "width": 148,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#6BCB77",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 74,
+ "version": 1,
+ "versionNonce": 81,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0029",
+ "type": "text",
+ "x": 808,
+ "y": 100,
+ "width": 148,
+ "height": 28,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 84,
+ "version": 1,
+ "versionNonce": 87,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "log",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "log",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "t0030",
+ "type": "text",
+ "x": 808,
+ "y": 132,
+ "width": 148,
+ "height": 24,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 94,
+ "version": 1,
+ "versionNonce": 97,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Sysmon-like",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "Sysmon-like",
+ "autoResize": true,
+ "lineHeight": 1.25
+ },
+ {
+ "id": "a0031",
+ "type": "arrow",
+ "x": 777,
+ "y": 128,
+ "width": 31,
+ "height": 0,
+ "angle": 0,
+ "strokeColor": "#1B4B6B",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 2
+ },
+ "seed": 104,
+ "version": 1,
+ "versionNonce": 113,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "startBinding": null,
+ "endBinding": null,
+ "lastCommittedPoint": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 31,
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "r0032",
+ "type": "rectangle",
+ "x": 220,
+ "y": 200,
+ "width": 527,
+ "height": 36,
+ "angle": -0.026,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "#8EC5FF",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 1,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 110,
+ "version": 1,
+ "versionNonce": 117,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "t0033",
+ "type": "text",
+ "x": 220,
+ "y": 202,
+ "width": 527,
+ "height": 32,
+ "angle": 0,
+ "strokeColor": "#2B2118",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 111,
+ "version": 1,
+ "versionNonce": 114,
+ "isDeleted": false,
+ "boundElements": null,
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "exploits and isolates take time - logs can be late",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": null,
+ "originalText": "exploits and isolates take time - logs can be late",
+ "autoResize": true,
+ "lineHeight": 1.25
+ }
+ ],
+ "appState": {
+ "gridSize": null,
+ "viewBackgroundColor": "#FFF6DC",
+ "currentItemFontFamily": 2,
+ "width": 980,
+ "height": 270
+ },
+ "files": {}
+}
\ No newline at end of file
diff --git a/docs/_static/figures/workflow.svg b/docs/_static/figures/workflow.svg
new file mode 100644
index 0000000..78c4d3f
--- /dev/null
+++ b/docs/_static/figures/workflow.svg
@@ -0,0 +1,18 @@
+
+one tick
+
+
+
+
+one tick
+reset seeded net
+observe SIEM + mask
+act Red / Blue
+resolve conflicts
+log Sysmon-like
+
+
+
+
+exploits and isolates take time - logs can be late
+
diff --git a/docs/_templates/page.html b/docs/_templates/page.html
new file mode 100644
index 0000000..3907aa5
--- /dev/null
+++ b/docs/_templates/page.html
@@ -0,0 +1,17 @@
+{% extends "!page.html" %}
+
+{% block content %}
+
+
+
+{{ super() }}
+{% endblock %}
diff --git a/docs/api/environment.md b/docs/api/environment.md
index 9583e0b..d87752f 100644
--- a/docs/api/environment.md
+++ b/docs/api/environment.md
@@ -1,5 +1,12 @@
# Environment API
-::: netforge_rl.environment.parallel_env
-::: netforge_rl.backends.jax
-::: netforge_rl.core.functional
\ No newline at end of file
+```{eval-rst}
+.. automodule:: netforge_rl.environment.parallel_env
+ :members: NetForgeRLEnv
+ :no-undoc-members:
+ :show-inheritance:
+
+.. automodule:: netforge_rl.environment.registry
+ :members:
+ :no-undoc-members:
+```
diff --git a/docs/api/nlp.md b/docs/api/nlp.md
index 043a7fd..cfd06a5 100644
--- a/docs/api/nlp.md
+++ b/docs/api/nlp.md
@@ -1,3 +1,7 @@
-# NLP Encoder API
+# NLP encoder API
-::: netforge_rl.nlp.log_encoder
\ No newline at end of file
+```{eval-rst}
+.. automodule:: netforge_rl.nlp.log_encoder
+ :members:
+ :no-undoc-members:
+```
diff --git a/docs/api/python.md b/docs/api/python.md
new file mode 100644
index 0000000..c809214
--- /dev/null
+++ b/docs/api/python.md
@@ -0,0 +1,10 @@
+# Python API
+
+```{eval-rst}
+.. automodule:: netforge_rl
+ :members:
+ :no-undoc-members:
+ :imported-members:
+```
+
+{doc}`environment` · {doc}`siem` · {doc}`nlp` · {doc}`sim2real`
diff --git a/docs/api/siem.md b/docs/api/siem.md
index 07b43d9..3cf066d 100644
--- a/docs/api/siem.md
+++ b/docs/api/siem.md
@@ -1,3 +1,7 @@
# SIEM API
-::: netforge_rl.siem.siem_logger
\ No newline at end of file
+```{eval-rst}
+.. automodule:: netforge_rl.siem.siem_logger
+ :members:
+ :no-undoc-members:
+```
diff --git a/docs/api/sim2real.md b/docs/api/sim2real.md
index 8bd0588..05a7b83 100644
--- a/docs/api/sim2real.md
+++ b/docs/api/sim2real.md
@@ -1,3 +1,7 @@
# Sim2Real API
-::: netforge_rl.docker_bridge.bridge
\ No newline at end of file
+```{eval-rst}
+.. automodule:: netforge_rl.docker_bridge.bridge
+ :members:
+ :no-undoc-members:
+```
diff --git a/docs/architecture/dynamic_topology.md b/docs/architecture/dynamic_topology.md
new file mode 100644
index 0000000..e1958aa
--- /dev/null
+++ b/docs/architecture/dynamic_topology.md
@@ -0,0 +1,17 @@
+# Dynamic topologies
+
+`TopologyEventEngine` mutates the graph mid-episode: churn (online/offline),
+migration (subnet/IP), arrival (BYOD from padding slots).
+
+Events: `host_offline`, `host_online`, `host_migrate`, `host_arrive`.
+
+```python
+cfg = {
+ 'topology_churn_rate': 0.02,
+ 'topology_migration_rate': 0.01,
+ 'topology_arrival_rate': 0.005,
+}
+env = NetForgeRLEnv(cfg)
+```
+
+Rates also scale with {doc}`../training/curriculum`.
diff --git a/docs/architecture/multimodal_obs.md b/docs/architecture/multimodal_obs.md
new file mode 100644
index 0000000..563d5d3
--- /dev/null
+++ b/docs/architecture/multimodal_obs.md
@@ -0,0 +1,28 @@
+# Multi-modal obs
+
+```python
+env = NetForgeRLEnv({'scenario_type': 'ransomware', 'pcap_obs': True})
+```
+
+| Key | Shape | |
+|---|---|---|
+| `pcap` | (32, 20) | packet snapshot this tick |
+| `node_features` | (100, 8) | per-host GNN matrix |
+| `adj_matrix` | (10000,) | 100×100 routing (always) |
+
+PCAP dims 0–19: src/dst idx, protocol, port, payload, SYN/RST/ACK/PSH, lateral,
+C2, recon, exfil, exploit, dst_sensitive, src_privilege, dst_compromised,
+tick_norm, encrypted, severity. All in `[0, 1]`.
+
+Node dims: privilege, online, compromised, decoy, DC, subnet_type, cvss/10, EDR.
+
+Packets come from state (C2 beacons, lateral, then benign fill), not noise.
+
+```python
+import torch
+from torch_geometric.data import Data
+
+x = torch.tensor(obs['node_features'])
+adj = torch.tensor(obs['adj_matrix'].reshape(100, 100))
+data = Data(x=x, edge_index=adj.nonzero().t().contiguous())
+```
diff --git a/docs/architecture/nlp_siem.md b/docs/architecture/nlp_siem.md
index 417cdd7..9526c7b 100644
--- a/docs/architecture/nlp_siem.md
+++ b/docs/architecture/nlp_siem.md
@@ -1,10 +1,5 @@
-# NLP-SIEM Pipeline
+# NLP-SIEM
-The NLP-SIEM Pipeline translates raw network telemetry strings into vectorized observation spaces or natural language prompts specifically for Blue agents.
-
-## Mechanics
-
-- **Event Generation**: The `ConflictResolutionEngine` processes successful and failed state modifications, emitting raw text signatures matching Sysmon/Windows Event Logs (e.g., Event ID 4624 for `Logon`).
-- **Log Buffer**: These raw strings are appended to a rolling global event queue (`siem_log_buffer`), which simulates the real-time logging pipeline.
-- **Vectorization (RL Agents)**: For standard neural architectures, the most recent $N$ logs in the buffer are mapped through a static TF-IDF encoder or dense embedding layer, transforming the strings into a fixed-size `BlueAgentObservationVector`.
-- **Text Translation (LLM Agents)**: For foundation models, the `state_to_text` function directly pulls raw text from the buffer and injects it into a structured natural language prompt, bypassing the vectorization phase entirely.
\ No newline at end of file
+Conflict resolution emits Sysmon-like strings into `siem_log_buffer`.
+RL agents encode the last N logs (TF-IDF or dense). LLM agents get
+`state_to_text` from the same buffer.
diff --git a/docs/architecture/ot_physics.md b/docs/architecture/ot_physics.md
new file mode 100644
index 0000000..2ee8b1c
--- /dev/null
+++ b/docs/architecture/ot_physics.md
@@ -0,0 +1,17 @@
+# OT/SCADA physics
+
+First-order lag on `PLC_Firmware` hosts (`netforge_rl/scenarios/ot_physics.py`).
+Ticked after actions, before termination.
+
+`x[t+1] = x[t] + (x_sp − x[t]) / τ + N(0, σ)`
+
+| | Nominal | τ | σ | Alarm | Critical |
+|---|---|---|---|---|---|
+| temperature °C | 40–60 | 20 | 0.1 | > 80 | > 120 |
+| pressure bar | 90–110 | 10 | 0.2 | >130 or <70 | >180 or <30 |
+| flow L/min | 40–60 | 5 | 0.3 | >90 or <20 | >150 or <5 |
+
+Alarm → SIEM severity 7. Critical → `kinetic_destruction`, severity 10, ends `ot_stuxnet`.
+
+`OverloadPLC` (id 20, Root on PLC) jumps setpoints. ~10–15 ticks to destruction.
+`IsolateHost` stops physics on that PLC.
diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md
index f5dcc7c..1c02b52 100644
--- a/docs/architecture/overview.md
+++ b/docs/architecture/overview.md
@@ -1,7 +1,9 @@
-# Execution Architecture
+# Tick loop
-## 1. Simulation Loop
-The simulation loop is fully parallelized via the PettingZoo API (`parallel_env.step()`) and JAX vectorization (`jax.vmap`).
+Each step: agents pick `[action_type, host_index]`, actions run, conflicts
+resolve, SIEM logs fire, Blue obs update from the buffer.
+
+PettingZoo `parallel_env.step()`. JAX `vmap` is a throughput surrogate.
```mermaid
sequenceDiagram
@@ -16,48 +18,21 @@ sequenceDiagram
Environment->>Environment: action.execute(GlobalNetworkState)
Environment->>ConflictResolutionEngine: resolve(effects_dict)
ConflictResolutionEngine->>GlobalNetworkState: Apply ActionEffect.state_deltas
- Environment-->>Agents: Return new observations, rewards, terminations
+ Environment-->>Agents: obs, rewards, dones
```
-## 2. Telemetry Pipeline
-Blue agent observations are generated exclusively through the simulated Security Information and Event Management (SIEM) pipeline.
+Blue obs come from SIEM only.
```mermaid
flowchart TD
- subgraph Action Execution
- R1[Red Actions]
- end
-
- subgraph Simulation Core
- GNS[(GlobalNetworkState)]
- CRE{Conflict Resolution Engine}
- SL[SIEMLogger]
- end
-
- subgraph Blue Observability
- LB[(siem_log_buffer)]
- NLP[NLP Log Encoder]
- BO[Blue Agent Observation Vector]
- end
-
- R1 -->|ActionEffect| CRE
- CRE -->|Valid Effects| GNS
- CRE -->|Action Metrics| SL
-
- SL -.->|Stochastic Noise Injection| SL
- SL -->|Generates Sysmon/Windows Logs| LB
-
- LB -->|N Most Recent Logs| NLP
- NLP -->|TF-IDF / Dense Embeddings| BO
+ R1[Red Actions] -->|ActionEffect| CRE{Conflict Resolution}
+ CRE -->|Valid Effects| GNS[(GlobalNetworkState)]
+ CRE -->|Metrics| SL[SIEMLogger]
+ SL -.->|Noise| SL
+ SL -->|Sysmon-like logs| LB[(siem_log_buffer)]
+ LB --> NLP[Log Encoder]
+ NLP --> BO[Blue observation]
```
-## 3. Component Details
-
-### `BaseAction` and `ActionEffect`
-All agent capabilities inherit from `BaseAction`. `execute()` returns an `ActionEffect` containing `state_deltas` determining specific state tensor modifications.
-
-### `ConflictResolutionEngine`
-Resolves temporal collisions occurring in the same parallel tick. Handled deterministically within the `EnvState` interpreter.
-
-### `SIEMLogger` and `LogEncoder`
-`SIEMLogger` translates state deltas into standardized string logs matching Windows/Sysmon syntax, injecting stochastic benign noise. When `log_latency > 0` it holds each log for that many ticks before it becomes visible, modelling a lagging SOC feed. `LogEncoder` vectorizes the buffer into dense observations.
+`BaseAction.execute()` returns `ActionEffect.state_deltas`.
+Resolution is deterministic per tick. `log_latency` can delay logs.
diff --git a/docs/architecture/sim2real.md b/docs/architecture/sim2real.md
index b7d2075..5a6c8d1 100644
--- a/docs/architecture/sim2real.md
+++ b/docs/architecture/sim2real.md
@@ -1,9 +1,5 @@
# Sim2Real Bridge
-The Sim2Real Bridge interfaces internal RL action tensors into executable network commands for live environments, such as Docker containers (e.g., Vulhub).
-
-## Mechanics
-
-- **Translation Mapping**: Maps discrete RL action integers (e.g., `ActionType.ExploitEternalBlue`) to standardized payload sequences (e.g., Metasploit RPC commands).
-- **Execution Engine**: Interacts with the live target via a strictly typed execution wrapper. It captures the physical stdout/stderr output resulting from the payload injection.
-- **State Feedback**: Evaluates the output against expected success signatures. If validation passes, the corresponding simulated `GlobalNetworkState` tensor is updated to reflect the physical reality (e.g., escalating privilege to `Root`).
\ No newline at end of file
+Maps discrete actions (e.g. `ExploitEternalBlue`) to live commands (Docker /
+Vulhub). Stdout/stderr is matched against success signatures; on hit, the
+sim `GlobalNetworkState` is updated (e.g. privilege → Root).
diff --git a/docs/architecture/zero_trust.md b/docs/architecture/zero_trust.md
index b2ede4e..bffc6d1 100644
--- a/docs/architecture/zero_trust.md
+++ b/docs/architecture/zero_trust.md
@@ -1,9 +1,5 @@
# Zero-Trust Identity
-The Zero-Trust Identity module governs authorization tokens and their alignment with `GlobalNetworkState` access vectors.
-
-## Mechanics
-
-- **Token Arrays**: Each active node possesses a continuous token array representing authorized privilege contexts.
-- **Verification**: Red agents executing identity-based lateral movement (e.g., `PassTheHash`, `PassTheTicket`) are validated strictly against this array rather than service vulnerability flags.
-- **Invalidation**: Blue agents execute actions like `RotateKerberos` to forcefully clear token arrays globally. If a Red agent attempts to utilize an invalidated token for authentication, the action `success` flag returns `False` and a failed authentication event is passed to the SIEM buffer.
\ No newline at end of file
+Each node holds a token array. Identity lateral movement (`PassTheHash`,
+`PassTheTicket`) checks tokens, not CVEs. Blue `RotateKerberos` clears them;
+stale tokens fail and log to SIEM.
diff --git a/docs/arena.md b/docs/arena.md
new file mode 100644
index 0000000..dc7f257
--- /dev/null
+++ b/docs/arena.md
@@ -0,0 +1,25 @@
+# Evaluation splits
+
+Train, check, and hold out topologies with the same metrics: mission, SLA,
+false positives, exfil, kinetic failures.
+
+```{image} _static/figures/arena.svg
+:alt: train / dev / hidden splits
+:class: fe-fig
+```
+
+```bash
+netforge arena --json
+netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 1 --max-ticks 40
+netforge benchmark --table --seeds 0 1 --max-ticks 40
+```
+
+| Split | Public | Use |
+|---|---|---|
+| `train` | yes | learning |
+| `dev` | yes | pick a checkpoint |
+| `hidden` | organizer | official eval (repo ships a public proxy) |
+
+Blue is scored on keeping the network up without wrecking it (isolation / FPs).
+Red eval can use a **population** (`random`, heuristic, kill-chain), not one
+script. {doc}`submissions` · {doc}`questions`
diff --git a/docs/backends.md b/docs/backends.md
new file mode 100644
index 0000000..19b9d29
--- /dev/null
+++ b/docs/backends.md
@@ -0,0 +1,14 @@
+# Python vs JAX
+
+Python is the gym. JAX is a throughput surrogate — not a SIEM replica.
+
+| | Python | JAX |
+|---|---|---|
+| Role | Arena, SIEM, probes | massive rollouts |
+| SIEM / NLP | yes | scalar alert |
+| Durative / event time | yes | reduced core |
+| Belief graphs | yes | host arrays (not yet) |
+| Parity | golden trajectory | NumPy reference kernel |
+
+`tests/parity/test_jax_reference_equivalence.py`, `test_golden_trajectory.py`.
+SIEM / deception / probes → Python. 10⁵ steps/s → JAX, state the subset.
diff --git a/docs/baselines.md b/docs/baselines.md
new file mode 100644
index 0000000..e84d0b7
--- /dev/null
+++ b/docs/baselines.md
@@ -0,0 +1,12 @@
+# Baselines
+
+In-tree: `random`, `heuristic-blue`, `heuristic-red`, `killchain-red`,
+JAX IPPO (`netforge_rl.baselines.jax_ppo`).
+
+Your MAPPO / GNN / QMIX / LLM: train outside, evaluate here.
+
+```bash
+netforge benchmark --table --split dev --seeds 0 1 --max-ticks 40
+```
+
+Configs: `benchmarks/configs/`. Same split, seeds, and `max_ticks` when you compare runs.
diff --git a/docs/benchmarks/baselines.md b/docs/benchmarks/baselines.md
new file mode 100644
index 0000000..94e0f27
--- /dev/null
+++ b/docs/benchmarks/baselines.md
@@ -0,0 +1,23 @@
+# Baselines
+
+`netforge_rl.baselines`.
+
+| Policy | |
+|---|---|
+| `RandomPolicy` | uniform `MultiDiscrete([32, 100])` |
+| `HeuristicBluePolicy` | isolate first compromised host, else analyse |
+| `HeuristicRedPolicy` | exploit without recon — usually 0 compromises |
+| `KillChainRedPolicy` | recon → exploit → pivot; ~2–3 hosts/ep, SLA drops |
+| `jax_ppo` | on-device IPPO (`baselines/jax_ppo.py`) |
+
+`ExploitRemoteService` needs prior `DiscoverNetworkServices`. Use kill-chain as
+the Red reference, not heuristic-red.
+
+```bash
+python -m benchmarks.build_leaderboard --episodes 5 --max-steps 150
+python -m benchmarks.train_curve --name blue_ransomware --iters 40
+```
+
+Committed IPPO on ransomware: mean reward **0.06 → 0.71** (40 iters, CPU).
+Load with `jax_ppo.load_params`. RLlib: `benchmarks/rllib_rmappo.py`.
+{doc}`run` · {doc}`self_play`
diff --git a/docs/benchmarks/overview.md b/docs/benchmarks/overview.md
new file mode 100644
index 0000000..e88104b
--- /dev/null
+++ b/docs/benchmarks/overview.md
@@ -0,0 +1,9 @@
+# Benchmarks
+
+`benchmarks/` scripts, `benchmarks/results/` artifacts.
+
+- [Baselines](baselines.md)
+- [Self-play / Elo](self_play.md)
+- [Run](run.md) — throughput, eval, sweeps, env spec
+
+JAX CPU, batch 4096: **270,795** env-steps/s (1,083,181 agent-steps/s). Reproduce in [Run](run.md).
diff --git a/docs/benchmarks/run.md b/docs/benchmarks/run.md
new file mode 100644
index 0000000..1b11f92
--- /dev/null
+++ b/docs/benchmarks/run.md
@@ -0,0 +1,46 @@
+# Run
+
+```bash
+python -m benchmarks.throughput --batches 1024 4096 --steps 50 --warmup 2
+```
+
+CPU. `agent-steps/s = env-steps/s × agents`.
+
+| Backend | Batch | env-steps/s | agent-steps/s |
+|---|---|---|---|
+| Python | 1 | ~300 | ~1,200 |
+| JAX | 1024 | 158,540 | 634,160 |
+| JAX | 4096 | 270,795 | 1,083,181 |
+
+## Competition
+
+`benchmarks/competition_eval.py` → `results/leaderboard.json`.
+
+Blue: `SLA×50 − compromised×2 − MTTC×0.1 + blue_reward×0.1`
+Red: `compromised×2 + exfil×0.01 − SLA×10 + red_reward×0.1`
+
+```bash
+python -m benchmarks.competition_eval --name my_agent --team blue --episodes 10
+python -m benchmarks.run_benchmark --name heuristic --team blue --seeds 20
+python -m benchmarks.run_benchmark --name killchain --team red --red killchain --blue heuristic --seeds 20
+python -m benchmarks.run_benchmark --name killchain --team red --red killchain --gap --seeds 20
+python -m benchmarks.build_leaderboard --episodes 5 --max-steps 150
+python -m benchmarks.env_spec --json
+```
+
+| `info` key | |
+|---|---|
+| `compromised_hosts` / `isolated_hosts` / `active_hosts` | padding excluded |
+| `SLA_Uptime_Percentage` | healthy online fraction |
+| `MTTC` / `containment_time` | time to contain |
+| `detection_rate` | compromises that were isolated |
+| `Total_Exfiltrated_Data` | cumulative |
+| `false_positives` / `successful_exploits` / `services_restored` | step outcomes |
+| `deception_hits` / `deception_efficacy` | decoy / honeytoken |
+| `attack_techniques` / `attack_coverage` | ATT&CK ids |
+| `blue_score` / `red_score` / `normalized_reward` | composites |
+| `agent_energy` | remaining budget |
+| `information_asymmetry` | under `DiagnosticsWrapper` |
+| `__curriculum__` | phase / mean_reward / … |
+
+{doc}`baselines`
diff --git a/docs/benchmarks/self_play.md b/docs/benchmarks/self_play.md
new file mode 100644
index 0000000..6f85081
--- /dev/null
+++ b/docs/benchmarks/self_play.md
@@ -0,0 +1,26 @@
+# Self-play / Elo
+
+One ladder: Blue score = mean SLA, Red = `1 − SLA`. `benchmarks/self_play.py`.
+
+```bash
+python -m benchmarks.self_play --seeds 5 --max-ticks 150
+```
+
+```python
+from benchmarks.self_play import population_tournament
+
+result = population_tournament(
+ scenarios=['ransomware', 'apt_espionage', 'cloud_hybrid'],
+ seeds=list(range(5)),
+ max_ticks=150,
+)
+```
+
+Default pools: `random`, `heuristic`, `killchain-red`. Pass `red_pool` / `blue_pool`
+(`name → factory`) to add trained agents.
+
+```
+ 1. heuristic-blue 1041.7
+ 2. random-blue 1022.1
+ 3. killchain-red 980.4
+```
diff --git a/docs/capabilities.md b/docs/capabilities.md
new file mode 100644
index 0000000..609dd71
--- /dev/null
+++ b/docs/capabilities.md
@@ -0,0 +1,9 @@
+# Capability tests
+
+```bash
+netforge test-policy --policy heuristic-blue --out cards/
+```
+
+Scores in `[0, 1]`: Safety, OOD, Memory, FPs, Temporal, Deception, Adaptation,
+Attention. Radar if matplotlib is installed. Full-state graphs stay on
+`DiagnosticsWrapper`.
diff --git a/docs/changelog.md b/docs/changelog.md
index adb0c78..958819f 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,8 +1,40 @@
# Changelog
-All notable changes to the `netforge_rl` project will be documented in this file.
+All notable changes to the `netforge-rl` project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## [4.0.0] — 2026-09-20
+
+Cybersecurity RL gym: PettingZoo / Gymnasium ids, SIEM belief graphs, eval CLI.
+Not on PyPI.
+
+### Added
+- **NetForge Arena 2026**: frozen train/dev/hidden splits, competition metrics
+ (mission, disruption, security, false positives, exfiltration, catastrophic,
+ CVaR / worst-case), OOD tasks, adversarial Red populations, leaderboard helpers.
+- **`netforge` CLI**: `run`, `test-policy`, `benchmark`, `arena`, `replay`, `collect`,
+ `questions`, `evaluate`.
+ Package name `netforge-rl`.
+- **Belief-state graphs**: Blue `info['graph']` is reconstructed from SIEM + inventory;
+ privileged `oracle_graph` is diagnostic/replay only.
+- **Capability probes**: deception resistance and adaptation (mid-episode Red shift),
+ plus agent cards (`Safety`, `OOD Generalization`, `Memory`, `False Positive Rate`, …).
+- **Scenario YAML / community packs**: hospital ransomware, cloud IAM, enterprise APT,
+ finance, IoT, OT, zero-trust. `netforge run my_scenario.yaml`.
+- **Reward-design and constrained RL wrappers**; communication-failure wrapper;
+ league / PSRO payoff helper; event vs fixed `time_mode`.
+- **Offline datasets**: trajectory collector + Minari-compatible npz / HDF5 export.
+- **Named probes** (`netforge questions`) and CAGE-style submissions
+ (`netforge evaluate`) ranked on Arena metrics, not mean reward.
+- **Gymnasium / PettingZoo registration**: `NetForge/Blue-v4`, `netforge/ransomware-v4`.
+- **Interactive HTML replay viewer** (network + SIEM + reward timeline).
+- **Sphinx + Furo** docs (MyST), GitHub Pages from Actions — same kit as FlowEdge.
+
+### Changed
+- README and docs lead with Red vs Blue and SIEM. MAPPO / QMIX / CT-GMARL are
+ evaluated *on* the gym, not vendored into it.
+- Blue shared comm no longer leaks ground-truth privilege.
+
## [3.1.0] — 2026-07-10
### Fixed
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..4c5a684
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,125 @@
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+project = 'NetForge'
+author = 'Igor Jankowski'
+copyright = '2026, Igor Jankowski'
+release = '4.0.0'
+
+extensions = [
+ 'myst_parser',
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.viewcode',
+ 'sphinx.ext.intersphinx',
+ 'sphinxcontrib.mermaid',
+]
+
+myst_enable_extensions = ['colon_fence', 'deflist', 'dollarmath']
+myst_heading_anchors = 2
+myst_fence_as_directive = ['mermaid']
+myst_suppress_warnings = ['myst.header']
+source_suffix = {'.md': 'markdown', '.rst': 'restructuredtext'}
+
+html_theme = 'furo'
+html_title = 'NetForge'
+html_logo = '_static/figures/mark.svg'
+html_static_path = ['_static']
+templates_path = ['_templates']
+html_css_files = ['custom.css']
+html_js_files = ['custom.js']
+html_copy_source = False
+html_show_sphinx = False
+
+html_theme_options = {
+ 'navigation_with_keys': True,
+ 'source_repository': 'https://github.com/reforcemind/NetForge_RL/',
+ 'source_branch': 'main',
+ 'source_directory': 'docs/',
+ 'footer_icons': [
+ {
+ 'name': 'GitHub',
+ 'url': 'https://github.com/reforcemind/NetForge_RL',
+ 'html': """
+
+""",
+ 'class': '',
+ },
+ ],
+ 'light_css_variables': {
+ 'color-brand-primary': '#7b2733',
+ 'color-brand-content': '#7b2733',
+ 'color-background-primary': '#f7efdc',
+ 'color-background-secondary': '#f1e6cc',
+ 'color-background-hover': '#ece0c4',
+ 'color-background-border': '#e2d4b6',
+ 'color-foreground-primary': '#2b2521',
+ 'color-foreground-secondary': '#5c5248',
+ 'color-foreground-muted': '#8a7d6d',
+ 'color-code-background': '#f2e8d0',
+ 'color-code-foreground': '#5e1f2b',
+ 'color-admonition-background': '#f1e6cc',
+ 'color-highlight-on-target': '#fcefd4',
+ 'font-stack': "'Libre Franklin', ui-sans-serif, system-ui, sans-serif",
+ 'font-stack--monospace': "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace",
+ 'font-stack--headings': "'Newsreader', Georgia, 'Times New Roman', serif",
+ 'sidebar-caption-font-size': '0.72rem',
+ },
+ 'dark_css_variables': {
+ 'color-brand-primary': '#dd94a0',
+ 'color-brand-content': '#e3a6b0',
+ 'color-background-primary': '#211b19',
+ 'color-background-secondary': '#2a2320',
+ 'color-background-hover': '#342a25',
+ 'color-background-border': '#3b302b',
+ 'color-foreground-primary': '#ece0cb',
+ 'color-foreground-secondary': '#c9bba6',
+ 'color-foreground-muted': '#9a8b78',
+ 'color-code-background': '#2a2320',
+ 'color-code-foreground': '#e6c9b8',
+ 'color-admonition-background': '#2a2320',
+ 'font-stack': "'Libre Franklin', ui-sans-serif, system-ui, sans-serif",
+ 'font-stack--monospace': "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace",
+ 'font-stack--headings': "'Newsreader', Georgia, 'Times New Roman', serif",
+ },
+}
+
+exclude_patterns = [
+ '_build',
+ 'README.md',
+ 'stylesheets',
+]
+
+autodoc_mock_imports = [
+ 'jax',
+ 'jaxlib',
+ 'ray',
+ 'torch',
+ 'transformers',
+ 'sentence_transformers',
+ 'moviepy',
+ 'docker',
+ 'scapy',
+ 'minari',
+ 'h5py',
+ 'matplotlib',
+ 'stable_baselines3',
+]
+napoleon_google_docstring = True
+autodoc_member_order = 'bysource'
+autodoc_default_options = {
+ 'members': True,
+ 'undoc-members': False,
+ 'show-inheritance': True,
+}
+
+intersphinx_mapping = {
+ 'python': ('https://docs.python.org/3', None),
+ 'numpy': ('https://numpy.org/doc/stable/', None),
+ 'gymnasium': ('https://gymnasium.farama.org/', None),
+}
+
+nitpicky = False
+suppress_warnings = ['myst.header']
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..45fc651
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,16 @@
+# Contribute
+
+[CONTRIBUTING.md](https://github.com/reforcemind/NetForge_RL/blob/main/CONTRIBUTING.md) ·
+[Code of Conduct](https://github.com/reforcemind/NetForge_RL/blob/main/CODE_OF_CONDUCT.md)
+
+Packs, probes, wrappers. Trainers stay out of tree. Blue sees SIEM.
+
+Gate: `scripts/verify_all.sh` / `scripts/verify_local.ps1`.
+CI builds the Sphinx site; `docs.yml` deploys it to GitHub Pages on `main`.
+
+| | |
+|---|---|
+| Probe | {doc}`questions` |
+| YAML pack | {doc}`scenarios` |
+| Verify | {doc}`guides/verification` |
+| Policy | {doc}`submissions` |
diff --git a/docs/cybersec/blue_actions.md b/docs/cybersec/blue_actions.md
index 6208c17..cfa378a 100644
--- a/docs/cybersec/blue_actions.md
+++ b/docs/cybersec/blue_actions.md
@@ -1,53 +1,16 @@
-# Blue Team Actions
-
-### `ConfigureACL`
-- **Module:** `mitigation.py`
-- **Description:** Modifies the firewall state to drop traffic targeting specific ports on a subnet.
-
-### `IsolateHost`
-- **Module:** `mitigation.py`
-- **Description:** Sets target host status to isolated, dropping all inbound and outbound traffic capabilities.
-
-### `Remove`
-- **Module:** `mitigation.py`
-- **Description:** Resets the compromised state of a target host, terminating executing Red policies on that node.
-
-### `RestoreFromBackup`
-- **Module:** `mitigation.py`
-- **Description:** Reverts target host state vector to baseline initial configuration.
-
-### `RestoreHost`
-- **Module:** `mitigation.py`
-- **Description:** Reverts host status from isolated, re-establishing network connectivity.
-
-### `SecurityAwarenessTraining`
-- **Module:** `mitigation.py`
-- **Description:** Decreases the `human_vulnerability_score` scalar for all nodes in a target subnet.
-
-### `DecoyApache` / `DecoySSHD` / `DecoyTomcat`
-- **Module:** `deception.py`
-- **Description:** Instantiates a decoy service node matching specific port signatures (80, 22, 8080) to intercept discovery requests.
-
-### `DeployDecoy`
-- **Module:** `deception.py`
-- **Description:** Instantiates a generic decoy service node.
-
-### `DeployHoneytoken`
-- **Module:** `deception.py`
-- **Description:** Injects a honeytoken state into a host. Subsequent token extraction actions by Red agents trigger an explicit SIEM event with perfect confidence mapping to the executor's ID.
-
-### `Misinform`
-- **Module:** `deception.py`
-- **Description:** Alters telemetry states to return falsified observation arrays to Red agent discovery actions.
-
-### `RotateKerberos`
-- **Module:** `identity.py`
-- **Description:** Invalidates existing Domain Controller token arrays globally.
-
-### `Analyze`
-- **Module:** `analysis.py`
-- **Description:** Queries the complete unmasked state of a specific host node.
-
-### `Monitor`
-- **Module:** `analysis.py`
-- **Description:** Increases the SIEM event generation probability scalar for actions executing on a target subnet.
+# Blue actions
+
+| Action | Module | |
+|---|---|---|
+| `ConfigureACL` | mitigation | drop ports on a subnet |
+| `IsolateHost` | mitigation | cut all traffic |
+| `RestoreHost` | mitigation | un-isolate |
+| `Remove` | mitigation | clear compromise |
+| `RestoreFromBackup` | mitigation | reset to baseline |
+| `SecurityAwarenessTraining` | mitigation | lower `human_vulnerability_score` |
+| `DecoyApache` / `SSHD` / `Tomcat` | deception | sinkhole discovery |
+| `DeployDecoy` / `DeployHoneytoken` | deception | decoy node / token → SIEM on use |
+| `Misinform` | deception | lie on Red discovery |
+| `RotateKerberos` | identity | wipe DC tokens |
+| `Analyze` | analysis | unmasked host (diagnostic) |
+| `Monitor` | analysis | raise SIEM rate on a subnet |
diff --git a/docs/cybersec/overview.md b/docs/cybersec/overview.md
index 156a313..b56b8bc 100644
--- a/docs/cybersec/overview.md
+++ b/docs/cybersec/overview.md
@@ -1,19 +1,11 @@
-# Environment Overview
+# Cyber
-NetForge RL defines a zero-sum, asymmetric, multi-agent environment with partial observability.
+Asymmetric POMDP on a simulated network.
-## Red Policy Characteristics
+**Red** starts with no visibility: discover, exploit, escalate, exfil, or hit a
+PLC. Success needs the right CVE / token / routing, not a dice roll.
-- **Objective**: Maximize scalar reward by successfully executing impact actions (e.g., data exfiltration, service disruption) on designated target nodes.
-- **Initial State**: Zero network visibility. `GlobalNetworkState` is fully masked.
-- **Mechanics**: Agents must sequentially execute discovery, exploit, and privilege escalation actions to modify node states. Success probabilities are calculated deterministically against the target's vulnerability array and current access level.
+**Blue** sees SIEM, not the true compromise map. Isolate, ACL, restore, decoys
+(`169.254.x.x`). Quiet Red may never show up in logs.
-## Blue Policy Characteristics
-
-- **Objective**: Maximize scalar reward by maintaining service uptime and minimizing compromised host counts across the episode.
-- **Initial State**: Full visibility of the uncompromised network baseline.
-- **Mechanics**: Agents observe the environment through SIEM event logs. Actions consist of isolation, firewall ACL modification, host restoration, and decoy deployment (`169.254.x.x`). Detection capabilities depend on Red action signatures; stealthy actions may bypass event generation.
-
-## Kinetic Impacts
-
-The environment supports Cyber-Physical System (CPS) nodes. Specific Red actions (`OverloadPLC`) can transition CPS nodes into a `kinetic_destruction` state. This represents a terminal fail-state for Blue agents, immediately ending the episode.
\ No newline at end of file
+**Kinetic** — `OverloadPLC` can set `kinetic_destruction` (Blue fail-state).
diff --git a/docs/cybersec/red_actions.md b/docs/cybersec/red_actions.md
index 10d9c0c..7d2374d 100644
--- a/docs/cybersec/red_actions.md
+++ b/docs/cybersec/red_actions.md
@@ -1,54 +1,16 @@
-# Red Team Actions
-
-### `DumpLSASS`
-- **Module:** `post_exploitation.py`
-- **Description:** Extracts token arrays from memory. Requires `Root` privilege state on the target node.
-
-### `ExploitBlueKeep` / `ExploitEternalBlue` / `ExploitHTTP_RFI`
-- **Module:** `exploits.py`
-- **Description:** Target specific service vulnerabilities (RDP, SMB, HTTP). Validates against host vulnerability vectors to modify access state.
-
-### `ExploitRemoteService`
-- **Module:** `exploits.py`
-- **Description:** Generic exploit attempting to modify access state from network visibility to local `User` privilege.
-- **Args:** `agent_id` (str), `target_ip` (str), `port` (int).
-
-### `PassTheTicket` / `PassTheHash`
-- **Module:** `post_exploitation.py` / `privilege_escalation.py`
-- **Description:** Validates previously extracted token arrays to modify access state on target nodes without requiring vulnerable services.
-
-### `PrivilegeEscalate`
-- **Module:** `privilege_escalation.py`
-- **Description:** Modifies agent privilege state from `User` to `Root`/`SYSTEM`.
-- **Args:** `agent_id` (str), `target_ip` (str).
-
-### `DiscoverNetworkServices`
-- **Module:** `reconnaissance.py`
-- **Description:** Queries the active service array of a target node. Updates agent visibility mask.
-- **Args:** `agent_id` (str), `target_ip` (str).
-
-### `DiscoverRemoteSystems` / `NetworkScan`
-- **Module:** `reconnaissance.py`
-- **Description:** Queries the active IP allocation array of a target subnet. Updates agent visibility mask.
-- **Args:** `agent_id` (str), `target_subnet` (str).
-
-### `ShareIntelligence`
-- **Module:** `coordination.py`
-- **Description:** Performs a union operation on the visibility masks of two allied agents.
-
-### `ExfiltrateData` / `Impact` / `KillProcess`
-- **Module:** `impact.py`
-- **Description:** Executes terminal objectives. Modifies the data state or process array on a compromised node to calculate scenario rewards.
-
-### `JuicyPotato` / `V4L2KernelExploit`
-- **Module:** `privilege_escalation.py`
-- **Description:** Platform-specific privilege escalation actions targeting specific OS vectors (Windows DCOM / Linux Kernel).
-
-### `OverloadPLC`
-- **Module:** `kinetic.py`
-- **Description:** Modifies the physical state tensor of an OT node to `kinetic_destruction`.
-
-### `SpearPhishing`
-- **Module:** `social_engineering.py`
-- **Description:** Evaluates success strictly against the `human_vulnerability_score` scalar of a target endpoint. Bypasses routing graph constraints.
-- **Args:** `agent_id` (str), `target_ip` (str).
+# Red actions
+
+| Action | Module | |
+|---|---|---|
+| `DiscoverRemoteSystems` / `NetworkScan` | reconnaissance | subnet IPs → visibility |
+| `DiscoverNetworkServices` | reconnaissance | services on a host |
+| `ExploitBlueKeep` / `EternalBlue` / `HTTP_RFI` | exploits | RDP / SMB / HTTP vs CVE flags |
+| `ExploitRemoteService` | exploits | network vis → User |
+| `PrivilegeEscalate` | privilege_escalation | User → Root |
+| `JuicyPotato` / `V4L2KernelExploit` | privilege_escalation | Windows DCOM / Linux kernel |
+| `DumpLSASS` | post_exploitation | tokens; needs Root |
+| `PassTheHash` / `PassTheTicket` | post_exploitation | tokens, not CVEs |
+| `ShareIntelligence` | coordination | union of visibility masks |
+| `ExfiltrateData` / `Impact` / `KillProcess` | impact | terminal objectives |
+| `OverloadPLC` | kinetic | PLC → `kinetic_destruction` |
+| `SpearPhishing` | social_engineering | vs `human_vulnerability_score`; no routing |
diff --git a/docs/cybersec/threat_model.md b/docs/cybersec/threat_model.md
index 221ef7a..52f65f7 100644
--- a/docs/cybersec/threat_model.md
+++ b/docs/cybersec/threat_model.md
@@ -1,29 +1,15 @@
-# Action Taxonomy Mapping
+# Threat model
-NetForge RL actions are mapped to specific execution mechanics rather than abstract probabilities.
+Actions are mechanics, not dice rolls.
-## Implemented Vectors
+| | |
+|---|---|
+| Spearphish | `human_vulnerability_score`; no routing |
+| BlueKeep / EternalBlue / HTTP RFI | RDP / SMB / HTTP CVE flags |
+| JuicyPotato / V4L2 | DCOM / Linux kernel flags |
+| Pass-the-hash | DC tokens, not CVEs |
+| Honeytoken | unmaskable SIEM on ingest |
+| Decoys | Apache / Tomcat / SSH sinkholes |
-### Initial Access & Execution
-- **Spearphishing**: Evaluated against the `human_vulnerability_score` scalar of the target end-user node. Bypasses routing constraints.
-- **Remote Exploits**:
- - `ExploitBlueKeep` (CVE-2019-0708): Modifies access state on RDP.
- - `ExploitEternalBlue` (MS17-010): Modifies access state on SMB.
- - `ExploitHTTP_RFI`: Modifies access state on HTTP/HTTPS.
-
-### Privilege Escalation
-Agents transitioning from `User` to `Root`/`SYSTEM` state requirements:
-- **JuicyPotato**: Exploits DCOM (`SeImpersonatePrivilege`) on Windows nodes.
-- **V4L2 Kernel Exploit**: Targets specific kernel array flags on Linux nodes.
-
-### Lateral Movement
-- **Pass the Hash**: Validates token exchanges against Domain Controller state vectors, allowing state modification without exploiting service CVEs.
-
-### Defense Evasion & Deception
-Blue agent countermeasures:
-- **DeployHoneytoken**: Injects a monitored token state into the target host. Ingestion by a Red agent generates an unmaskable SIEM event alert.
-- **Decoys**: Instantiates isolated nodes mimicking Apache/Tomcat/SSH services to intercept and sinkhole Red discovery actions.
-
-## Visibility Masks
-- **Red Policy**: Local knowledge graph updated only via successful discovery actions (`NetworkScan`, `DiscoverRemoteSystems`).
-- **Blue Policy**: Graph updated only when an executed action triggers a SIEM log event according to its predefined signature.
\ No newline at end of file
+Red visibility updates only on successful discovery. Blue visibility updates
+only when an action emits a SIEM signature.
diff --git a/docs/datasets.md b/docs/datasets.md
new file mode 100644
index 0000000..a89b333
--- /dev/null
+++ b/docs/datasets.md
@@ -0,0 +1,13 @@
+# Offline RL datasets
+
+Git extra `offline`. Hugging Face dumps are not published yet.
+
+```bash
+pip install 'netforge-rl[offline]'
+netforge collect --policy heuristic-blue --red killchain-red \
+ --episodes 8 --quality mixed --out netforge_offline.npz
+netforge collect --hdf5 --out netforge_offline.hdf5
+```
+
+`export_npz` / `export_hdf5` are Minari-shaped without importing `minari`.
+Mix random / heuristic / RL / self-play / LLM; tag quality before fine-tune.
diff --git a/docs/diagnostics/overview.md b/docs/diagnostics/overview.md
new file mode 100644
index 0000000..18340c3
--- /dev/null
+++ b/docs/diagnostics/overview.md
@@ -0,0 +1,4 @@
+# Diagnostics
+
+{doc}`/capabilities`. `DiagnosticsWrapper` adds `oracle_obs`,
+`information_asymmetry`, `oracle_graph`.
diff --git a/docs/environment/difficulty.md b/docs/environment/difficulty.md
new file mode 100644
index 0000000..bf97cc4
--- /dev/null
+++ b/docs/environment/difficulty.md
@@ -0,0 +1,7 @@
+# Difficulty and splits
+
+Presets: `easy` / `medium` / `hard` + frozen `EVAL_SEEDS`.
+
+- **train** — public seeds, `evaluation_mode=False`
+- **dev** — `EVAL_SEEDS`, `evaluation_mode=True`
+- **hidden** — organizer seed block
diff --git a/docs/environment/graph_observations.md b/docs/environment/graph_observations.md
new file mode 100644
index 0000000..638bc19
--- /dev/null
+++ b/docs/environment/graph_observations.md
@@ -0,0 +1,15 @@
+# Belief-state graphs
+
+Blue’s graph is reconstructed from inventory + SIEM. Red’s is fog of war.
+The privileged map is for diagnostics, not for training Blue.
+
+| Mode | Who | |
+|---|---|---|
+| `belief` | Blue (default) | assets + alerts |
+| `fog` | Red | discovered hosts |
+| `oracle` | diagnostics | true compromise |
+
+`GraphObservationWrapper` sets `info["graph"]`.
+`oracle=True` also sets `info["oracle_graph"]`.
+GNN policies read `node_features`, `edge_index`, `edge_attr`, `node_mask`.
+The PettingZoo `obs` vector is unchanged.
diff --git a/docs/environment/reproducibility.md b/docs/environment/reproducibility.md
new file mode 100644
index 0000000..22cfee6
--- /dev/null
+++ b/docs/environment/reproducibility.md
@@ -0,0 +1,5 @@
+# Reproducibility
+
+Fixed seed replays obs, SIEM embeddings, infos, rewards.
+`tests/parity/test_seeded_observation_replay.py` + golden trajectory.
+Pin package + Arena version + config.
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..117437e
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,37 @@
+# Getting Started
+
+Python 3.12+. Install from git (not PyPI).
+
+```bash
+python -m pip install 'netforge-rl @ git+https://github.com/reforcemind/NetForge_RL'
+netforge run hospital_ransomware --replay replay.html
+```
+
+That spins Red vs Blue on a hospital ransomware pack and writes an HTML replay
+of hosts, SIEM, and rewards.
+
+```bash
+git clone https://github.com/reforcemind/NetForge_RL.git
+cd NetForge_RL
+pip install -e ".[dev]"
+```
+
+```python
+import netforge_rl
+from pettingzoo import make
+from netforge_rl.environment.graph_wrapper import GraphObservationWrapper
+
+env = GraphObservationWrapper(make('parallel', 'netforge/ransomware-v4', max_ticks=80))
+obs, infos = env.reset(seed=0)
+```
+
+`infos["blue_dmz"]["graph"]` is what Blue believes from SIEM.
+One Blue vs a scripted attacker: `gym.make("NetForge/Blue-v4")`.
+
+| | |
+|---|---|
+| Env ids | [Python](python-quickstart) |
+| Red / Blue / SIEM | [Cyber](cybersec/overview) |
+| Packs | [Scenarios](scenarios) |
+| Your policy | [Submissions](submissions) |
+| Gate | [Verification](guides/verification) |
diff --git a/docs/guides/verification.md b/docs/guides/verification.md
new file mode 100644
index 0000000..3e39392
--- /dev/null
+++ b/docs/guides/verification.md
@@ -0,0 +1,18 @@
+# Verification
+
+```bash
+./scripts/verify_all.sh # Linux / Git Bash
+./scripts/verify_local.ps1 # Windows
+```
+
+```bash
+pip install -e ".[dev,docs]"
+ruff check . && ruff format --check .
+pytest tests/ -m fast
+sphinx-build -b html -W --keep-going docs docs/_build/html
+```
+
+CI: ruff + pytest + Sphinx HTML (`ci.yml`). Pages: `docs.yml` deploys to GitHub Pages on `main`.
+Do not vendor MAPPO/QMIX/CT-GMARL.
+
+{doc}`/getting-started` · {doc}`/python-quickstart` · {doc}`/contributing`
diff --git a/docs/index.md b/docs/index.md
index 32bcac6..dd69a2c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,112 +1,217 @@
-
-
-
-
-
-
+# NetForge
+
+
Cybersecurity gym for reinforcement learning. Red compromises hosts. Blue contains them from SIEM, not from an oracle. Train with PettingZoo or Gymnasium.
+
+```{raw} html
+
+ Red vs Blue
+ SIEM telemetry
+ PettingZoo
+ Gymnasium
+
+
+
+
+ 1 Red · 3 Blue
+ DMZ, internal, restricted
+
+
+ SIEM only
+ Blue never sees the true map
+
+
+ durative actions
+ exploits and isolates take ticks
+
-# NetForge RL
-
-**A fast, reproducible multi-agent reinforcement learning environment for autonomous
-cyber-defense.** Red agents attack a procedurally generated enterprise/OT network
-(exploit, pivot, escalate, ransomware, kinetic sabotage); Blue agents defend it (monitor,
-analyze, isolate, deceive, restore). Built on the PettingZoo parallel API with a JAX
-vectorized backend for high-throughput training.
-
----
-
-## Why NetForge
-
-- **Standardized API** — PettingZoo `ParallelEnv`, Gymnasium spaces, conformance-tested.
-- **Partial observability that means something** — Blue sees only a filtered, optionally
- *delayed* SIEM feed; Red operates under fog of war and must recon before it can exploit.
-- **Realistic telemetry** — actions emit real Windows/Sysmon event XML (4624, 4688, Sysmon
- 1/3/10/22, …) which an NLP encoder turns into observations. Plug an LLM in as a SOC analyst.
-- **MITRE ATT&CK aligned** — exploits map to real CVEs (MS17-010, CVE-2019-0708, Log4Shell).
-- **Reproducible to the bit** — a fixed seed replays observations, SIEM embeddings, infos and
- rewards identically (test-guaranteed). See [Reproducibility](environment/reproducibility.md).
-- **Tunable difficulty** — named `easy`/`medium`/`hard` presets and a frozen held-out
- evaluation split. See [Difficulty & Splits](environment/difficulty.md).
-- **Diagnostic probes + capability cards** — a 6-capability suite (memory, attention, temporal,
- precision, safety, generalization) that isolates *what* a policy can and cannot do, summarised
- as a per-policy radar card. See [Diagnostics](diagnostics/overview.md).
-- **Graph-native observations** — hosts as nodes, reachability as edges, fog-of-war aware,
- one line to PyTorch Geometric / jraph. See [Graph Observations](environment/graph_observations.md).
-- **Self-play & Elo** — a population tournament that rates red and blue on one ladder. See
- [Self-Play](benchmarks/self_play.md).
-- **Deception as a mechanic** — decoys and honeytokens with a `deception_efficacy` metric
- measuring how much of Red's effort was wasted on traps.
-- **SOC export** — replay an episode as OCSF-style JSON for real SIEM tooling. See
- [SOC Export](interop/soc_export.md).
-- **Trained baselines, not just scripted** — a JIT-fused JAX IPPO trainer whose whole rollout
- runs on-device; a committed `ransomware` run learns mean reward 0.06 → 0.71. Checkpoints
- reload into self-play and capability cards. See [Baselines](benchmarks/baselines.md).
-- **Standard single-agent API** — `NetForgeSingleAgentEnv` is a `gymnasium.Env` (passes
- `check_env`) for Stable-Baselines3 / CleanRL, with the action mask in `info`.
-- **MITRE ATT&CK coverage** — episodes report which ATT&CK techniques Red exercised and the
- fraction of the taxonomy covered (`attack_coverage`).
-- **Fast** — JAX backend measured at **270,795 env-steps/s (1,083,181 agent-steps/s)** at
- batch 4096 on CPU, with an optional in-kernel numeric SIEM signal. See
- [Benchmarks](benchmarks/overview.md).
-- **JAX backend matches real action timing** — the vectorized backend enqueues and resolves
- actions against the same per-agent duration table as the Python engine: an agent can't act
- again until its pending action resolves, and isolating a host cancels an in-flight exploit
- against it. Verified against a NumPy reference with zero mismatches across all five
- scenarios under randomized rollouts.
-
-## Quick Start
+
+
+ 01
+ Start
+ Install, run a ransomware pack.
+
+
+ 02
+ Python
+ gym.make / pettingzoo.make
+
+
+ 03
+ Cyber
+ Red, Blue, SIEM, OT
+
+
+ 04
+ Scenarios
+ Hospital, cloud, plant, IoT
+
+
+ 05
+ Architecture
+ Tick loop, logs, graphs
+
+
+ 06
+ API
+ Env, SIEM, NLP
+
+
+```
+
+## News
+
+**September 2026**
+
+- Gym ids: PettingZoo `netforge/ransomware-v4`, Gymnasium `NetForge/Blue-v4`.
+- `netforge` CLI: `run`, `evaluate`, `questions`, HTML replay.
+- Arena train / dev / hidden. Typed `EnvConfig`. {doc}`changelog`
+
+## The problem
+
+A defender does not get the true compromise map. The SOC sees Sysmon-like logs
+that can be late, dropped, or noisy. Red starts blind. Exploits and isolates
+take ticks. A gym that hands Blue an oracle, or scores mean reward against one
+scripted Red, is training the wrong job.
+
+## What NetForge does
+
+Red starts blind, discovers hosts, exploits CVEs, escalates, exfiltrates or
+hits a PLC. Blue isolates, restores, drops decoys, and reads Sysmon-like logs
+that can be late or noisy. Padding hosts (`169.254.x.x`) are decoys.
+
+```{image} _static/figures/purpose.svg
+:alt: Red, Blue, and the network
+:class: fe-fig
+```
+
+```{image} _static/figures/workflow.svg
+:alt: reset, observe SIEM, act, next tick
+:class: fe-fig
+```
+
+```{image} _static/figures/why-this.svg
+:alt: God-mode vs SIEM, instant logs vs delay, isolate-all vs SLA
+:class: fe-fig
+```
+
+## Why this, not the usual stack
+
+Blue never sees the true map. Logs can lag. Isolate-everything wrecks SLA.
+Eval uses a Red population. Trainers stay in your repo.
+
+## Install
```bash
-pip install 'netforge_rl[jax] @ git+https://github.com/reforcemind/NetForge_RL'
+python -m pip install 'netforge-rl @ git+https://github.com/reforcemind/NetForge_RL'
+netforge run hospital_ransomware --replay replay.html
```
```python
-from netforge_rl.environment import make_env
+import netforge_rl
+import gymnasium as gym
+from pettingzoo import make
-env = make_env('medium', scenario_type='ransomware', seed=0)
+env = make('parallel', 'netforge/ransomware-v4', max_ticks=80)
obs, infos = env.reset(seed=0)
-while env.agents:
- actions = {a: env.action_space(a).sample() for a in env.agents}
- obs, rewards, term, trunc, infos = env.step(actions)
- if all(term.values()) or all(trunc.values()):
- break
+blue = gym.make('NetForge/Blue-v4', max_ticks=80)
+```
+
+Not on PyPI. Python 3.12+. Trainers stay in your repo.
+
+MIT, with CybORG / DSTG notices in the LICENSE file.
+
+```{toctree}
+:hidden:
+:maxdepth: 1
+:caption: Start
+Overview
+getting-started
+python-quickstart
+arena
+questions
+submissions
+capabilities
+```
+
+```{toctree}
+:hidden:
+:maxdepth: 1
+:caption: Guides
+scenarios
+baselines
+datasets
+backends
+diagnostics/overview
+environment/difficulty
+environment/reproducibility
+environment/graph_observations
+training/curriculum
+training/rllib
+training/scenarios
+guides/verification
+interop/pettingzoo
+interop/soc_export
+```
+
+```{toctree}
+:hidden:
+:maxdepth: 1
+:caption: Benchmarks
+benchmarks/overview
+benchmarks/baselines
+benchmarks/self_play
+benchmarks/run
+```
+
+```{toctree}
+:hidden:
+:maxdepth: 1
+:caption: Architecture
+architecture/overview
+architecture/dynamic_topology
+architecture/ot_physics
+architecture/multimodal_obs
+architecture/zero_trust
+architecture/sim2real
+architecture/nlp_siem
+```
+
+```{toctree}
+:hidden:
+:maxdepth: 1
+:caption: Cyber
+cybersec/overview
+cybersec/threat_model
+cybersec/red_actions
+cybersec/blue_actions
+```
+
+```{toctree}
+:hidden:
+:maxdepth: 1
+:caption: API
+api/python
+api/environment
+api/siem
+api/nlp
+api/sim2real
+```
+
+```{toctree}
+:hidden:
+:maxdepth: 1
+:caption: Contribute
+contributing
+research
```
-See the [Quick Start Guide](quickstart.md) for the raw PettingZoo loop, the JAX vectorized
-loop, difficulty presets, baselines, and diagnostics.
-
-## What's inside
-
-| Layer | Package | Purpose |
-|---|---|---|
-| Environment | `environment/` | PettingZoo `NetForgeRLEnv`, difficulty presets, curriculum |
-| Scenarios | `scenarios/` | 5 reward/objective families (ransomware, APT, cloud, IoT, OT) |
-| Actions | `actions/red`, `actions/blue` | ATT&CK-aligned red/blue capabilities |
-| Telemetry | `siem/`, `nlp/` | Sysmon/Windows log synthesis + NLP encoding |
-| JAX backend | `backends/jax/` | Vectorized `vmap`/`jit` kernels + NumPy reference |
-| Baselines | `baselines/` | Random, heuristic, kill-chain red, JAX PPO |
-| Diagnostics | `diagnostics/` | Capability probes + oracle information-asymmetry |
-| Bridges | `bridges/` | RLlib, JaxMARL, CleanRL, DLPack adapters |
-| Semantic | `semantic/` | LLM SOC agents, grammars, fine-tuning recipes |
-
-## The five scenarios
-
-| Scenario | Red objective | Blue objective | Terminal condition |
-|---|---|---|---|
-| `ransomware` | Encrypt/compromise Corporate + Secure | Contain, restore, avoid downtime | all Corporate/Secure compromised, or PLC kinetic |
-| `apt_espionage` | Stealthy persistence + exfiltration | Detect and isolate every foothold | every infected host isolated |
-| `cloud_hybrid` | Breach the Secure enclave | Protect the Secure subnet SLA | every Secure host compromised |
-| `iot_grid` | Take the grid controllers | Keep controllers healthy | all controllers compromised |
-| `ot_stuxnet` | Drive a PLC to kinetic destruction | Prevent physical damage | any PLC kinetic destruction |
-
-## Citation
-
-```bibtex
-@misc{netforge_rl_2026,
- title = {NetForge RL: A Fast Multi-Agent Cybersecurity Benchmark for Reinforcement Learning},
- author = {ReforceMind},
- year = {2026},
- url = {https://github.com/reforcemind/NetForge_RL}
-}
+```{toctree}
+:hidden:
+:maxdepth: 1
+:caption: Project
+roadmap
+product-direction
+DATASHEET
+changelog
```
diff --git a/docs/interop/pettingzoo.md b/docs/interop/pettingzoo.md
new file mode 100644
index 0000000..fb80d58
--- /dev/null
+++ b/docs/interop/pettingzoo.md
@@ -0,0 +1,23 @@
+# PettingZoo and Gymnasium
+
+```python
+import netforge_rl
+import gymnasium as gym
+from pettingzoo import make
+
+env = make('parallel', 'netforge/ransomware-v4', max_ticks=200)
+single = gym.make('NetForge/Blue-v4', max_ticks=200)
+```
+
+| API | Id |
+|---|---|
+| PettingZoo parallel | `netforge/ransomware-v4`, `netforge/arena-v4` |
+| PettingZoo AEC | same ids |
+| Gymnasium | `NetForge/Blue-v4`, alias `NetForge-v4` |
+
+`gym.make("netforge_rl:NetForge/Blue-v4")` also works. Mask is flat `int8[132]`;
+`CyberActionSpace.sample(mask=…)` accepts it.
+
+Farama listing (docs PR to PettingZoo `third_party_envs`): Parallel + AEC
+`netforge/ransomware-v4`, Gymnasium `NetForge/Blue-v4`,
+https://reforcemind.github.io/NetForge_RL/
diff --git a/docs/interop/soc_export.md b/docs/interop/soc_export.md
new file mode 100644
index 0000000..72329cf
--- /dev/null
+++ b/docs/interop/soc_export.md
@@ -0,0 +1,24 @@
+# SOC export (OCSF)
+
+`record_siem=True` keeps the full stream. `export_ocsf` writes JSONL.
+
+```python
+from netforge_rl.environment.parallel_env import NetForgeRLEnv
+from netforge_rl.siem.export import export_ocsf, siem_to_ocsf
+
+env = NetForgeRLEnv(
+ {'scenario_type': 'ransomware', 'max_ticks': 100, 'record_siem': True}
+)
+env.reset(seed=0)
+export_ocsf(env, 'runs/episode.ocsf.jsonl')
+siem_to_ocsf(log_line, subnet='10.0.0.0/24', tick=42)
+```
+
+| EventID | OCSF |
+|---|---|
+| 4624 / 4625 / 4648 / 4768 / 4776 | Authentication (3002) |
+| 4688 / Sysmon 1 / 10 | Process (1007) |
+| Sysmon 3 | Network (4001) |
+| Sysmon 22 | DNS (4003) |
+
+Raw XML stays in `raw_event`. Honeytoken / incident → higher `severity_id`.
diff --git a/docs/product-direction.md b/docs/product-direction.md
new file mode 100644
index 0000000..3c1ab5a
--- /dev/null
+++ b/docs/product-direction.md
@@ -0,0 +1,19 @@
+# Scope
+
+A gym for training cyber-defense agents. Grow packs, metrics, and wrappers —
+not host-type count and not a bundled MAPPO.
+
+```{image} _static/figures/status.svg
+:alt: What ships in the gym
+:class: fe-fig
+```
+
+| In this repo | Bring your own |
+|---|---|
+| Network, SIEM, Red/Blue actions | MAPPO / QMIX / GNN trainers |
+| Belief graphs from logs | Privileged Blue graphs at train time |
+| YAML packs | Weekly ATT&CK dumps |
+| Heuristic / kill-chain baselines | Production SOC connectors |
+
+Sister docs kit: [FlowEdge](https://github.com/reforcemind/FlowEdge).
+{doc}`questions` · {doc}`arena`
diff --git a/docs/python-quickstart.md b/docs/python-quickstart.md
new file mode 100644
index 0000000..8164f36
--- /dev/null
+++ b/docs/python-quickstart.md
@@ -0,0 +1,45 @@
+# Python
+
+`import netforge_rl` registers the ids. CPython 3.12+. Not on PyPI.
+
+```bash
+python -m pip install 'netforge-rl @ git+https://github.com/reforcemind/NetForge_RL'
+# from a clone:
+python -m pip install -e ".[dev]"
+```
+
+```python
+import netforge_rl
+import gymnasium as gym
+from pettingzoo import make
+
+env = make('parallel', 'netforge/ransomware-v4', max_ticks=200)
+obs, infos = env.reset(seed=0)
+actions = {agent: env.action_space(agent).sample() for agent in env.agents}
+obs, rewards, term, trunc, infos = env.step(actions)
+
+blue = gym.make('NetForge/Blue-v4', max_ticks=200)
+```
+
+| API | Id | |
+|---|---|---|
+| PettingZoo parallel | `netforge/ransomware-v4` | 1 Red + 3 Blue |
+| PettingZoo parallel | `netforge/arena-v4` | same factory |
+| PettingZoo AEC | same ids | turn-based |
+| Gymnasium | `NetForge/Blue-v4` | one Blue vs scripted Red |
+| Gymnasium | `NetForge-v4` | alias |
+
+Action: `MultiDiscrete([32, 100])`, mask `int8[132]`.
+Blue graph: `GraphObservationWrapper`.
+
+| Extra | |
+|---|---|
+| *(none)* | env, CLI, heuristics |
+| `dev` | ruff, pytest |
+| `docs` | Sphinx |
+| `jax` | vectorized surrogate |
+| `rllib` | RLlib env wrapper |
+| `offline` | HDF5 export |
+| `nlp` / `llm_clients` | SIEM encoder / LLM agents |
+
+{doc}`interop/pettingzoo`
diff --git a/docs/questions.md b/docs/questions.md
new file mode 100644
index 0000000..5bad5c1
--- /dev/null
+++ b/docs/questions.md
@@ -0,0 +1,30 @@
+# Probes
+
+Small experiments the gym already runs. Heuristics are in-tree so they execute.
+
+```{image} _static/figures/belief.svg
+:alt: SIEM belief vs full state
+:class: fe-fig
+```
+
+```bash
+netforge questions
+netforge questions belief-vs-oracle --seeds 0 --max-ticks 40
+```
+
+| id | What it checks |
+|---|---|
+| `belief-vs-oracle` | How much compromise is missing from Blue’s SIEM graph |
+| `reward-design` | SLA-only / FP-penalized reward vs isolation |
+| `red-population` | One Blue vs several Reds |
+| `risk-sensitive` | Mean vs CVaR vs kinetic tails |
+| `time-mode` | Event-driven vs fixed-step |
+| `ood-topology` | More live hosts than training |
+| `adaptation` | Red changes strategy mid-episode |
+| `deception` | Honeypots / decoys |
+| `constrained-sla` | Defense under SLA / FP costs |
+| `gnn-belief` | Train a GNN on SIEM belief graphs (your trainer) |
+| `offline-to-online` | Pretrain on dumps, then online |
+| `league-psro` | Best response to a Red league |
+
+Add one in `netforge_rl/arena/questions/`. {doc}`submissions`
diff --git a/docs/quickstart.md b/docs/quickstart.md
deleted file mode 100644
index 5906642..0000000
--- a/docs/quickstart.md
+++ /dev/null
@@ -1,147 +0,0 @@
-# Quick Start Guide
-
-## Installation
-
-Requires Python 3.12+.
-
-```bash
-pip install 'netforge_rl[jax,render,finetune] @ git+https://github.com/reforcemind/NetForge_RL'
-```
-
-Extras: `jax` (vectorized backend), `render` (matplotlib + moviepy), `finetune`
-(TRL/PEFT for LLM agents), `rllib` (Ray RLlib bridge).
-
-## PettingZoo execution (single environment)
-
-The reference Python backend implements the PettingZoo parallel API. Each agent acts with a
-`MultiDiscrete([32, 100])` action — `[action_type_id, target_host_index]`.
-
-```python
-import numpy as np
-from netforge_rl.environment.parallel_env import NetForgeRLEnv
-
-env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 50})
-obs, infos = env.reset(seed=0)
-
-while env.agents:
- actions = {a: env.action_space(a).sample() for a in env.agents}
- obs, rewards, term, trunc, infos = env.step(actions)
- if all(term.values()) or all(trunc.values()):
- break
-```
-
-Each `obs[agent]` is a dict with `obs` (256-d state), `action_mask` (132-d), `siem_embedding`
-(128-d), `adj_matrix`, and `delta_t`; Blue agents additionally get `blue_comm`.
-
-## Difficulty presets and the held-out split
-
-Instead of hand-writing config dicts, use named difficulty tiers and the frozen evaluation
-seed suite. See [Difficulty & Splits](environment/difficulty.md) for the full knob table.
-
-```python
-from netforge_rl.environment import make_env, EVAL_SEEDS
-
-# Train on any seeds at a chosen difficulty
-train_env = make_env('medium', scenario_type='apt_espionage', seed=0)
-
-# Report on held-out topologies never seen during training
-eval_env = make_env('hard', scenario_type='apt_espionage', evaluation=True, seed=EVAL_SEEDS[0])
-```
-
-## JAX vectorized execution
-
-Hardware-accelerated batched rollouts for high-throughput training.
-
-```python
-import jax
-from netforge_rl.backends.jax import VectorEnvSpec
-from netforge_rl.bridges.jaxmarl import JaxMARLEnv, random_action_dict
-
-env = JaxMARLEnv(spec=VectorEnvSpec(n_hosts=100, n_red=1, n_blue=3), batch_size=4096)
-key = jax.random.PRNGKey(0)
-
-obs, state = env.reset(key)
-obs, state, reward, done, info = env.step(key, state, random_action_dict(env, key))
-```
-
-## Running baselines
-
-```python
-from netforge_rl.environment.parallel_env import NetForgeRLEnv
-from netforge_rl.baselines.policies import KillChainRedPolicy, HeuristicBluePolicy
-
-env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 150})
-env.reset(seed=0)
-red, blue = KillChainRedPolicy(seed=0), HeuristicBluePolicy(seed=0)
-
-while env.agents:
- actions = {a: (red.act(env, a) if 'red' in a else blue.act(env, a)) for a in env.agents}
- _, _, term, trunc, _ = env.step(actions)
- if all(term.values()) or all(trunc.values()):
- break
-```
-
-`KillChainRedPolicy` runs a recon → exploit → pivot kill-chain and actually compromises
-hosts; see [Baselines](benchmarks/baselines.md).
-
-## Single-agent training (Gymnasium / SB3 / CleanRL)
-
-For standard single-agent RL, `NetForgeSingleAgentEnv` is a `gymnasium.Env` that controls one
-agent against scripted opponents. It passes `gymnasium`'s `check_env` and exposes the action
-mask in `info` for maskable algorithms.
-
-```python
-from netforge_rl.environment import NetForgeSingleAgentEnv
-from netforge_rl.baselines.policies import KillChainRedPolicy
-
-env = NetForgeSingleAgentEnv(
- 'ransomware',
- controlled_agent='blue_dmz',
- opponents={'red_operator': KillChainRedPolicy(seed=0)},
-)
-obs, info = env.reset(seed=0) # obs: Box(384,), info['action_mask']: (132,)
-obs, reward, terminated, truncated, info = env.step(env.action_space.sample())
-```
-
-## Training IPPO on the JAX backend
-
-```python
-from netforge_rl.baselines.jax_ppo import ippo_train, PPOConfig, save_params
-
-out = ippo_train(PPOConfig(total_iters=40, num_steps=48, batch_size=128))
-save_params(out['params'], 'runs/ippo_blue.npz')
-print('reward:', out['reward_curve'][0], '->', out['reward_curve'][-1])
-```
-
-See [Baselines](benchmarks/baselines.md) for the committed learning curve.
-
-## Running the diagnostic probes
-
-```python
-from netforge_rl.diagnostics import all_diagnostics, run_diagnostic
-from netforge_rl.baselines.policies import HeuristicBluePolicy
-
-for probe in all_diagnostics():
- result = run_diagnostic(probe, HeuristicBluePolicy(seed=0), seed=0)
- print(f'{probe.capability:14} {probe.name:26} score={result.score:.2f}')
-```
-
-See [Diagnostics](diagnostics/overview.md) for what each capability measures.
-
-## LLM SOC agent (optional)
-
-`run_episode` takes a `clients` dict mapping each agent id to an LLM client; uncontrolled
-agents no-op, and unparseable replies are counted as `invalid_replies`.
-
-```python
-from netforge_rl.environment.parallel_env import NetForgeRLEnv
-from netforge_rl.semantic.clients.mock import MockLLMClient
-from netforge_rl.semantic.runner import run_episode
-
-env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 50})
-result = run_episode(env, clients={'blue_dmz': MockLLMClient()}, seed=0)
-print(result.rewards, result.invalid_replies)
-```
-
-Swap `MockLLMClient` for an OpenAI/Anthropic/vLLM client to have a language model read raw
-SIEM logs and issue defensive actions.
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..f37ea70
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,11 @@
+sphinx>=8.0
+furo>=2024.8.6
+myst-parser>=4.0
+sphinxcontrib-mermaid>=1.0.0
+numpy>=2.0
+gymnasium>=1.2
+pettingzoo>=1.26
+networkx>=3.4
+pyyaml>=6.0
+pillow>=10.0
+scikit-learn>=1.0
diff --git a/docs/research.md b/docs/research.md
new file mode 100644
index 0000000..f275b8f
--- /dev/null
+++ b/docs/research.md
@@ -0,0 +1,16 @@
+# Directions
+
+This tree is the gym: network, logs, actions, eval. Trainers and new exploit
+families stay out unless they are a wrapper, pack, or probe.
+
+- Belief vs oracle (SOC view vs true compromise)
+- Defense under SLA / false-positive cost
+- Reward that does not isolate the whole net
+- Offline logs → online fine-tune
+- Adaptive Red, leagues
+- Event time vs fixed ticks
+- LLM vs RL on the same env
+
+Catalog: `netforge questions`. Issues: [RESEARCH.md](https://github.com/reforcemind/NetForge_RL/blob/main/RESEARCH.md).
+
+{doc}`product-direction` · {doc}`roadmap`
diff --git a/docs/roadmap.md b/docs/roadmap.md
new file mode 100644
index 0000000..fc91e28
--- /dev/null
+++ b/docs/roadmap.md
@@ -0,0 +1,9 @@
+# Roadmap
+
+Backlog: [RESEARCH.md](https://github.com/reforcemind/NetForge_RL/blob/main/RESEARCH.md).
+Labels: `research` / `good first issue`.
+
+Shipped: PettingZoo / Gymnasium ids, SIEM belief graphs, eval CLI.
+
+Next: trainers **on** this env (out of tree), hidden-split harness, Farama listing,
+published checkpoints.
diff --git a/docs/scenarios.md b/docs/scenarios.md
new file mode 100644
index 0000000..47a6d67
--- /dev/null
+++ b/docs/scenarios.md
@@ -0,0 +1,18 @@
+# Scenarios
+
+Pick a family and a topology. No simulator patch.
+
+Families: `ransomware`, `apt_espionage`, `cloud_hybrid`, `iot_grid`, `ot_stuxnet`.
+
+Packs: `hospital_ransomware`, `cloud_iam`, `enterprise_apt`, `finance`,
+`iot_fleet`, `ot_plant`, `zero_trust`.
+
+```bash
+netforge run hospital_ransomware
+netforge run examples/my_scenario.yaml
+```
+
+Reward knobs (`RewardDesignWrapper`): `sla_only`, `security_only`,
+`fp_penalized`, `constrained_sla`, `safety_critical`, `sparse`.
+
+YAML lives in `netforge_rl/scenarios/packs/`.
diff --git a/docs/stylesheets/custom.css b/docs/stylesheets/custom.css
deleted file mode 100644
index eec8751..0000000
--- a/docs/stylesheets/custom.css
+++ /dev/null
@@ -1,107 +0,0 @@
-/* ReforceMind Target Site Emulation */
-
-@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Libre+Franklin:wght@400;500;600;700&family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,600;6..72,700&display=swap');
-
-:root {
- /* Colors based on the reforcemind.github.io parchment/ink palette */
- --md-primary-fg-color: #1a4231; /* forest-intellect */
- --md-accent-fg-color: #8b2635; /* crimson-authority */
-
- --md-default-bg-color: #F8F5F0; /* parchment-base */
- --md-default-fg-color: #1A1D20; /* ink-black */
-
- --md-code-bg-color: #e9e5de;
- --md-code-fg-color: #1A1D20;
-}
-
-[data-md-color-scheme="slate"] {
- --md-default-bg-color: #0b0f19;
- --md-default-fg-color: #e2e8f0;
- --md-code-bg-color: #0f172a;
- --md-primary-fg-color: #2ab88a;
- --md-accent-fg-color: #ff6b6b;
-}
-
-/* Background overlay mimicking the target site gradients */
-body[data-md-color-scheme="default"] {
- background-image:
- radial-gradient(ellipse at top left, rgba(26, 66, 49, 0.1) 0%, transparent 50%),
- radial-gradient(ellipse at bottom right, rgba(139, 38, 53, 0.08) 0%, transparent 50%);
- background-attachment: fixed;
-}
-
-/* Typography Overrides */
-.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4, .md-typeset h5, .md-typeset h6 {
- font-family: 'Newsreader', serif;
- font-weight: 600;
- color: #1a4231;
-}
-
-[data-md-color-scheme="slate"] .md-typeset h1,
-[data-md-color-scheme="slate"] .md-typeset h2 {
- color: #2ab88a;
-}
-
-/* Header Overlay */
-.md-header {
- background: rgba(248, 245, 240, 0.8) !important;
- backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
- border-bottom: 1px solid rgba(26, 66, 49, 0.1);
- color: #1a4231 !important;
-}
-
-[data-md-color-scheme="slate"] .md-header {
- background: rgba(11, 15, 25, 0.7) !important;
- border-bottom: 1px solid rgba(42, 184, 138, 0.2);
- color: #2ab88a !important;
-}
-
-/* Force header links to be visible on the light parchment background */
-.md-header__button.md-logo,
-.md-header__button.md-icon,
-.md-header__title,
-.md-search__input,
-.md-tabs__link {
- color: #1A1D20 !important;
-}
-
-[data-md-color-scheme="slate"] .md-header__button.md-logo,
-[data-md-color-scheme="slate"] .md-header__button.md-icon,
-[data-md-color-scheme="slate"] .md-header__title,
-[data-md-color-scheme="slate"] .md-tabs__link {
- color: #e2e8f0 !important;
-}
-
-/* Code Block Styling */
-.md-typeset pre > code {
- border-radius: 8px;
- border: 1px solid rgba(0, 0, 0, 0.05);
- box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.02);
- font-family: 'JetBrains Mono', monospace;
-}
-
-/* Admonition Overrides */
-.md-typeset .admonition {
- border-radius: 8px;
- background: rgba(0, 0, 0, 0.02);
- border: 1px solid rgba(0, 0, 0, 0.05);
- border-left: 4px solid var(--md-admonition-fg-color);
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.03);
- transition: transform 0.2s ease, box-shadow 0.2s ease;
-}
-
-.md-typeset .admonition:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
-}
-
-/* Active Navigation Item */
-.md-nav__item--active > .md-nav__link {
- color: #8b2635 !important;
- font-weight: 600;
-}
-
-[data-md-color-scheme="slate"] .md-nav__item--active > .md-nav__link {
- color: #ff6b6b !important;
-}
diff --git a/docs/submissions.md b/docs/submissions.md
new file mode 100644
index 0000000..f1d7308
--- /dev/null
+++ b/docs/submissions.md
@@ -0,0 +1,24 @@
+# Your agent
+
+Drop in a Blue policy with `make_blue`. Eval reports mission, SLA, FPs, security,
+kinetic failures — not only return.
+
+```bash
+netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 1 --max-ticks 40
+```
+
+```python
+NAME, TEAM, TECHNIQUE = 'my-blue', 'lab', 'ppo-belief'
+
+
+def wrap(env):
+ return env # belief graphs already on; do not attach oracle_graph
+
+
+def make_blue(seed=0):
+ return MyPolicy(seed=seed)
+```
+
+`act(env, agent_id) -> np.ndarray`. Optional `make_red`. `--card` adds probes.
+
+{doc}`arena` · `examples/submissions/heuristic_blue.py`
diff --git a/docs/training/curriculum.md b/docs/training/curriculum.md
new file mode 100644
index 0000000..7932681
--- /dev/null
+++ b/docs/training/curriculum.md
@@ -0,0 +1,11 @@
+# Curriculum
+
+`CurriculumWrapper` raises difficulty when a moving-average reward clears a threshold.
+
+| Phase | Hosts | Scenarios | Mechanics |
+|---|---|---|---|
+| Novice | 5 | ransomware | static, 3× reward |
+| Intermediate | 25 | + apt | DHCP every 80 ticks, 1.5× |
+| Expert | 100 | + iot_grid | DHCP 40, dynamic topology, 1× |
+
+{doc}`../architecture/dynamic_topology`
diff --git a/docs/training/rllib.md b/docs/training/rllib.md
index adef1ad..33cd69f 100644
--- a/docs/training/rllib.md
+++ b/docs/training/rllib.md
@@ -1,26 +1,13 @@
-# Training with Ray RLlib
-
-## Environment Wrapper
-
-NetForge implements the RLlib `MultiAgentEnv` interface via `NetForgeRLlibEnv`.
+# Ray RLlib
```python
from netforge_rl.bridges.rllib_bridge import NetForgeRLlibEnv
-env = NetForgeRLlibEnv({"scenario_type": "ransomware", "max_ticks": 100})
-```
-
-## RMAPPO Baseline
-
-The reference training script is [`benchmarks/rllib_rmappo.py`](https://github.com/reforcemind/NetForge_RL/blob/main/benchmarks/rllib_rmappo.py). It trains two shared LSTM policies (one red, one blue) with PPO on the legacy API stack.
-
-```bash
-python benchmarks/rllib_rmappo.py
+env = NetForgeRLlibEnv({'scenario_type': 'ransomware', 'max_ticks': 100})
```
-Key settings: `train_batch_size=1024`, `use_lstm=True`, legacy API stack (`enable_rl_module_and_learner=False`). Custom metrics from `NetForgeMetricsCallback` are logged per-agent each step.
-
-## PPO Configuration
+Reference: `python benchmarks/rllib_rmappo.py` — shared LSTM PPO, red + blue,
+`train_batch_size=1024`, legacy API stack.
```python
import ray
@@ -29,54 +16,26 @@ from ray.rllib.algorithms.ppo import PPOConfig
from netforge_rl.bridges.rllib_bridge import NetForgeRLlibEnv
ray.init()
-register_env("netforge-v0", lambda cfg: NetForgeRLlibEnv(cfg))
-
+register_env('netforge-v0', lambda cfg: NetForgeRLlibEnv(cfg))
config = (
PPOConfig()
- .environment("netforge-v0", env_config={"scenario_type": "ransomware"})
- .api_stack(enable_rl_module_and_learner=False, enable_env_runner_and_connector_v2=False)
+ .environment('netforge-v0', env_config={'scenario_type': 'ransomware'})
+ .api_stack(
+ enable_rl_module_and_learner=False,
+ enable_env_runner_and_connector_v2=False,
+ )
.multi_agent(
policies={
- "red_rmappo": (None, None, None, {"model": {"use_lstm": True}}),
- "blue_rmappo": (None, None, None, {"model": {"use_lstm": True}}),
+ 'red_rmappo': (None, None, None, {'model': {'use_lstm': True}}),
+ 'blue_rmappo': (None, None, None, {'model': {'use_lstm': True}}),
},
policy_mapping_fn=lambda agent_id, *a, **kw: (
- "red_rmappo" if "red" in agent_id else "blue_rmappo"
+ 'red_rmappo' if 'red' in agent_id else 'blue_rmappo'
),
)
.training(train_batch_size=1024)
)
algo = config.build()
-result = algo.train()
-```
-
-## Curriculum Learning
-
-`CurriculumWrapper` wraps `NetForgeRLEnv` and automatically progresses through three phases based on rolling mean episode reward.
-
-| Phase | Active hosts | Scenarios | DHCP | Dynamic topology | Reward scale | Graduate at |
-|---|---|---|---|---|---|---|
-| `novice` | 5 | ransomware | off | off | 3× | mean ≥ 60 over 10 eps |
-| `intermediate` | 25 | ransomware, apt | 80 ticks | off | 1.5× | mean ≥ 40 over 15 eps |
-| `expert` | 100 | ransomware, apt, iot | 40 ticks | churn + migration | 1× | final |
-
-```python
-from netforge_rl.environment.curriculum import CurriculumWrapper
-
-env = CurriculumWrapper(
- base_cfg={"max_ticks": 200},
- on_phase_advance=lambda idx, name: print(f"Phase {idx}: {name}"),
-)
-obs, _ = env.reset(seed=0)
```
-Each step's `info` dict contains `__curriculum__` with `phase`, `phase_index`, `mean_reward`, `window_fill`, and `phase_advanced`.
-
-Start from a specific phase or supply custom phases:
-
-```python
-from netforge_rl.environment.curriculum import CurriculumWrapper, PHASES
-
-env = CurriculumWrapper(phases=PHASES, start_phase=1) # start at intermediate
-```
-```
\ No newline at end of file
+Curriculum: {doc}`curriculum`.
diff --git a/docs/training/scenarios.md b/docs/training/scenarios.md
index 68c12f3..9047f55 100644
--- a/docs/training/scenarios.md
+++ b/docs/training/scenarios.md
@@ -1,123 +1,51 @@
-# Custom Scenarios
+# Custom scenarios
-Custom scenarios define the **win conditions** and **reward tensors** for the environment. Core resolution logic (e.g., executing an exploit) is handled independently by the `Functional Core`.
-
-## BaseScenario Interface
-
-Scenarios must inherit from `netforge_rl.scenarios.base_scenario.BaseScenario` and implement two abstract methods:
-
-1. `calculate_reward(self, agent_id, global_state, effect)`: Returns a scalar float representing the delta reward for the current step.
-2. `check_termination(self, global_state)`: Returns a dictionary mapping agent IDs to boolean terminal states.
-
-## Configuration: Ransomware
-
-In a Ransomware configuration, the Red agent seeks to compromise system integrity flags, while the Blue agent seeks to maximize healthy host ratios.
-
-### 1. Structure and Initialization
+Subclass `BaseScenario`: `calculate_reward(agent_id, global_state, effect)` and
+`check_termination(global_state)`. Exploits stay in the functional core.
```python
from netforge_rl.scenarios.base_scenario import BaseScenario
-class CustomRansomware(BaseScenario):
- def __init__(self, agents):
- self.agents = agents
-```
-
-### 2. Base Reward Routing
-A central `calculate_reward` method routes scalar logic to specific agent functions, applying global penalties (like action cost) universally.
-
-```python
+class CustomRansomware(BaseScenario):
def calculate_reward(self, agent_id, global_state, effect=None):
reward = 0.0
-
- # Base penalty for taking actions
if effect and getattr(effect, 'cost', 0) > 0:
reward -= effect.cost * 0.05
-
if 'red' in agent_id:
return self._red_reward(global_state, effect)
return self._blue_reward(global_state, effect)
-```
-### 3. Red Agent Logic
-
-Red agents are rewarded exclusively for successfully modifying the `GlobalNetworkState` access and integrity tensors.
-
-```python
def _red_reward(self, global_state, effect):
- reward = 0.0
- if effect and effect.success and effect.state_deltas:
- for key, val in effect.state_deltas.items():
- if 'privilege' in key and val == 'Root':
- reward += 5.0
- if 'system_integrity' in key and val == 'kinetic_destruction':
- reward += 10000.0
- return reward
-```
-
-### 4. Blue Agent Logic
+ if not (effect and effect.success and effect.state_deltas):
+ return 0.0
+ r = 0.0
+ for key, val in effect.state_deltas.items():
+ if 'privilege' in key and val == 'Root':
+ r += 5.0
+ if 'system_integrity' in key and val == 'kinetic_destruction':
+ r += 10000.0
+ return r
-Blue agents are rewarded proportionally to the number of nodes maintaining a baseline (healthy) state vector.
-
-```python
def _blue_reward(self, global_state, effect):
+ hosts = global_state.all_hosts.values()
healthy = sum(
- 1 for h in global_state.all_hosts.values()
- if h.compromised_by == 'None' and h.status != 'isolated'
+ 1 for h in hosts if h.compromised_by == 'None' and h.status != 'isolated'
)
return healthy / len(global_state.all_hosts)
-```
-
-### 5. Termination Logic
-
-The episode halts immediately if a critical Cyber-Physical node transitions to a destroyed state.
-```python
def check_termination(self, global_state):
- if any(h.system_integrity == 'kinetic_destruction' for h in global_state.all_hosts.values()):
- return {agent: True for agent in self.agents}
-
- return {agent: False for agent in self.agents}
-```
-
-## OT/SCADA Scenario: `ot_stuxnet`
-
-The `OTStuxnetScenario` rewards Red for achieving physical process destruction on PLC hosts, and Blue for keeping OT nodes clean. A 25% chance each episode generates an OT subnet (`10.0.99.0/24`) with `PLC_Firmware` hosts.
-
-### Physical state
-
-Each PLC host tracks three process variables governed by first-order lag ODEs (in `PLCPhysicsEngine`):
-
-| Variable | Nominal | Alarm | Critical | τ (ticks) |
-|---|---|---|---|---|
-| temperature (°C) | 40–60 | > 80 | > 120 | 20 |
-| pressure (bar) | 90–110 | > 130 or < 70 | > 180 or < 30 | 10 |
-| flow_rate (L/min) | 40–60 | > 90 or < 20 | > 150 or < 5 | 5 |
-
-Each tick: `x[t+1] = x[t] + (setpoint − x[t]) / τ + noise`
-
-### Attack flow
-
-`OverloadPLC` (red_operator action ID 20, requires Root on a PLC host) manipulates setpoints rather than directly spiking values:
-
-```python
-# setpoints jump to unsafe targets; physics engine drives process toward them
-temperature_setpoint += uniform(80, 150) # °C above normal
-pressure_setpoint *= uniform(1.5, 2.0) # 1.5–2× nominal
-flow_rate_setpoint = 3.0 # near-zero (choke)
+ dead = any(
+ h.system_integrity == 'kinetic_destruction'
+ for h in global_state.all_hosts.values()
+ )
+ return {agent: dead for agent in self.agents}
```
-Blue receives `SCADA_PHYSICAL_ALARM` SIEM alerts (severity 7) when variables enter alarm range, and `SCADA_KINETIC_BREACH` (severity 10) when critical thresholds are crossed. Blue has roughly 10 ticks between first alarm and physical destruction to isolate the PLC.
+`ot_stuxnet`: PLC ODE physics, `OverloadPLC` moves setpoints, Blue has ~10 ticks
+after `SCADA_PHYSICAL_ALARM` to isolate. Tables: {doc}`../architecture/ot_physics`.
```python
env = NetForgeRLEnv({'scenario_type': 'ot_stuxnet', 'max_ticks': 300})
-```
-
-## Registering Configurations
-
-Scenario classes are registered dynamically via the `parallel_env` instantiation dictionary:
-
-```python
env = NetForgeRLEnv({'scenario_type': 'custom_ransomware'})
-```
\ No newline at end of file
+```
diff --git a/examples/submissions/heuristic_blue.py b/examples/submissions/heuristic_blue.py
index a8ac9a9..8584b1a 100644
--- a/examples/submissions/heuristic_blue.py
+++ b/examples/submissions/heuristic_blue.py
@@ -1,6 +1,6 @@
"""CAGE-style ``make_blue`` example.
- netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 --max-ticks 20
+netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 --max-ticks 20
"""
NAME = 'heuristic-blue'
diff --git a/mkdocs.yml b/mkdocs.yml
deleted file mode 100644
index 926c1e7..0000000
--- a/mkdocs.yml
+++ /dev/null
@@ -1,128 +0,0 @@
-site_name: NetForge RL
-site_description: Multi-Agent Cybersecurity Reinforcement Learning Environment
-site_author: Igor Jankowski
-site_url: https://reforcemind.github.io/NetForge_RL/
-repo_url: https://github.com/reforcemind/NetForge_RL
-repo_name: reforcemind/NetForge_RL
-edit_uri: edit/main/docs/
-
-theme:
- name: material
- palette:
- - scheme: default
- primary: teal
- accent: deep orange
- toggle:
- icon: material/brightness-7
- name: Switch to dark mode
- - scheme: slate
- primary: teal
- accent: deep orange
- toggle:
- icon: material/brightness-4
- name: Switch to light mode
- font:
- text: Libre Franklin
- code: JetBrains Mono
- features:
- - navigation.tabs
- - navigation.tabs.sticky
- - navigation.sections
- - navigation.expand
- - navigation.top
- - navigation.footer
- - search.highlight
- - search.share
- - content.code.copy
- - content.code.annotate
- - content.tabs.link
- icon:
- repo: fontawesome/brands/github
- logo: material/shield-lock
-
-extra_css:
- - stylesheets/custom.css
-
-extra:
- social:
- - icon: fontawesome/brands/github
- link: https://github.com/xaiqo/NetForge_RL
-
-markdown_extensions:
- - admonition
- - pymdownx.details
- - pymdownx.superfences:
- custom_fences:
- - name: mermaid
- class: mermaid
- format: !!python/name:pymdownx.superfences.fence_code_format
- - pymdownx.highlight:
- anchor_linenums: true
- - pymdownx.inlinehilite
- - pymdownx.tabbed:
- alternate_style: true
- - pymdownx.emoji:
- emoji_index: !!python/name:material.extensions.emoji.twemoji
- emoji_generator: !!python/name:material.extensions.emoji.to_svg
- - pymdownx.tasklist:
- custom_checkbox: true
- - attr_list
- - md_in_html
- - tables
- - toc:
- permalink: true
-
-plugins:
- - search
- - mkdocstrings:
- handlers:
- python:
- paths: [.]
- options:
- docstring_style: google
- show_source: true
- show_root_heading: true
- show_root_full_path: false
- members_order: source
- separate_signature: true
- show_signature_annotations: true
-
-nav:
- - Home: index.md
- - Quick Start: quickstart.md
- - Datasheet: DATASHEET.md
- - Environment:
- - Difficulty & Splits: environment/difficulty.md
- - Reproducibility: environment/reproducibility.md
- - Graph Observations: environment/graph_observations.md
- - Diagnostics: diagnostics/overview.md
- - Interoperability:
- - SOC Export (OCSF): interop/soc_export.md
- - Architecture:
- - Overview: architecture/overview.md
- - Dynamic Topologies: architecture/dynamic_topology.md
- - OT/SCADA Physics: architecture/ot_physics.md
- - Multi-Modal Observations: architecture/multimodal_obs.md
- - Zero-Trust Identity: architecture/zero_trust.md
- - Sim2Real Bridge: architecture/sim2real.md
- - NLP-SIEM Pipeline: architecture/nlp_siem.md
- - Cybersecurity Reference:
- - Environment Overview: cybersec/overview.md
- - Threat Model & ATT&CK: cybersec/threat_model.md
- - Red Team Actions: cybersec/red_actions.md
- - Blue Team Actions: cybersec/blue_actions.md
- - API Reference:
- - Environment: api/environment.md
- - Sim2Real: api/sim2real.md
- - SIEM: api/siem.md
- - NLP Encoder: api/nlp.md
- - Training Guide:
- - Curriculum Learning: training/curriculum.md
- - RLlib + LSTM: training/rllib.md
- - Scenarios: training/scenarios.md
- - Benchmarks:
- - Overview: benchmarks/overview.md
- - Baselines: benchmarks/baselines.md
- - Self-Play & Elo: benchmarks/self_play.md
- - Run: benchmarks/run.md
- - Changelog: changelog.md
diff --git a/pyproject.toml b/pyproject.toml
index 09d5284..a22807c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -122,6 +122,7 @@ docstring-code-line-length = "dynamic"
"netforge_rl/semantic/clients/*.py" = ["F401"]
"netforge_rl/render/replay_html.py" = ["E501"]
[tool.ruff.lint]
+select = ["E4", "E7", "E9", "F"]
ignore = ["E701", "E402", "E702"]
[tool.setuptools.packages.find]
diff --git a/tests/render/test_render_pipeline.py b/tests/render/test_render_pipeline.py
index 75bf41c..daf722d 100644
--- a/tests/render/test_render_pipeline.py
+++ b/tests/render/test_render_pipeline.py
@@ -68,6 +68,8 @@ def test_frame_recorder_append_and_len() -> None:
def test_frame_recorder_save_requires_frames(tmp_path) -> None:
pytest.importorskip('moviepy')
from netforge_rl.render.recorder import FrameRecorder
+
+ rec = FrameRecorder(fps=4)
with pytest.raises(ValueError):
rec.save(tmp_path / 'empty.gif')