Module scenario.cli

Scenario CLI entry point.

Installed as the scenario console script. Currently exposes:

  • scenario redteam-report — launch the Streamlit dashboard on a red-team batch directory, auto-discovering the latest batch if none specified.

The Streamlit app itself lives at :mod:scenario.report.app.

Expand source code
"""Scenario CLI entry point.

Installed as the ``scenario`` console script. Currently exposes:

  * ``scenario redteam-report`` — launch the Streamlit dashboard on a
    red-team batch directory, auto-discovering the latest batch if none
    specified.

The Streamlit app itself lives at :mod:`scenario.report.app`.
"""

from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Optional


def _find_batch_dir(
    reports_root: Path, batch: Optional[str], latest_n: int
) -> Optional[Path]:
    """Return the batch directory to open, or None if none found."""
    if batch:
        p = reports_root / batch
        return p if p.is_dir() else None

    if not reports_root.is_dir():
        return None

    candidates = sorted(
        (p for p in reports_root.iterdir() if p.is_dir()),
        key=lambda p: p.name,
        reverse=True,
    )
    if not candidates:
        return None
    idx = max(1, latest_n) - 1
    if idx >= len(candidates):
        return None
    return candidates[idx]


def _cmd_redteam_report(args: argparse.Namespace) -> int:
    reports_root = Path(args.dir).resolve()
    batch_dir = _find_batch_dir(reports_root, args.batch, args.latest)
    if batch_dir is None:
        if args.batch:
            print(f"error: batch '{args.batch}' not found under {reports_root}", file=sys.stderr)
        elif not reports_root.is_dir():
            print(
                f"error: no reports directory at {reports_root}\n"
                f"hint: run a test with a RedTeamAgent first, or pass --dir",
                file=sys.stderr,
            )
        else:
            print(
                f"error: no batches found under {reports_root}\n"
                f"hint: run a test with a RedTeamAgent first",
                file=sys.stderr,
            )
        return 2

    if shutil.which("streamlit") is None:
        print(
            "error: streamlit is not installed.\n"
            "install with: pip install streamlit plotly pandas",
            file=sys.stderr,
        )
        return 3

    # Streamlit app lives in this package.
    app_path = Path(__file__).parent / "report" / "app.py"
    if not app_path.is_file():
        print(f"error: dashboard app not found at {app_path}", file=sys.stderr)
        return 4

    cmd = [
        "streamlit", "run", str(app_path),
        "--server.port", str(args.port),
        "--server.headless", "true" if args.no_browser else "false",
        "--",
        "--batch-dir", str(batch_dir),
    ]
    env = os.environ.copy()
    # The app reads the batch via CLI arg (see app._parse_cli_batch_dir);
    # env var duplicates it as a belt-and-suspenders fallback.
    env["SCENARIO_REDTEAM_BATCH_DIR"] = str(batch_dir)

    print(f"[scenario] opening {batch_dir}")
    print(f"[scenario] dashboard: http://localhost:{args.port}")
    return subprocess.call(cmd, env=env)


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="scenario",
        description="Scenario framework command-line interface.",
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    rt = subparsers.add_parser(
        "redteam-report",
        help="Open the red-team Streamlit dashboard on a saved batch.",
        description=(
            "Launches the red-team findings dashboard on a batch of saved "
            "scenario reports. With no flags, opens the latest batch under "
            "./redteam-reports/."
        ),
    )
    rt.add_argument(
        "--dir",
        default=os.environ.get("SCENARIO_REDTEAM_REPORT_DIR", "./redteam-reports"),
        help="Root directory containing timestamped batch subdirectories. "
        "Defaults to $SCENARIO_REDTEAM_REPORT_DIR or ./redteam-reports.",
    )
    rt.add_argument(
        "--batch",
        default=None,
        help="Specific batch name (e.g. 20260414_143022) to open. "
        "Overrides --latest when provided.",
    )
    rt.add_argument(
        "--latest",
        type=int,
        default=1,
        help="Open the Nth-most-recent batch (1 = latest, 2 = second latest, ...). "
        "Default: 1.",
    )
    rt.add_argument(
        "--port",
        type=int,
        default=int(os.environ.get("SCENARIO_REDTEAM_PORT", "8501")),
        help="Streamlit port. Default: 8501 (or $SCENARIO_REDTEAM_PORT).",
    )
    rt.add_argument(
        "--no-browser",
        action="store_true",
        help="Don't auto-open a browser tab (headless mode, useful for SSH).",
    )
    rt.set_defaults(func=_cmd_redteam_report)

    return parser


def main(argv: Optional[list[str]] = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)
    return args.func(args)


if __name__ == "__main__":
    sys.exit(main())

Functions

def main(argv: Optional[list[str]] = None) ‑> int
Expand source code
def main(argv: Optional[list[str]] = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)
    return args.func(args)