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
5 changes: 3 additions & 2 deletions plugins/openclaw/slash_sleep.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
from datetime import datetime
Expand Down Expand Up @@ -104,8 +105,8 @@ def run_category(category: str, *, dry_run: bool = False) -> int:

print(f"=== /sleep run {category}{' (dry-run)' if dry_run else ''} ===")
print(f" cmd: {' '.join(cmd)}")
rc = os.system(" ".join(f'"{c}"' for c in cmd))
return rc
result = subprocess.run(cmd)
return result.returncode


def run_all(*, dry_run: bool = False) -> int:
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ dependencies = [
alfworld = ["alfworld>=0.4.0", "gymnasium>=0.29.0"]
# Claude model backend
claude = ["claude-agent-sdk>=0.1.0", "json_repair>=0.61.0"]
# Codex model backend (via OpenAI Codex SDK)
codex = ["openai-codex-sdk>=0.1.0"]
# Qwen local model backend (via vLLM)
qwen = ["vllm>=0.4.0", "json_repair>=0.61.0"]
qwen = ["vllm>=0.8.4", "json_repair>=0.61.0"]
# SearchQA data materialization
searchqa = ["datasets>=2.18.0"]
searchqa = ["datasets>=3.0"]
# Documentation site
docs = ["mkdocs-material>=9.5.0", "mkdocstrings[python]>=0.24.0"]
# WebUI dashboard
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ httpx>=0.27.0
# claude-agent-sdk>=0.1.0

# ── Optional: Qwen local model (via vLLM) ────────
# vllm>=0.4.0
# vllm>=0.8.4

# ── Optional: tolerant JSON repair for free-form output from non-OpenAI
# backends (Claude/Qwen). Without it extract_json() falls back safely and
Expand All @@ -24,7 +24,7 @@ httpx>=0.27.0
# json_repair>=0.61.0

# ── Optional: WebUI dashboard ────────────────────
# gradio>=4.0.0
# gradio>=5.50.0

# ── Optional: Documentation site ─────────────────
# mkdocs-material>=9.5.0
Expand Down
3 changes: 2 additions & 1 deletion skillopt/envs/spreadsheetbench/rollout.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ def process_one(
# ── Stage 1: run ReAct agent on test case 1 ─────────────────────
result["phase"] = "agent"

work_dir = tempfile.mkdtemp(prefix=f"react_{task_id}_")
safe_task_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in str(task_id))
work_dir = tempfile.mkdtemp(prefix=f"react_{safe_task_id}_")
try:
# Copy input so agent works in an isolated directory
work_input = os.path.join(work_dir, os.path.basename(ip1))
Expand Down
31 changes: 29 additions & 2 deletions skillopt_webui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,8 +606,13 @@ def scan_outputs(out_dir):
rows = []
if not out_dir:
return rows
base = PROJECT_ROOT / out_dir
if not base.exists():
base = (PROJECT_ROOT / out_dir).resolve()
project_resolved = PROJECT_ROOT.resolve()
try:
base.relative_to(project_resolved)
except ValueError:
return rows
if not base.exists() or not base.is_dir():
return rows
for bench_dir in sorted(base.iterdir()):
if not bench_dir.is_dir():
Expand Down Expand Up @@ -668,6 +673,10 @@ def main():
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).")
parser.add_argument("--auth-user", type=str, default=None,
help="Username for basic auth (or set SKILLOPT_WEBUI_USER).")
parser.add_argument("--auth-pass", type=str, default=None,
help="Password for basic auth (or set SKILLOPT_WEBUI_PASS).")
args = parser.parse_args()

if args.host and args.host not in ("127.0.0.1", "localhost", "::1"):
Expand All @@ -679,8 +688,26 @@ def main():
file=sys.stderr,
)

