Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rogue-ai"
version = "0.1.3"
version = "0.1.4"
description = "Rogue agent evaluator by Qualifire"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion rogue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@
]

# Version info
__version__ = "0.1.3"
__version__ = "0.1.4"
__author__ = "Qualifire"
__description__ = "Library for evaluating AI agents against scenarios"
15 changes: 15 additions & 0 deletions rogue/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@

from .common.logging.config import configure_logger
from .common.tui_installer import RogueTuiInstaller
from .common.update_checker import check_for_updates
from .run_cli import run_cli, set_cli_args
from .run_server import run_server, set_server_args
from .run_tui import run_rogue_tui
from .run_ui import run_ui, set_ui_args
from . import __version__


load_dotenv()

Expand All @@ -31,6 +34,13 @@ def common_parser() -> ArgumentParser:
default=False,
help="Enable debug logging",
)
parent_parser.add_argument(
"--version",
action="store_true",
default=False,
help="Show version",
)

return parent_parser


Expand Down Expand Up @@ -79,6 +89,10 @@ def parse_args() -> Namespace:
def main() -> None:
args = parse_args()

if args.version:
print(f"Rogue AI version: {__version__}")
sys.exit(0)

tui_mode = args.mode == "tui" or args.mode is None

log_file_path: Path | None = None
Expand Down Expand Up @@ -142,4 +156,5 @@ def main() -> None:


if __name__ == "__main__":
check_for_updates(__version__)
main()
92 changes: 57 additions & 35 deletions rogue/common/tui_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import platformdirs
import requests
from loguru import logger
from rich.console import Console


class RogueTuiInstaller:
Expand Down Expand Up @@ -43,15 +44,23 @@ def _os(self) -> str:

def _get_latest_github_release(self) -> Optional[dict]:
"""Get the latest release information from GitHub."""
console = Console()

try:
url = f"https://api.github.com/repos/{self._repo}/releases/latest"
response = requests.get(
url,
timeout=10,
headers=self._headers,
)
response.raise_for_status()
return response.json()

with console.status(
"[bold blue]Fetching latest release information...",
spinner="dots",
):
response = requests.get(
url,
timeout=10,
headers=self._headers,
verify=False, # nosec: B501
)
response.raise_for_status()
return response.json()
except Exception:
logger.exception("Error fetching latest release")
return None
Expand All @@ -76,6 +85,8 @@ def _find_asset_for_platform(
return None

def _download_rogue_tui_to_temp(self) -> str:
console = Console()

# Get latest release
release_data = self._get_latest_github_release()
if not release_data:
Expand All @@ -90,25 +101,29 @@ def _download_rogue_tui_to_temp(self) -> str:
)
raise Exception("No suitable binary found for current platform.")

logger.info(f"Downloading: {download_url}")

response = requests.get(
download_url,
timeout=60,
headers={
"Accept": "application/octet-stream",
**self._headers,
},
)
response.raise_for_status()
# Show spinner during download
with console.status(
"[bold green]Downloading rogue-tui binary...",
spinner="dots",
):
response = requests.get(
download_url,
timeout=60,
headers={
"Accept": "application/octet-stream",
**self._headers,
},
verify=False, # nosec: B501
)
response.raise_for_status()

Comment on lines +104 to 119
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Stream download and write to same-volume temp file; avoid verify=False and large memory spike.

  • Avoid reading the whole asset into memory.
  • Write into a temp file in the target directory to enable atomic replace later.
  • Keep TLS verification enabled.

As per coding guidelines

Apply:

