Skip to content
Merged
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
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ irm https://unsloth.ai/install.ps1 | iex

#### Launch
```bash
unsloth studio -H 0.0.0.0 -p 8888
unsloth studio -p 8888
```
> For cloud VMs or LAN access, add `-H 0.0.0.0` to bind on all interfaces.

#### Update
To update, use the same install commands as above. Or run (does not work on Windows):
Expand Down Expand Up @@ -167,7 +168,7 @@ The below advanced instructions are for Unsloth Studio. For Unsloth Core advance
git clone https://github.com/unslothai/unsloth
cd unsloth
./install.sh --local
unsloth studio -H 0.0.0.0 -p 8888
unsloth studio -p 8888
```
Then to update :
```bash
Expand All @@ -180,7 +181,7 @@ git clone https://github.com/unslothai/unsloth.git
cd unsloth
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\install.ps1 --local
unsloth studio -H 0.0.0.0 -p 8888
unsloth studio -p 8888
```
Then to update :
```bash
Expand All @@ -193,11 +194,11 @@ git clone https://github.com/unslothai/unsloth
cd unsloth
git checkout nightly
./install.sh --local
unsloth studio -H 0.0.0.0 -p 8888
unsloth studio -p 8888
```
Then to launch every time:
```bash
unsloth studio -H 0.0.0.0 -p 8888
unsloth studio -p 8888
```

#### Nightly: Windows:
Expand All @@ -208,11 +209,11 @@ cd unsloth
git checkout nightly
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\install.ps1 --local
unsloth studio -H 0.0.0.0 -p 8888
unsloth studio -p 8888
```
Then to launch every time:
```bash
unsloth studio -H 0.0.0.0 -p 8888
unsloth studio -p 8888
```

#### Uninstall
Expand Down
20 changes: 15 additions & 5 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ try {
} catch {}
exit 1
}
`$studioCommand = '& "' + `$studioExe + '" studio -H 0.0.0.0 -p ' + `$launchPort
`$studioCommand = '& "' + `$studioExe + '" studio -p ' + `$launchPort
`$launchArgs = @(
'-NoExit',
'-NoProfile',
Expand Down Expand Up @@ -1344,15 +1344,25 @@ shell.Run cmd, 0, False

New-StudioShortcuts -UnslothExePath $UnslothExe

# Launch studio automatically in interactive terminals;
# in non-interactive environments (CI, Docker) just print instructions.
# In interactive terminals, ask the user before starting Studio.
# In non-interactive environments (CI, Docker) just print instructions.
$IsInteractive = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected)
if ($IsInteractive) {
& $UnslothExe studio -H 0.0.0.0 -p 8888
Write-Host ""
$reply = Read-Host " Start Unsloth Studio now? [Y/n]"
if ([string]::IsNullOrWhiteSpace($reply) -or $reply -match '^[Yy]') {
& $UnslothExe studio -p 8888
} else {
step "launch" "to start later, run:"
substep "unsloth studio -p 8888"
substep "(add -H 0.0.0.0 to allow network / cloud access)"
Write-Host ""
}
} else {
step "launch" "manual commands:"
substep "& `"$VenvDir\Scripts\Activate.ps1`""
substep "unsloth studio -H 0.0.0.0 -p 8888"
substep "unsloth studio -p 8888"
substep "(add -H 0.0.0.0 to allow network / cloud access)"
Write-Host ""
}
}
Expand Down
56 changes: 37 additions & 19 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -622,11 +622,11 @@ if [ -t 1 ]; then
) &
# Clear traps so exec does not trigger _release_lock (the subshell owns it)
trap - EXIT INT TERM
exec "$UNSLOTH_EXE" studio -H 0.0.0.0 -p "$_launch_port"
exec "$UNSLOTH_EXE" studio -p "$_launch_port"
else
# ── Background mode (no TTY) ──
# Used by macOS .app and headless invocations.
_launch_cmd=$(printf '%q ' "$UNSLOTH_EXE" studio -H 0.0.0.0 -p "$_launch_port")
_launch_cmd=$(printf '%q ' "$UNSLOTH_EXE" studio -p "$_launch_port")
_launch_cmd=${_launch_cmd% }
_spawn_terminal "$_launch_cmd"