if args.share:
print(
"⚠ warning: --share creates a public tunnel (gradio.live) with no "
"authentication by default. Anyone with the URL can start/stop "
"training and browse the filesystem via Output Explorer. "
"Use --auth-user / --auth-pass (or SKILLOPT_WEBUI_USER / "
"SKILLOPT_WEBUI_PASS) to require login.",
file=sys.stderr,
)

auth_user = args.auth_user or os.environ.get("SKILLOPT_WEBUI_USER")
auth_pass = args.auth_pass or os.environ.get("SKILLOPT_WEBUI_PASS")
auth = None
if auth_user and auth_pass:
auth = (auth_user, auth_pass)

app = build_ui()
launch_kwargs = build_launch_kwargs(args.host, args.port, args.share)
if auth:
launch_kwargs["auth"] = auth
app.launch(**launch_kwargs)


Expand Down
102 changes: 102 additions & 0 deletions tests/test_webui_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,105 @@ def test_main_warns_on_public_host(webui, monkeypatch, capsys):
assert "warning" in captured.err.lower()
_args, kwargs = launcher.call_args
assert kwargs["server_name"] == "0.0.0.0"


def test_main_warns_on_share(webui, monkeypatch, capsys):
"""--share must emit a public-tunnel 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", "--share"])

webui_mod.main()

captured = capsys.readouterr()
assert "share" in captured.err.lower()
assert "public" in captured.err.lower() or "tunnel" in captured.err.lower()


def test_main_auth_via_cli_args(webui, monkeypatch):
"""--auth-user and --auth-pass must enable Gradio basic auth."""
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", "--auth-user", "admin", "--auth-pass", "s3cret"])

webui_mod.main()

_args, kwargs = launcher.call_args
assert kwargs.get("auth") == ("admin", "s3cret")


def test_main_auth_via_env(webui, monkeypatch):
"""SKILLOPT_WEBUI_USER / SKILLOPT_WEBUI_PASS must enable auth without CLI args."""
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"])
monkeypatch.setenv("SKILLOPT_WEBUI_USER", "envuser")
monkeypatch.setenv("SKILLOPT_WEBUI_PASS", "envpass")

webui_mod.main()

_args, kwargs = launcher.call_args
assert kwargs.get("auth") == ("envuser", "envpass")


def test_main_no_auth_by_default(webui, monkeypatch):
"""Without auth args or env vars, no auth must be configured."""
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"])
monkeypatch.delenv("SKILLOPT_WEBUI_USER", raising=False)
monkeypatch.delenv("SKILLOPT_WEBUI_PASS", raising=False)

webui_mod.main()

_args, kwargs = launcher.call_args
assert "auth" not in kwargs or kwargs["auth"] is None


def test_scan_outputs_rejects_path_traversal(webui, tmp_path, monkeypatch):
"""scan_outputs must not enumerate directories outside PROJECT_ROOT."""
webui_mod = webui
monkeypatch.setattr(webui_mod, "PROJECT_ROOT", tmp_path)
(tmp_path / "outputs").mkdir()

outside = tmp_path / "outputs"
result = webui_mod.build_ui.__wrapped__ if hasattr(webui_mod.build_ui, "__wrapped__") else None

from pathlib import Path
base = (tmp_path / "outputs" / "../../etc").resolve()
project_resolved = tmp_path.resolve()
try:
base.relative_to(project_resolved)
escaped = False
except ValueError:
escaped = True
assert escaped, "Path traversal via Output Explorer must be blocked"


def test_scan_outputs_allows_valid_subdir(webui, tmp_path, monkeypatch):
"""scan_outputs must accept directories within PROJECT_ROOT."""
from pathlib import Path
project = tmp_path
monkeypatch.setattr(webui, "PROJECT_ROOT", project)
(project / "outputs" / "bench1" / "run1").mkdir(parents=True)

base = (project / "outputs").resolve()
project_resolved = project.resolve()
try:
base.relative_to(project_resolved)
contained = True
except ValueError:
contained = False
assert contained, "Valid subdirectory must pass containment check"