Skip to content
Open
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
53 changes: 52 additions & 1 deletion rogue/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from rogue import __version__ # noqa: F401

from .common.logging.config import configure_logger
from .common.network import get_host_for_url
from .common.tui_installer import RogueTuiInstaller
from .common.update_checker import check_for_updates
from .run_cli import run_cli, set_cli_args
Expand Down Expand Up @@ -95,6 +96,13 @@ def parse_args() -> Namespace:
help="Run in non-interactive CLI mode",
parents=[common_parser()],
)
cli_parser.add_argument(
"--with-server",
action="store_true",
default=False,
help="Start the rogue server alongside the CLI",
)
set_server_args(cli_parser)
set_cli_args(cli_parser)

# TUI mode
Expand Down Expand Up @@ -263,7 +271,50 @@ def main() -> None:
if args.mode == "server":
run_server(args, background=False)
elif args.mode == "cli":
exit_code = asyncio.run(run_cli(args))
server_process = None
if args.with_server:
try:
server_process = run_server(
args,
background=True,
)
except Exception as e:
logger.error(f"Failed to start rogue server: {e}")
sys.exit(1)
if not server_process:
logger.error("Failed to start rogue server. Exiting.")
sys.exit(1)
client_host = get_host_for_url(args.host)
args.rogue_server_url = f"http://{client_host}:{args.port}"

# Auto-configure CLI args for examples if not specified
if args.example:
if not args.evaluated_agent_url:
args.evaluated_agent_url = (
f"http://{args.example_host}:{args.example_port}"
)

if args.example == "tshirt_store_langgraph_mcp":
from rogue_sdk.types import Protocol, Transport

if args.protocol == Protocol.A2A:
args.protocol = Protocol.MCP
if args.transport is None:
args.transport = Transport.STREAMABLE_HTTP

exit_code = 1
try:
exit_code = asyncio.run(run_cli(args))
except KeyboardInterrupt:
logger.info("Keyboard interrupt received. Exiting.")
exit_code = 0
except Exception as e:
logger.error(f"CLI execution failed: {e}")
exit_code = 1
finally:
if server_process:
server_process.terminate()
server_process.join()
sys.exit(exit_code)
elif args.mode == "tui":
if not RogueTuiInstaller().install_rogue_tui():
Expand Down
21 changes: 21 additions & 0 deletions rogue/common/network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from ipaddress import ip_address


def get_host_for_url(host: str) -> str:
"""
Normalize a host string for use in a URL.

- Converts 0.0.0.0/:: to 127.0.0.1 (for client connection)
- Wraps IPv6 addresses in brackets: [::1]
- Leaves hostnames unchanged
"""
if host in ("0.0.0.0", "::"): # nosec B104
return "127.0.0.1"

try:
if ip_address(host).version == 6:
return f"[{host}]"
except ValueError:
pass # hostname

return host
16 changes: 6 additions & 10 deletions rogue/run_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import os
import time
from argparse import ArgumentParser, Namespace
from ipaddress import ip_address

from pathlib import Path

import psutil
import requests
from loguru import logger

from .common.network import get_host_for_url
from .server.main import start_server


Expand Down Expand Up @@ -94,15 +95,7 @@ def wait_until_server_ready(
# Double-check with HTTP request to ensure the server is fully ready
try:
# Normalize host for HTTP requests (handle 0.0.0.0/:: and IPv6)
http_host = host
if host in ("0.0.0.0", "::"): # nosec B104
http_host = "127.0.0.1"
else:
try:
if ip_address(host).version == 6:
http_host = f"[{host}]"
except ValueError:
pass # hostname, leave as-is
http_host = get_host_for_url(host)

response = requests.get(
f"http://{http_host}:{port}/api/v1/health",
Expand Down Expand Up @@ -145,6 +138,9 @@ def run_server(
)
if background_wait_for_ready:
if not wait_until_server_ready(process, host, port):
# Terminate the orphan process before raising
process.terminate()
process.join(timeout=5)
raise Exception("Server failed to start")
logger.info("Rogue server ready", extra={"host": host, "port": port})
return process
Expand Down
26 changes: 26 additions & 0 deletions rogue/tests/common/test_network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from rogue.common.network import get_host_for_url


def test_get_host_for_url_ipv4():
assert get_host_for_url("127.0.0.1") == "127.0.0.1"
assert get_host_for_url("192.168.1.1") == "192.168.1.1"


def test_get_host_for_url_hostname():
assert get_host_for_url("localhost") == "localhost"
assert get_host_for_url("example.com") == "example.com"


def test_get_host_for_url_ipv6():
assert get_host_for_url("::1") == "[::1]"
assert get_host_for_url("2001:db8::1") == "[2001:db8::1]"
assert (
get_host_for_url("[::1]") == "[::1]"
) # Already bracketed? No, ip_address might fail or return IPv6.
# ip_address("[::1]") raises ValueError. So it returns input as is (hostname).
# That is acceptable behavior for already bracketed input if considered a hostname.


def test_get_host_for_url_wildcard():
assert get_host_for_url("0.0.0.0") == "127.0.0.1"
assert get_host_for_url("::") == "127.0.0.1"
Loading