Expand Down Expand Up @@ -1847,28 +1847,46 @@ printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio installed!"
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
echo ""

# Launch studio automatically in interactive terminals;
# in non-interactive environments (Docker, CI, cloud-init) just print instructions.
# In interactive terminals, ask the user before starting Studio.
# In non-interactive environments (Docker, CI, cloud-init) just print instructions.
if [ -t 1 ]; then
step "launch" "starting Unsloth Studio..."
"$VENV_DIR/bin/unsloth" studio -H 0.0.0.0 -p 8888
_LAUNCH_EXIT=$?
if [ "$_LAUNCH_EXIT" -ne 0 ] && [ "$_MIGRATED" = true ]; then
echo ""
echo "⚠️ Unsloth Studio failed to start after migration."
echo " Your migrated environment may be incompatible."
echo " To fix, remove the environment and reinstall:"
echo ""
echo " rm -rf $VENV_DIR"
echo " curl -fsSL https://unsloth.ai/install.sh | sh"
echo ""
echo ""
printf " Start Unsloth Studio now? [Y/n] "
if [ -r /dev/tty ]; then
read -r _reply </dev/tty || _reply="y"
else
_reply="y"
fi
exit "$_LAUNCH_EXIT"
case "${_reply:-y}" in
[Yy]*|"")
step "launch" "starting Unsloth Studio..."
"$VENV_DIR/bin/unsloth" studio -p 8888
_LAUNCH_EXIT=$?
if [ "$_LAUNCH_EXIT" -ne 0 ] && [ "$_MIGRATED" = true ]; then
echo ""
echo "⚠️ Unsloth Studio failed to start after migration."
echo " Your migrated environment may be incompatible."
echo " To fix, remove the environment and reinstall:"
echo ""
echo " rm -rf $VENV_DIR"
echo " curl -fsSL https://unsloth.ai/install.sh | sh"
echo ""
fi
exit "$_LAUNCH_EXIT"
;;
*)
step "launch" "to start later, run:"
substep "unsloth studio -p 8888"
substep "(add -H 0.0.0.0 to allow network / cloud access)"
echo ""
;;
esac
else
step "launch" "manual commands:"
substep "unsloth studio -H 0.0.0.0 -p 8888"
substep "unsloth studio -p 8888"
substep "or activate env first:"
substep "source ${VENV_DIR}/bin/activate"
substep "unsloth studio -H 0.0.0.0 -p 8888"
substep "unsloth studio -p 8888"
substep "(add -H 0.0.0.0 to allow network / cloud access)"
echo ""
fi
8 changes: 6 additions & 2 deletions studio/backend/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def _graceful_shutdown(server = None):