-        with console.status(
+        with console.status(
             "[bold green]Downloading rogue-tui binary...",
             spinner="dots",
         ):
-            response = requests.get(
-                download_url,
-                timeout=60,
-                headers={
-                    "Accept": "application/octet-stream",
-                    **self._headers,
-                },
-                verify=False,  # nosec: B501
-            )
-            response.raise_for_status()
-
-            # Create a temporary file
-            with tempfile.NamedTemporaryFile(
-                delete=False,
-                suffix="-rogue-tui",
-            ) as tmp_file:
-                tmp_file.write(response.content)
-                tmp_path = tmp_file.name
+            with requests.get(
+                download_url,
+                timeout=60,
+                headers={"Accept": "application/octet-stream", **self._headers, "User-Agent": "rogue-updater"},
+                stream=True,
+            ) as response:
+                response.raise_for_status()
+                install_dir = self._get_install_path().parent
+                Path(install_dir).mkdir(parents=True, exist_ok=True)
+                with tempfile.NamedTemporaryFile(
+                    delete=False,
+                    dir=install_dir,
+                    prefix=".rogue-tui.",
+                    suffix=".tmp",
+                ) as tmp_file:
+                    for chunk in response.iter_content(chunk_size=1_048_576):
+                        if chunk:
+                            tmp_file.write(chunk)
+                    tmp_path = tmp_file.name

Optional: prefer asset["browser_download_url"] when available to avoid the API indirection requirement for Accept header.

Also applies to: 125-132

🤖 Prompt for AI Agents
In rogue/common/tui_installer.py around lines 109-124 (and similarly 125-132),
the code downloads the entire binary into memory and disables TLS verification;
change to stream the response (requests.get(..., stream=True)) with verify left
enabled (remove verify=False), call response.raise_for_status(), and write
chunks to a same-volume temporary file in the target directory (use
tempfile.NamedTemporaryFile(dir=target_dir, delete=False) or mkstemp) iterating
response.iter_content(chunk_size=8192) to avoid memory spikes; after fully
writing and flushing/fsync the temp file, atomically replace the final target
with os.replace(temp_path, final_path) and ensure the temp file is removed on
exceptions to avoid leaks; optionally use asset["browser_download_url"] when
present to skip the API Accept header.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@drorIvry streaming the file instead of keeping it all in memory sounds like a good idea

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

# Create a temporary file
with tempfile.NamedTemporaryFile(
delete=False,
suffix="-rogue-tui",
) as tmp_file:
tmp_file.write(response.content)
tmp_path = tmp_file.name
# Create a temporary file
with tempfile.NamedTemporaryFile(
delete=False,
suffix="-rogue-tui",
) as tmp_file:
tmp_file.write(response.content)
tmp_path = tmp_file.name

# Make it executable
os.chmod(tmp_path, 0o755) # nosec: B103
Expand All @@ -134,7 +149,6 @@ def _handle_path_env(self, install_dir: Path) -> None:
sep = ";"

if str(install_dir) not in os.environ.get("PATH", "").split(sep):
logger.info(f"Adding {install_dir} to PATH environment variable.")
os.environ["PATH"] += sep + str(install_dir)
# TODO update shellrc file to update the path

Expand All @@ -153,33 +167,41 @@ def _is_rogue_tui_installed(self) -> bool:
else:
return False

def install_rogue_tui(self) -> bool:
def install_rogue_tui(
self,
upgrade: bool = False,
) -> bool:
"""Install rogue-tui from GitHub releases if not already installed."""
console = Console()
# Check if rogue-tui is already available
if self._is_rogue_tui_installed():
logger.info("rogue-tui is already installed.")
if self._is_rogue_tui_installed() and not upgrade:
console.print("[green]✅ rogue-tui is already installed.[/green]")
return True

Comment on lines 170 to 180
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Upgrade path can fail on Windows: use atomic replace instead of shutil.move.

When upgrade=True and the destination exists, shutil.move can fail on Windows if the file exists/in use. Use Path.replace (os.replace) for atomic overwrite.

Apply:

-            install_path = self._get_install_path()
-            shutil.move(tmp_path, install_path)
+            install_path = self._get_install_path()
+            # Atomic overwrite (works on Windows and POSIX)
+            Path(tmp_path).replace(install_path)

Also applies to: 177-184

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@drorIvry we should test this but it sounds good

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

logger.info("rogue-tui not found. Installing from GitHub releases...")

# Get platform information
logger.info(f"Detected platform: {self._os}-{self._architecture}")
console.print(
"[yellow]📦 Installing rogue-tui from GitHub releases...[/yellow]",
)

try:
tmp_path = self._download_rogue_tui_to_temp()
except Exception:
console.print("[red]❌ Failed to download rogue-tui.[/red]")
logger.exception("Failed to download rogue-tui.")
return False

try:
# Move to final location
install_path = self._get_install_path()
shutil.move(tmp_path, install_path)

with console.status("[bold yellow]Installing rogue-tui...", spinner="dots"):
shutil.move(tmp_path, install_path)
except Exception:
console.print("[red]❌ Failed to install rogue-tui.[/red]")
logger.exception("Failed to install rogue-tui.")
return False

self._handle_path_env(install_path.parent)

logger.info(f"rogue-tui installed successfully to {install_path}")
console.print("[green]✅ rogue-tui installed successfully![/green]")
# logger.debug(f"rogue-tui installed to {install_path}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# logger.debug(f"rogue-tui installed to {install_path}")

return True
Loading
Loading