e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Auto-shutdown for an exposed first-run Studio whose admin password is unchanged.
|
|
|
|
On a fresh install the seeded bootstrap admin password stays a valid login
|
|
credential until first login changes it. When the web UI is put on the network
|
|
(``--secure`` / ``0.0.0.0``) and nobody completes that first-login change within
|
|
a deadline, tear Studio down so a fresh, unconfigured instance does not stay
|
|
publicly reachable indefinitely. If the password was changed, Studio keeps
|
|
running.
|
|
|
|
Scope: web UI launches only (never ``--api-only``, which authenticates by API
|
|
key rather than the admin password, and never Colab). Configurable via
|
|
``UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT`` (seconds; default 3600; ``0`` disables).
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import threading
|
|
|
|
BOOTSTRAP_TIMEOUT_ENV_VAR = "UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT"
|
|
DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS = 3600
|
|
|
|
|
|
def bootstrap_timeout_seconds(env = None) -> int:
|
|
"""Resolve the deadline in seconds. ``0`` (or invalid/negative) disables it.
|
|
|
|
A malformed value falls back to the default rather than disabling, so a typo
|
|
cannot silently remove the protection.
|
|
"""
|
|
env = os.environ if env is None else env
|
|
raw = env.get(BOOTSTRAP_TIMEOUT_ENV_VAR)
|
|
if raw is None or raw.strip() == "":
|
|
return DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS
|
|
try:
|
|
value = int(raw)
|
|
except ValueError:
|
|
return DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS
|
|
return value if value > 0 else 0
|
|
|
|
|
|
def _is_exposed_bind(host: str, secure: bool) -> bool:
|
|
"""True when this launch puts the web UI on the network (tunnel or non-loopback)."""
|
|
if secure:
|
|
return True
|
|
if host in ("0.0.0.0", "::"):
|
|
return True
|
|
try:
|
|
from utils.host_policy import is_external_host
|
|
except Exception:
|
|
return False
|
|
return bool(is_external_host(host))
|
|
|
|
|
|
def should_arm_bootstrap_timeout(
|
|
*,
|
|
host: str,
|
|
secure: bool,
|
|
api_only: bool,
|
|
frontend_served: bool,
|
|
is_colab: bool,
|
|
requires_change: bool,
|
|
timeout_seconds: int,
|
|
) -> bool:
|
|
"""Whether to arm the deadline: only for an exposed web UI whose seeded admin
|
|
password is still unchanged. Pure decision (no I/O) for cheap unit testing."""
|
|
if timeout_seconds <= 0:
|
|
return False
|
|
if api_only or not frontend_served or is_colab:
|
|
return False
|
|
if not requires_change:
|
|
return False
|
|
return _is_exposed_bind(host, secure)
|
|
|
|
|
|
def _format_duration(seconds: int) -> str:
|
|
"""Human-friendly duration for the shutdown message (seconds under a minute)."""
|
|
|
|
def _plural(n: int, unit: str) -> str:
|
|
return f"{n} {unit}{'' if n == 1 else 's'}"
|
|
|
|
if seconds < 60:
|
|
return _plural(seconds, "second")
|
|
minutes, rem = divmod(seconds, 60)
|
|
label = _plural(minutes, "minute")
|
|
if rem:
|
|
label += f" {_plural(rem, 'second')}"
|
|
return label
|
|
|
|
|
|
def enforce_bootstrap_password_deadline(
|
|
storage,
|
|
trigger_shutdown,
|
|
*,
|
|
timeout_seconds: int,
|
|
logger = None,
|
|
) -> bool:
|
|
"""Deadline handler: shut down iff the seeded admin password is still unchanged.
|
|
|
|
Returns True if it shut Studio down, False if it left it running (the
|
|
password was changed in time).
|
|
"""
|
|
try:
|
|
still_default = storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME)
|
|
except Exception:
|
|
return False
|
|
if not still_default:
|
|
return False # password changed in time -> leave Studio running
|
|
|
|
message = (
|
|
"\nUnsloth Studio was exposed on the network but its default admin "
|
|
f"password was not changed within {_format_duration(timeout_seconds)}. "
|
|
"Shutting down to avoid leaving an unsecured public instance running.\n"
|
|
"Next time, sign in and change the password on first login, or set "
|
|
f"{BOOTSTRAP_TIMEOUT_ENV_VAR}=0 to disable this timeout."
|
|
)
|
|
if logger is not None:
|
|
logger.warning(message)
|
|
print(message, file = sys.stderr, flush = True)
|
|
try:
|
|
trigger_shutdown()
|
|
except Exception as e: # shutdown is best-effort; never raise from the timer
|
|
if logger is not None:
|
|
logger.warning("Bootstrap-timeout shutdown failed: %s", e)
|
|
return True
|
|
|
|
|
|
def arm_bootstrap_timeout(
|
|
storage,
|
|
trigger_shutdown,
|
|
*,
|
|
timeout_seconds: int,
|
|
logger = None,
|
|
) -> "threading.Timer":
|
|
"""Start a daemon timer that enforces the deadline. Returns the Timer."""
|
|
timer = threading.Timer(
|
|
timeout_seconds,
|
|
enforce_bootstrap_password_deadline,
|
|
args = (storage, trigger_shutdown),
|
|
kwargs = {"timeout_seconds": timeout_seconds, "logger": logger},
|
|
)
|
|
timer.daemon = True
|
|
timer.start()
|
|
return timer
|