def run_server(
host: str = "0.0.0.0",
host: str = "127.0.0.1",
port: int = 8888,
frontend_path: Path = Path(__file__).resolve().parent.parent / "frontend" / "dist",
silent: bool = False,
Expand Down Expand Up @@ -392,7 +392,11 @@ def _trigger_shutdown():
pass

parser = argparse.ArgumentParser(description = "Run Unsloth UI Backend server")
parser.add_argument("--host", default = "0.0.0.0", help = "Host to bind to")
parser.add_argument(
"--host",
default = "127.0.0.1",
help = "Host to bind to (default: 127.0.0.1; use 0.0.0.0 for network/cloud access)",
)
parser.add_argument("--port", type = int, default = 8888, help = "Port to bind to")
parser.add_argument(
"--frontend",
Expand Down
98 changes: 98 additions & 0 deletions studio/backend/tests/test_host_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""Tests that Unsloth Studio defaults to 127.0.0.1 (loopback) not 0.0.0.0.

Uses AST parsing to inspect source-level defaults without requiring the
full studio venv (run.py has heavy dependencies like structlog/uvicorn).
"""

import ast
from pathlib import Path

_RUN_PY = Path(__file__).resolve().parent.parent / "run.py"


def _parse_function_param_defaults(source: str, func_name: str) -> dict:
"""Return {param_name: default_value} for a named function in *source*.

Only handles ast.Constant defaults (strings, ints, bools).
"""
tree = ast.parse(source)
for node in ast.walk(tree):
if (
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == func_name
):
result = {}
all_args = node.args.args
defaults = node.args.defaults
# Defaults are right-aligned against the args list
offset = len(all_args) - len(defaults)
for i, default in enumerate(defaults):
arg_name = all_args[offset + i].arg
if isinstance(default, ast.Constant):
result[arg_name] = default.value
return result
return {}


def _parse_argparse_add_argument_default(source: str, option_name: str):
"""Return the 'default' kwarg value for add_argument(option_name, ...) in *source*.

Walks the entire module so the call can live in __main__ or in a helper
function — only handles ast.Constant defaults.
"""
tree = ast.parse(source)
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
if not (isinstance(func, ast.Attribute) and func.attr == "add_argument"):
continue
if not node.args:
continue
first_arg = node.args[0]
if not (isinstance(first_arg, ast.Constant) and first_arg.value == option_name):
continue
for kw in node.keywords:
if kw.arg == "default" and isinstance(kw.value, ast.Constant):
return kw.value.value
return None


def test_run_server_default_host_is_loopback():
"""run_server() parameter default for 'host' must be 127.0.0.1, not 0.0.0.0.

Binding to 0.0.0.0 by default exposes the service on all network
interfaces, contradicting the documented "privacy first / 100% local"
guarantee. Loopback (127.0.0.1) is the least-permissive default;
users who need network access can pass -H 0.0.0.0 explicitly.
"""
source = _RUN_PY.read_text()
defaults = _parse_function_param_defaults(source, "run_server")
assert (
"host" in defaults
), "run_server() must have a 'host' parameter with a default"
host_default = defaults["host"]
assert host_default == "127.0.0.1", (
f"run_server() host default must be '127.0.0.1' (loopback) "
f"but got '{host_default}'. Binding to '{host_default}' by default "
f"exposes the service beyond localhost."
)


def test_argparse_default_host_is_loopback():
"""argparse --host add_argument default must be 127.0.0.1.

When run.py is invoked directly (python run.py), the argparse default
should match the function default so direct execution is equally safe.
"""
source = _RUN_PY.read_text()
host_default = _parse_argparse_add_argument_default(source, "--host")
assert (
host_default is not None
), "Could not find add_argument('--host', ...) in run.py"
assert (
host_default == "127.0.0.1"
), f"run.py argparse --host default must be '127.0.0.1', got '{host_default}'"
5 changes: 3 additions & 2 deletions studio/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1087,10 +1087,11 @@ else
fi
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
if [ "$_LLAMA_CPP_DEGRADED" = true ]; then
printf " ${C_DIM}%-15s${C_WARN}%s${C_RST}\n" "launch" "unsloth studio -H 0.0.0.0 -p 8888"
printf " ${C_DIM}%-15s${C_WARN}%s${C_RST}\n" "launch" "unsloth studio -p 8888"
else
printf " ${C_DIM}%-15s${C_OK}%s${C_RST}\n" "launch" "unsloth studio -H 0.0.0.0 -p 8888"
printf " ${C_DIM}%-15s${C_OK}%s${C_RST}\n" "launch" "unsloth studio -p 8888"
fi
printf " ${C_DIM}%-15s%s${C_RST}\n" "" "(add -H 0.0.0.0 to allow network / cloud access)"
fi
echo ""

Expand Down
Loading
Loading