openrange-gym / app.py
blocks3k's picture
OpenRange proxy via bore tunnel
f0926cb verified
"""OpenRange HF Space β€” Gradio frontend proxying to local Kind/Helm backend."""
import json
import os
import gradio as gr
import httpx
BACKEND_URL = os.environ.get("BACKEND_URL", "").rstrip("/")
print(f"[openrange] BACKEND_URL = {BACKEND_URL[:50]}..." if BACKEND_URL else "[openrange] WARNING: BACKEND_URL not set")
def _get(path: str, timeout: float = 60.0) -> str:
if not BACKEND_URL:
return '{"error": "BACKEND_URL not configured"}'
try:
r = httpx.get(f"{BACKEND_URL}/{path}", timeout=timeout)
return r.text
except Exception as e:
return json.dumps({"error": f"{type(e).__name__}: {e}"})
def _post(path: str, body: dict | None = None, timeout: float = 120.0) -> str:
if not BACKEND_URL:
return '{"error": "BACKEND_URL not configured"}'
try:
r = httpx.post(f"{BACKEND_URL}/{path}", json=body or {}, timeout=timeout)
return r.text
except Exception as e:
return json.dumps({"error": f"{type(e).__name__}: {e}"})
def _pretty(text: str) -> str:
try:
return json.dumps(json.loads(text), indent=2)
except Exception:
return text
# ── Callbacks ──────────────────────────────────────────────────────────────
def check_health():
return _pretty(_get("health"))
def get_metadata():
return _pretty(_get("metadata"))
def get_state():
return _pretty(_get("state"))
def get_console_snapshot():
return _pretty(_get("console/api/snapshot"))
def get_console_episode():
return _pretty(_get("console/api/episode"))
def get_console_history():
return _pretty(_get("console/api/history"))
def do_reset():
return _pretty(_post("reset"))
def do_step(command: str, mode: str):
if not command.strip():
return "Enter a command first"
return _pretty(_post("step", {"command": command, "mode": mode}))
# ── UI ─────────────────────────────────────────────────────────────────────
with gr.Blocks(
title="OpenRange",
theme=gr.themes.Default(primary_hue="blue", neutral_hue="slate"),
css="""
.output textarea { font-family: 'SF Mono','Fira Code',Consolas,monospace !important;
font-size: 13px !important; }
""",
) as demo:
gr.Markdown(
"# OpenRange\n"
"Multi-agent cybersecurity gymnasium β€” proxied to local Kind/Helm backend"
)
with gr.Tab("Operator Console"):
with gr.Row():
health_btn = gr.Button("Health", size="sm")
state_btn = gr.Button("State", size="sm")
meta_btn = gr.Button("Metadata", size="sm")
reset_btn = gr.Button("Reset Environment", variant="primary", size="sm")
info_out = gr.Textbox(label="Response", lines=8, interactive=False, elem_classes="output")
health_btn.click(fn=check_health, outputs=info_out)
state_btn.click(fn=get_state, outputs=info_out)
meta_btn.click(fn=get_metadata, outputs=info_out)
reset_btn.click(fn=do_reset, outputs=info_out)
gr.Markdown("---")
gr.Markdown("### Snapshot & Episode")
with gr.Row():
snap_btn = gr.Button("Snapshot Info", size="sm")
ep_btn = gr.Button("Episode Info", size="sm")
hist_btn = gr.Button("Action History", size="sm")
console_out = gr.Textbox(label="Console Data", lines=10, interactive=False, elem_classes="output")
snap_btn.click(fn=get_console_snapshot, outputs=console_out)
ep_btn.click(fn=get_console_episode, outputs=console_out)
hist_btn.click(fn=get_console_history, outputs=console_out)
with gr.Tab("Execute Step"):
gr.Markdown("Send a command to the range as Red (attacker) or Blue (defender).")
with gr.Row():
cmd_input = gr.Textbox(
label="Command",
placeholder="nmap -sV 10.0.1.0/24",
scale=4,
)
mode_input = gr.Dropdown(
choices=["red", "blue"],
value="red",
label="Mode",
scale=1,
)
step_btn = gr.Button("Execute", variant="primary")
step_out = gr.Textbox(label="Observation", lines=15, interactive=False, elem_classes="output")
step_btn.click(fn=do_step, inputs=[cmd_input, mode_input], outputs=step_out)
with gr.Tab("API Info"):
gr.Markdown(f"""
### Backend URL
`{BACKEND_URL or 'NOT SET β€” configure BACKEND_URL Space variable'}`
### OpenEnv Endpoints (proxied)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/health` | Backend health check |
| GET | `/metadata` | Environment metadata |
| GET | `/schema` | Action/Observation JSON schemas |
| POST | `/reset` | Reset environment (select new snapshot) |
| POST | `/step` | Execute an action |
| GET | `/state` | Current episode state |
| GET | `/console/` | Operator debug console |
### Architecture
```
You (browser) -> HF Space (this Gradio app) -> bore tunnel -> local machine -> Kind cluster
```
""")
demo.launch()