CIS490/tools/run_envelope_demo.py
Maximus Gorog 970698af83 Synthetic envelope demo: phase-driven load mimic + plotter
End-to-end pipeline now produces a labeled envelope from a single command.
Drives the orchestrator through an 8-phase XMRig-shaped schedule and
renders a 3-panel envelope (CPU%, RSS, IO write rate) with phase bands
sourced from labels.jsonl. Real telemetry, simulated load — validates the
collection + labeling shape before a real VM is involved.

Components:
- tools/load_mimic.py        phase-driven load generator. Reads phase
                             commands on stdin; CPU/IO behavior matches
                             the named phase (clean=idle, armed=light burst,
                             infecting=disk burst+CPU, infected_running=
                             CPU saturation+stratum-shaped writes,
                             dormant=quieter than clean).
- tools/run_envelope_demo.py spawns load_mimic, drives EpisodeRunner with
                             a default 85s schedule that includes the
                             classic infected_running → dormant → re-entry
                             pattern.
- tools/plot_envelope.py     reads telemetry + labels from an episode dir,
                             writes envelope.png with colored phase bands.

orchestrator: EpisodeRunner now takes an optional phase_schedule and an
on_phase callback. Walks the schedule emitting one label per transition.
Backwards-compatible — existing single-phase tests still green.

Doc fix (user pushback): README + architecture + threat-model no longer
imply the Pi5 is the deployment target. Pi5's actual role here is the
WireGuard-side collector for episode tarballs. Deployment target is
generic ("constrained Linux device"). The "gateway observer" concept
remains a deployment pattern, decoupled from the Pi5's collector role.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:53:20 -06:00

118 lines
3.3 KiB
Python

"""Run a synthetic envelope demo end-to-end.
Spawns ``tools/load_mimic.py`` as a subprocess, drives the orchestrator
through an XMRig-shaped phase schedule, and writes a complete episode
directory. Real telemetry from the host /proc collector; load is simulated
so we can validate the pipeline before a real VM is involved.
Usage:
uv run python tools/run_envelope_demo.py [--data-root data]
After it finishes, plot with:
uv run python tools/plot_envelope.py <episode_dir>
"""
from __future__ import annotations
import argparse
import logging
import subprocess
import sys
from pathlib import Path
# Allow running as a script: add repo root to sys.path before project imports.
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from orchestrator.episode import EpisodeConfig, EpisodeRunner # noqa: E402
# (phase, duration_seconds). Total ~85s.
DEFAULT_SCHEDULE = [
("clean", 10.0),
("armed", 2.0),
("infecting", 3.0),
("infected_running", 25.0),
("dormant", 15.0),
("infected_running", 20.0),
("dormant", 5.0),
("clean", 5.0),
]
def main() -> int:
parser = argparse.ArgumentParser(prog="run_envelope_demo")
parser.add_argument("--data-root", default="data")
parser.add_argument("--interval-ms", type=int, default=100)
args = parser.parse_args()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
here = Path(__file__).parent
load_mimic = here / "load_mimic.py"
proc = subprocess.Popen(
[sys.executable, str(load_mimic)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
text=True,
)
# First line: "load_mimic pid=<n> scratch=<path>"
first = proc.stdout.readline().strip()
if not first.startswith("load_mimic pid="):
proc.kill()
print(f"unexpected load_mimic startup: {first!r}", file=sys.stderr)
return 2
pid = int(first.split()[1].split("=", 1)[1])
print(first)
def on_phase(p: str) -> None:
proc.stdin.write(p + "\n")
proc.stdin.flush()
# Drain the echo line so the pipe doesn't fill up.
proc.stdout.readline()
print(f" >>> phase = {p}")
cfg = EpisodeConfig(
target_pid=pid,
duration_s=sum(d for _, d in DEFAULT_SCHEDULE),
interval_ms=args.interval_ms,
data_root=Path(args.data_root),
phase_schedule=DEFAULT_SCHEDULE,
image_name="load_mimic.py",
snapshot_name="(synthetic)",
)
try:
result = EpisodeRunner(cfg, on_phase=on_phase).run()
finally:
try:
proc.stdin.write("quit\n")
proc.stdin.flush()
except Exception:
pass
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
print()
print(f"episode_id = {result.episode_id}")
print(f"path = {result.episode_dir}")
print(f"rows_proc = {result.rows_proc}")
print(f"phases = {result.phases_observed}")
print()
print("To plot:")
print(f" uv run python tools/plot_envelope.py {result.episode_dir}")
return 0
if __name__ == "__main__":
sys.exit(main())