Studio: UNSLOTH_NPM_REGISTRY opt-in for corporate npm mirrors (#6491) (#6663)

* Studio: UNSLOTH_NPM_REGISTRY opt-in for corporate npm mirrors (#6491)

studio/frontend/.npmrc pins registry=https://registry.npmjs.org/ as a
supply-chain lock. A project-level pin takes precedence over a user's
~/.npmrc, so behind a corporate firewall that blocks npmjs.org the
frontend bun/npm install hit npmjs.org directly and failed with 403.

Add an opt-in UNSLOTH_NPM_REGISTRY env var (off by default). When set it
is threaded as --registry into every registry-touching install in
setup.sh, setup.ps1 and build.sh (bun bootstrap, bun install + retry, npm
fallback, OXC validator runtime). --registry is the highest-precedence
override for both bun and npm and leaves min-release-age and save-exact
in force, so the default lock is unchanged for everyone else.

On an install failure that looks like a blocked registry, print guidance
pointing at UNSLOTH_NPM_REGISTRY and auto-suggest the mirror already set
in the user's npm config. Registries are never switched automatically.

Also correct the .npmrc comment: the pin does not block an ambient
NPM_CONFIG_REGISTRY env var (npm and bun honor that at higher precedence);
it only guards against a lower-precedence stale ~/.npmrc.

* Studio: make the registry hint reachable under set -e; clean temp log (#6491)

run_quiet_no_exit returns non-zero on failure, which under `set -euo
pipefail` exits the script at the call site before the exit code is
captured, so the new UNSLOTH_NPM_REGISTRY hint never printed on the npm
fallback and OXC validator paths. Guard both with `|| _rc=$?` (the same
idiom every other run_quiet_no_exit caller already uses) so the failure
branch runs, and remove the _FRONTEND_INSTALL_LOG temp file on the
early-exit path.

* Studio: detect the user's mirror outside the pinned frontend dir (#6491)

_suggest_npm_registry / Show-NpmRegistryHint run while the cwd is still
studio/frontend, whose .npmrc pins registry=https://registry.npmjs.org/.
So `npm config get registry` returned that pin instead of the user's
~/.npmrc mirror, and the "Detected a registry" branch was skipped for the
main corporate case (mirror set in ~/.npmrc). Run the lookup from a
directory with no project .npmrc (/ in bash, the temp dir in PowerShell)
so the user/global mirror is surfaced. The NPM_CONFIG_REGISTRY env check
is unchanged and still takes precedence.
This commit is contained in:
Daniel Han 2026-06-25 04:01:43 -07:00 committed by GitHub
parent e1698e05c7
commit 54f25bf17e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 170 additions and 16 deletions

View file

@ -246,6 +246,15 @@ curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_STUDIO_HOME=/abs/path sh
$env:UNSLOTH_STUDIO_HOME='C:\path'; irm https://unsloth.ai/install.ps1 | iex
```
Point the frontend build at a corporate npm mirror/proxy with `UNSLOTH_NPM_REGISTRY` (for the developer install behind a firewall that blocks `registry.npmjs.org`):
```bash
UNSLOTH_NPM_REGISTRY=https://artifactory.example.com/api/npm/npm/ ./install.sh --local
```
```powershell
$env:UNSLOTH_NPM_REGISTRY='https://artifactory.example.com/api/npm/npm/'; .\install.ps1 --local
```
It is threaded as `--registry` into the Studio frontend `npm`/`bun` installs; the supply-chain locks (7-day `min-release-age`, exact version pins) stay in force.
Cap Studio's native CPU thread pools on high-core hosts: `UNSLOTH_CPU_THREADS=8 unsloth studio -p 8888`.
#### Uninstall

View file

@ -35,10 +35,19 @@ _restore_gitignores() {
}
trap _restore_gitignores EXIT
# Corporate-mirror / proxy escape hatch (#6491). When UNSLOTH_NPM_REGISTRY is set we
# thread it as `--registry <url>` into the installs (overrides frontend/.npmrc's pinned
# registry for both bun and npm; min-release-age / save-exact stay in force). Empty
# array (the default) expands to nothing under `set -u`.
_NPM_REGISTRY_ARGS=()
if [ -n "${UNSLOTH_NPM_REGISTRY:-}" ]; then
_NPM_REGISTRY_ARGS=(--registry "$UNSLOTH_NPM_REGISTRY")
fi
# Use bun for install if available (faster), fall back to npm.
_install_ok=false
if command -v bun &>/dev/null; then
if bun install; then
if bun install "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}"; then
_install_ok=true
else
echo "⚠ bun install failed, falling back to npm"
@ -46,8 +55,10 @@ if command -v bun &>/dev/null; then
fi
fi
if [ "$_install_ok" != "true" ]; then
if ! npm install; then
if ! npm install "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}"; then
echo "❌ ERROR: package install failed" >&2
echo " If you are behind a corporate firewall/proxy, set UNSLOTH_NPM_REGISTRY to your mirror and retry, e.g.:" >&2
echo " UNSLOTH_NPM_REGISTRY=https://your-mirror.example/api/npm/ ./build.sh" >&2
exit 1
fi
fi

View file

@ -14,9 +14,18 @@ min-release-age=7
# `npm install <name>@<version> --save-exact` pass) but it stops new
# carets from creeping into the manifest as patch-version footguns.
save-exact=true
# Lock the registry. A user-set PIP_INDEX_URL-style override (here:
# NPM_CONFIG_REGISTRY env var or a stale ~/.npmrc) shouldn't redirect
# our installs to an attacker registry.
# Pin the default registry so a stale or hostile *lower-precedence* ~/.npmrc
# can't silently redirect our installs to an attacker registry. Note this does
# NOT block an ambient NPM_CONFIG_REGISTRY env var: npm and bun honor that at a
# higher precedence than this project file. That is exactly why Unsloth does not
# read NPM_CONFIG_REGISTRY and instead exposes one deliberate, explicit opt-in.
#
# Corporate mirror / proxy (issue #6491): if your firewall blocks
# registry.npmjs.org, set UNSLOTH_NPM_REGISTRY=<your-mirror-url> when running
# ./install.sh (or setup.sh / setup.ps1). The installer threads it as
# `--registry <url>`, which overrides this line for both npm and bun while
# leaving the min-release-age and save-exact locks above in force. Do not edit
# this line for that -- the env var keeps the default pinned for everyone else.
registry=https://registry.npmjs.org/
audit-level=high
fund=false

View file

@ -37,6 +37,18 @@ $DefaultLlamaSource = "https://github.com/ggml-org/llama.cpp"
$DefaultLlamaTag = "latest"
$DefaultLlamaForceCompileRef = "master"
# Corporate-mirror / proxy escape hatch for the frontend npm/bun install (#6491).
# studio/frontend/.npmrc pins registry=https://registry.npmjs.org/ as a supply-chain
# lock, which overrides a corporate user's ~/.npmrc proxy and causes 403s behind a
# firewall. UNSLOTH_NPM_REGISTRY is a deliberate opt-in: when set we splat it as
# `--registry <url>` into every npm/bun install. `--registry` is the highest-precedence
# override for BOTH tools and leaves min-release-age / save-exact in force. Empty array
# (the default) splats to nothing, so normal installs are unchanged.
$NpmRegistryArgs = @()
if ($env:UNSLOTH_NPM_REGISTRY) {
$NpmRegistryArgs = @('--registry', $env:UNSLOTH_NPM_REGISTRY)
}
# Verbose can be enabled either by CLI flag or by UNSLOTH_VERBOSE=1.
$script:UnslothVerbose = ($env:UNSLOTH_VERBOSE -eq '1')
foreach ($a in $args) {
@ -877,6 +889,41 @@ function substep {
Write-StudioStdoutMirror (" {0,-15}{1}" -f "", $Message)
}
function Show-NpmRegistryHint {
# Print actionable guidance when a frontend/OXC npm/bun install fails and the
# registry lock is the likely cause (corporate firewall/proxy). No-op once the
# user has opted in via UNSLOTH_NPM_REGISTRY. We never switch registries
# automatically -- we only guide.
if ($env:UNSLOTH_NPM_REGISTRY) { return }
$mirror = $env:NPM_CONFIG_REGISTRY
if (-not $mirror) {
# Read npm config from a dir with no project .npmrc so the frontend's pinned
# registry= does not mask the user's ~/.npmrc / global mirror.
$pushed = $false
try {
Push-Location ([System.IO.Path]::GetTempPath()) -ErrorAction Stop
$pushed = $true
$mirror = (& npm config get registry 2>$null | Out-String).Trim()
} catch { $mirror = "" } finally { if ($pushed) { Pop-Location } }
}
if ($mirror -in @("", "undefined", "null", "https://registry.npmjs.org", "https://registry.npmjs.org/")) {
$mirror = ""
}
Write-Host ""
step "frontend" "registry.npmjs.org looks blocked (corporate firewall/proxy?)" "Yellow"
if ($mirror) {
substep "Studio pins the public npm registry; your mirror is being ignored."
substep "Detected a registry in your npm config:"
substep " $mirror"
substep "Re-run pointing Studio at it:"
substep " `$env:UNSLOTH_NPM_REGISTRY='$mirror'; .\install.ps1 --local"
} else {
substep "If you use a private mirror/proxy, point Studio at it and re-run:"
substep " `$env:UNSLOTH_NPM_REGISTRY='https://your-mirror.example/api/npm/'; .\install.ps1 --local"
}
substep "(min-release-age and save-exact stay enforced.)"
}
# ─────────────────────────────────────────────
# Banner
# ─────────────────────────────────────────────
@ -2113,7 +2160,7 @@ if ($NeedNodeForSetup) {
substep "installing bun (faster frontend package installs)..."
$prevEAP_bun = $ErrorActionPreference
$ErrorActionPreference = "Continue"
Invoke-SetupCommand { npm install -g bun --allow-scripts=bun } | Out-Null
Invoke-SetupCommand { npm install -g bun --allow-scripts=bun @NpmRegistryArgs } | Out-Null
$ErrorActionPreference = $prevEAP_bun
Refresh-Environment
# Refresh-Environment rebuilds PATH (Machine;User;current), demoting the
@ -2173,7 +2220,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
# the cache + retry once before falling back to npm.
if ($UseBun) {
Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray
$bunExit = Invoke-SetupCommand { bun install }
$bunExit = Invoke-SetupCommand { bun install @NpmRegistryArgs }
# On Windows, .bin/ entries vary by package manager:
# npm → tsc, tsc.cmd, tsc.ps1
# bun → tsc.exe, tsc.bunx
@ -2187,7 +2234,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue
}
Invoke-SetupCommand { bun pm cache rm } | Out-Null
$bunExit = Invoke-SetupCommand { bun install }
$bunExit = Invoke-SetupCommand { bun install @NpmRegistryArgs }
$hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") -or (Test-Path "node_modules\.bin\tsc.exe") -or (Test-Path "node_modules\.bin\tsc.bunx")
$hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") -or (Test-Path "node_modules\.bin\vite.exe") -or (Test-Path "node_modules\.bin\vite.bunx")
if ($bunExit -ne 0 -or -not $hasTsc -or -not $hasVite) {
@ -2206,13 +2253,14 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
}
}
if (-not $UseBun) {
$npmExit = Invoke-SetupCommand { npm install }
$npmExit = Invoke-SetupCommand { npm install @NpmRegistryArgs }
if ($npmExit -ne 0) {
Pop-Location
$ErrorActionPreference = $prevEAP_npm
foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue }
Write-Host "[ERROR] npm install failed (exit code $npmExit)" -ForegroundColor Red
Write-Host " Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow
Show-NpmRegistryHint
exit 1
}
}
@ -2249,11 +2297,12 @@ if ((Test-Path $OxcValidatorDir) -and $NodeSource -ne "skip" -and (Get-Command n
$prevEAP_oxc = $ErrorActionPreference
$ErrorActionPreference = "Continue"
Push-Location $OxcValidatorDir
$oxcInstallExit = Invoke-SetupCommand { npm install }
$oxcInstallExit = Invoke-SetupCommand { npm install @NpmRegistryArgs }
if ($oxcInstallExit -ne 0) {
Pop-Location
$ErrorActionPreference = $prevEAP_oxc
Write-Host "[ERROR] OXC validator npm install failed (exit code $oxcInstallExit)" -ForegroundColor Red
Show-NpmRegistryHint
exit 1
}
Pop-Location

View file

@ -74,6 +74,62 @@ verbose_substep() {
return 0
}
# ── Corporate-mirror / proxy escape hatch for the frontend npm/bun install (#6491) ──
# studio/frontend/.npmrc pins registry=https://registry.npmjs.org/ as a supply-chain
# lock. A project-level pin overrides a corporate user's ~/.npmrc proxy, so the install
# hits npmjs.org directly and a firewall returns 403. UNSLOTH_NPM_REGISTRY is a
# deliberate opt-in: when set we thread it as `--registry <url>` into every npm/bun
# install. `--registry` is the highest-precedence override for BOTH tools and leaves
# min-release-age / save-exact in force. Empty array (the default) expands to nothing
# under `set -u`, so normal installs are unchanged.
_NPM_REGISTRY_ARGS=()
if [ -n "${UNSLOTH_NPM_REGISTRY:-}" ]; then
_NPM_REGISTRY_ARGS=(--registry "$UNSLOTH_NPM_REGISTRY")
fi
# Failure-path capture log consumed by _suggest_npm_registry. Set to a temp file
# around the npm/bun installs; "" elsewhere so unrelated run_quiet calls don't capture.
_CAPTURE_LOG=""
# Print actionable guidance when a frontend/OXC npm/bun install fails and the registry
# lock is the likely cause (corporate firewall/proxy). No-op once the user has opted in
# via UNSLOTH_NPM_REGISTRY. We never switch registries automatically -- we only guide.
# $1 = path to a captured install log (may be empty/missing).
_suggest_npm_registry() {
[ -n "${UNSLOTH_NPM_REGISTRY:-}" ] && return 0
local _log="${1:-}"
# If we captured output and it does NOT look like a registry/network problem, stay
# quiet -- the raw error already shown is more useful than a misleading hint.
if [ -n "$_log" ] && [ -s "$_log" ] \
&& ! grep -Eqi '40[13]|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ConnectionRefused|failed to resolve|registry\.npmjs\.org|getaddrinfo|tunneling socket|network|proxy|self.?signed|unable to (get|verify)' "$_log"; then
return 0
fi
# Best-effort: surface a mirror the user already configured (env or ~/.npmrc).
# Read npm config from / (a dir with no project .npmrc) so the frontend's pinned
# registry= does not mask the user's ~/.npmrc / global mirror -- the caller is
# still inside studio/frontend when this runs.
local _mirror="${NPM_CONFIG_REGISTRY:-${npm_config_registry:-}}"
if [ -z "$_mirror" ] && command -v npm >/dev/null 2>&1; then
_mirror="$( (cd / 2>/dev/null && npm config get registry) 2>/dev/null || true )"
fi
case "$_mirror" in
""|undefined|null|https://registry.npmjs.org|https://registry.npmjs.org/) _mirror="" ;;
esac
printf '\n' >&2
step "frontend" "registry.npmjs.org looks blocked (corporate firewall/proxy?)" "$C_WARN" >&2
if [ -n "$_mirror" ]; then
substep "Studio pins the public npm registry; your mirror is being ignored." >&2
substep "Detected a registry in your npm config:" >&2
substep " $_mirror" >&2
substep "Re-run pointing Studio at it:" >&2
substep " UNSLOTH_NPM_REGISTRY=$_mirror ./install.sh --local" >&2
else
substep "If you use a private mirror/proxy, point Studio at it and re-run:" >&2
substep " UNSLOTH_NPM_REGISTRY=https://your-mirror.example/api/npm/ ./install.sh --local" >&2
fi
substep "(min-release-age and save-exact stay enforced.)" >&2
return 0
}
run_maybe_quiet() {
if _is_verbose; then
"$@"
@ -113,6 +169,7 @@ _run_quiet() {
local exit_code=$?
step "error" "$label failed (exit code $exit_code)" "$C_ERR" >&2
cat "$tmplog" >&2
if [ -n "${_CAPTURE_LOG:-}" ]; then cat "$tmplog" >> "$_CAPTURE_LOG" 2>/dev/null || true; fi
rm -f "$tmplog"
if [ "$on_fail" = "exit" ]; then
@ -647,7 +704,7 @@ elif [ "$NODE_SOURCE" = bundled ]; then
substep "installing bun..."
# --allow-scripts=bun: npm >=11.16 gates install scripts and bun's
# postinstall fetches its binary; without it the install is a broken stub.
if run_maybe_quiet npm install -g bun --allow-scripts=bun && command -v bun &>/dev/null; then
if run_maybe_quiet npm install -g bun --allow-scripts=bun "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}" && command -v bun &>/dev/null; then
substep "bun installed ($(bun --version))"
else
substep "bun install skipped (npm will be used instead)"
@ -690,7 +747,7 @@ trap _restore_gitignores EXIT
_try_bun_install() {
local _log _exit_code=0
_log=$(mktemp)
bun install >"$_log" 2>&1 || _exit_code=$?
bun install "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}" >"$_log" 2>&1 || _exit_code=$?
# bun may create .exe shims on Windows (Git Bash / MSYS2) instead of plain scripts
if [ "$_exit_code" -eq 0 ] \
@ -707,11 +764,15 @@ _try_bun_install() {
echo " bun install exited 0 but critical binaries are missing:"
fi
sed 's/^/ | /' "$_log" >&2
if [ -n "${_CAPTURE_LOG:-}" ]; then cat "$_log" >> "$_CAPTURE_LOG" 2>/dev/null || true; fi
rm -f "$_log"
rm -rf node_modules
return 1
}
# Capture install output (bun + npm fallback) so we can detect a registry block.
_FRONTEND_INSTALL_LOG=$(mktemp)
_CAPTURE_LOG="$_FRONTEND_INSTALL_LOG"
_bun_install_ok=false
if command -v bun &>/dev/null; then
substep "using bun for package install (faster)"
@ -728,12 +789,19 @@ if command -v bun &>/dev/null; then
fi
fi
if [ "$_bun_install_ok" = false ]; then
run_quiet_no_exit "npm install" npm install --no-fund --no-audit --loglevel=error
_npm_install_rc=$?
# `|| _npm_install_rc=$?` keeps this off `set -e`'s exit path (run_quiet_no_exit
# returns non-zero on failure) so the hint branch is reachable; it also captures
# the exact exit code. Mirrors the `|| BUILD_OK=false` idiom used below.
_npm_install_rc=0
run_quiet_no_exit "npm install" npm install --no-fund --no-audit --loglevel=error "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}" || _npm_install_rc=$?
if [ "$_npm_install_rc" -ne 0 ]; then
_suggest_npm_registry "$_FRONTEND_INSTALL_LOG"
rm -f "$_FRONTEND_INSTALL_LOG"
exit "$_npm_install_rc"
fi
fi
_CAPTURE_LOG=""
rm -f "$_FRONTEND_INSTALL_LOG"
run_quiet "npm run build" npm run build
_restore_gitignores
@ -759,11 +827,19 @@ fi # end frontend build check
# Node, so do not run npm install against an unsuitable/absent system Node.
if [ -d "$_OXC_DIR" ] && [ "${NODE_SOURCE:-}" != skip ] && command -v npm &>/dev/null; then
cd "$_OXC_DIR"
run_quiet_no_exit "npm install (oxc validator runtime)" npm install --no-fund --no-audit --loglevel=error
_oxc_install_rc=$?
_OXC_INSTALL_LOG=$(mktemp)
_CAPTURE_LOG="$_OXC_INSTALL_LOG"
# `|| _oxc_install_rc=$?` keeps this off `set -e`'s exit path so the hint branch
# below is reachable; it also captures the exact exit code.
_oxc_install_rc=0
run_quiet_no_exit "npm install (oxc validator runtime)" npm install --no-fund --no-audit --loglevel=error "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}" || _oxc_install_rc=$?
_CAPTURE_LOG=""
if [ "$_oxc_install_rc" -ne 0 ]; then
_suggest_npm_registry "$_OXC_INSTALL_LOG"
rm -f "$_OXC_INSTALL_LOG"
exit "$_oxc_install_rc"
fi
rm -f "$_OXC_INSTALL_LOG"
cd "$SCRIPT_DIR"
elif [ -d "$_OXC_DIR" ] && [ "${NODE_SOURCE:-}" != skip ]; then
# No npm on PATH: skip rather than abort; the backend Node resolver degrades