From e50d1655ddabfee90cdc9b2569d14f359b3a36a7 Mon Sep 17 00:00:00 2001 From: WODE25500 Date: Mon, 24 Aug 2026 10:46:20 +0800 Subject: [PATCH 1/4] fix(webui): security hardening + launch theme crash; add CI - Default bind 127.0.0.1 (was 0.0.0.0); warn to stderr on a non-localhost bind. - Move gradio theme onto gr.Blocks (fixes a launch crash: launch() has no theme param). - Guard empty out_dir in scan_outputs. - Add .github/workflows/ci.yml (test py 3.10/3.11/3.12 + docs mkdocs --strict). --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++++++ skillopt_webui/app.py | 18 +++++++++++++++--- 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c8e39f0c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install + run: pip install -e ".[dev]" + - name: Pytest + run: python -m pytest -q + env: + # Live backend/transcript tests are opt-in (gated on == "1"); keep + # them off in CI so the run stays hermetic and fast. + SKILLOPT_TEST_REAL_OPENCODE: "0" + SKILLOPT_TEST_REAL_OPENCODE_SOURCE: "0" + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install + run: pip install -e ".[docs]" + - name: Build docs (strict) + run: mkdocs build --strict diff --git a/skillopt_webui/app.py b/skillopt_webui/app.py index e4978c5f..63aa9b4d 100644 --- a/skillopt_webui/app.py +++ b/skillopt_webui/app.py @@ -470,6 +470,7 @@ def build_ui(): with gr.Blocks( title="SkillOpt WebUI", + theme=gr.themes.Soft(primary_hue="indigo"), ) as app: gr.Markdown("# 🧠 SkillOpt Training Dashboard") gr.Markdown("*SKILLOPT: Executive Strategy for Self-Evolving Agent Skills — Configure, launch, and monitor training.*") @@ -598,6 +599,8 @@ def on_refresh(): def scan_outputs(out_dir): rows = [] + if not out_dir: + return rows base = PROJECT_ROOT / out_dir if not base.exists(): return rows @@ -643,16 +646,25 @@ def main(): parser = argparse.ArgumentParser(description="SkillOpt WebUI") parser.add_argument("--port", type=int, default=7860) parser.add_argument("--share", action="store_true") - parser.add_argument("--host", type=str, default="0.0.0.0", - help="Server host. Use 0.0.0.0 for public access.") + parser.add_argument("--host", type=str, default="127.0.0.1", + help="Server host. Default is localhost; use 0.0.0.0 " + "to expose publicly (no auth, use with care).") args = parser.parse_args() + if args.host and args.host not in ("127.0.0.1", "localhost", "::1"): + print( + f"⚠ warning: binding SkillOpt WebUI on {args.host} with no auth " + "exposes the Output Explorer (reads any path you type) and the " + "training controls to reachable clients. Prefer --host 127.0.0.1; " + "use 0.0.0.0 only if you understand the risk.", + file=sys.stderr, + ) + app = build_ui() app.launch( server_name=args.host, server_port=args.port, share=args.share, - theme=gr.themes.Soft(primary_hue="indigo"), ) From 623dff30715aead0bc5379ac53eebbfa44a672b8 Mon Sep 17 00:00:00 2001 From: WODE25500 Date: Mon, 24 Aug 2026 10:57:03 +0800 Subject: [PATCH 2/4] test(webui): cover localhost default + public-bind warning --- tests/test_webui_security.py | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_webui_security.py diff --git a/tests/test_webui_security.py b/tests/test_webui_security.py new file mode 100644 index 00000000..5886da65 --- /dev/null +++ b/tests/test_webui_security.py @@ -0,0 +1,57 @@ +"""Tests for the SkillOpt WebUI security posture (bind default + public warning). + +The WebUI is gradio-coupled, so we inject a minimal fake ``gradio`` module and +mock ``build_ui``/``launch`` to exercise ``main()``'s argparse + host-check +logic without the heavy ``webui`` extra. +""" + +from __future__ import annotations + +import sys +import types +import unittest.mock as mock + +import pytest + + +@pytest.fixture +def webui(monkeypatch): + fake_gradio = types.ModuleType("gradio") + fake_gradio.themes = types.SimpleNamespace(Soft=lambda **kw: mock.MagicMock()) + monkeypatch.setitem(sys.modules, "gradio", fake_gradio) + import skillopt_webui.app as app + + return app + + +def test_main_defaults_host_to_localhost(webui, monkeypatch): + """The server must not be publicly bound by default.""" + webui_mod = webui + launcher = mock.MagicMock() + app_mock = mock.MagicMock() + app_mock.launch = launcher + monkeypatch.setattr(webui_mod, "build_ui", lambda: app_mock) + monkeypatch.setattr(sys, "argv", ["app.py"]) + + webui_mod.main() + + launcher.assert_called_once() + _args, kwargs = launcher.call_args + assert kwargs["server_name"] == "127.0.0.1" + + +def test_main_warns_on_public_host(webui, monkeypatch, capsys): + """An explicit public bind must emit an unauthenticated-exposure warning.""" + webui_mod = webui + launcher = mock.MagicMock() + app_mock = mock.MagicMock() + app_mock.launch = launcher + monkeypatch.setattr(webui_mod, "build_ui", lambda: app_mock) + monkeypatch.setattr(sys, "argv", ["app.py", "--host", "0.0.0.0"]) + + webui_mod.main() + + captured = capsys.readouterr() + assert "warning" in captured.err.lower() + _args, kwargs = launcher.call_args + assert kwargs["server_name"] == "0.0.0.0" From bed1620145eeceae3de821427bd1faf07ca3a9e7 Mon Sep 17 00:00:00 2001 From: WODE25500 Date: Mon, 24 Aug 2026 17:56:19 +0800 Subject: [PATCH 3/4] fix(webui): gradio theme version-compat + real launch smoke test Address maintainer review on #249: - Place the gradio theme on Blocks for Gradio <=5 and on launch() for Gradio 6, detected via the installed major, so the WebUI works on any supported version without an ignored-argument warning or a TypeError. - Add a real Gradio build/launch smoke test (skips without the webui extra) asserting the theme is actually applied and no error is raised. --- skillopt_webui/app.py | 24 +++++++++++++--------- tests/test_webui_build_gradio.py | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 tests/test_webui_build_gradio.py diff --git a/skillopt_webui/app.py b/skillopt_webui/app.py index 63aa9b4d..88331089 100644 --- a/skillopt_webui/app.py +++ b/skillopt_webui/app.py @@ -24,6 +24,11 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent +# Gradio moved where `theme` lives across versions: <=5 uses `Blocks(theme=...)`, +# >=6 moved it to `launch()`. Detect the installed major so the WebUI works on +# any supported version without an ignored-argument warning or a TypeError. +_GRADIO_MAJOR = int((getattr(gr, "__version__", "4.0").split(".")[0]) or 4) + # ─── Config helpers ────────────────────────────────────────────────────────── @@ -468,10 +473,10 @@ def render_pipeline_html(active_stage: str = "") -> str: def build_ui(): configs = discover_configs() - with gr.Blocks( - title="SkillOpt WebUI", - theme=gr.themes.Soft(primary_hue="indigo"), - ) as app: + _blocks_kwargs = {"title": "SkillOpt WebUI"} + if _GRADIO_MAJOR < 6: + _blocks_kwargs["theme"] = gr.themes.Soft(primary_hue="indigo") + with gr.Blocks(**_blocks_kwargs) as app: gr.Markdown("# 🧠 SkillOpt Training Dashboard") gr.Markdown("*SKILLOPT: Executive Strategy for Self-Evolving Agent Skills — Configure, launch, and monitor training.*") @@ -661,11 +666,12 @@ def main(): ) app = build_ui() - app.launch( - server_name=args.host, - server_port=args.port, - share=args.share, - ) + launch_kwargs = dict(server_name=args.host, server_port=args.port, share=args.share) + if _GRADIO_MAJOR >= 6: + # Gradio 6 moved the theme to launch(); applying it here avoids an + # ignored-argument warning. + launch_kwargs["theme"] = gr.themes.Soft(primary_hue="indigo") + app.launch(**launch_kwargs) if __name__ == "__main__": diff --git a/tests/test_webui_build_gradio.py b/tests/test_webui_build_gradio.py new file mode 100644 index 00000000..e7972fbf --- /dev/null +++ b/tests/test_webui_build_gradio.py @@ -0,0 +1,34 @@ +"""Real Gradio build/launch smoke test (requires the `webui` extra). + +Verifies the WebUI builds and launches on the installed Gradio without a +TypeError or ignored-argument warning, and that the selected theme is actually +applied for the installed major version. +""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("gradio") + +import gradio as gr # noqa: E402 + +import skillopt_webui.app as app # noqa: E402 + + +def test_webui_builds_and_launches_theme(): + ui = app.build_ui() + launch_kwargs = {"prevent_thread_lock": True} + if app._GRADIO_MAJOR >= 6: + # Gradio 6 applies theme at launch(); add it here and assert applied. + launch_kwargs["theme"] = gr.themes.Soft(primary_hue="indigo") + ui.launch(**launch_kwargs) + try: + assert ui.theme is not None, "theme was not applied on the launched app" + finally: + ui.close() + + +def test_gradio_major_detected(): + # The constant must reflect the installed Gradio major (6 for 6.25). + assert app._GRADIO_MAJOR == int(gr.__version__.split(".")[0]) From ea94bba0f24b5bdd94ac2f7f6b7dcc513fcd7b5e Mon Sep 17 00:00:00 2001 From: WODE25500 Date: Mon, 24 Aug 2026 18:23:34 +0800 Subject: [PATCH 4/4] test(webui): robust real-Gradio build/launch smoke across versions Verified against real Gradio 4.44, 5.50, and 6.25 (built sequentially to avoid conflicting pins): - build_ui() succeeds on all three; theme is placed on Blocks for <6 and on launch() for >=6 (no TypeError / ignored-arg warning). - The launch smoke skips cleanly when a headless/sandboxed environment blocks localhost (not a compatibility bug), and asserts theme application when it launches. --- tests/test_webui_build_gradio.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/test_webui_build_gradio.py b/tests/test_webui_build_gradio.py index e7972fbf..b1e9bc5d 100644 --- a/tests/test_webui_build_gradio.py +++ b/tests/test_webui_build_gradio.py @@ -1,8 +1,8 @@ """Real Gradio build/launch smoke test (requires the `webui` extra). -Verifies the WebUI builds and launches on the installed Gradio without a -TypeError or ignored-argument warning, and that the selected theme is actually -applied for the installed major version. +Verifies the WebUI builds (and launches where the environment allows) on the +installed Gradio without a TypeError or ignored-argument warning, and that the +theme is placed on the right object for the installed major version. """ from __future__ import annotations @@ -16,13 +16,28 @@ import skillopt_webui.app as app # noqa: E402 +def test_webui_builds_theme_on_blocks(): + """build_ui() must not raise; for Gradio <6 the theme lives on Blocks.""" + ui = app.build_ui() + assert ui is not None + if app._GRADIO_MAJOR < 6: + assert ui.theme is not None, "theme was not set on Blocks for Gradio <6" + + def test_webui_builds_and_launches_theme(): ui = app.build_ui() launch_kwargs = {"prevent_thread_lock": True} if app._GRADIO_MAJOR >= 6: - # Gradio 6 applies theme at launch(); add it here and assert applied. + # Gradio 6 applies theme at launch(); we add it here and assert applied. launch_kwargs["theme"] = gr.themes.Soft(primary_hue="indigo") - ui.launch(**launch_kwargs) + try: + ui.launch(**launch_kwargs) + except ValueError as exc: + # Headless/sandboxed environments may not expose localhost; that is an + # environment limitation, not a theme-compatibility bug. + if "localhost is not accessible" in str(exc): + pytest.skip("headless environment blocks localhost launch") + raise try: assert ui.theme is not None, "theme was not applied on the launched app" finally: @@ -30,5 +45,5 @@ def test_webui_builds_and_launches_theme(): def test_gradio_major_detected(): - # The constant must reflect the installed Gradio major (6 for 6.25). + # The constant must reflect the installed Gradio major. assert app._GRADIO_MAJOR == int(gr.__version__.split(".")[0])