Commit graph

53 commits

Author SHA1 Message Date
Nilay
ceef4123e6
Studio: Stop every running Unsloth server, not just the last one recorded (#7577)
* Stop every running Unsloth server, and refuse to start a second on a taken port

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Check the fallback range, guard PID reuse, and keep writing studio.pid

* Signal each server once when its PID is recorded in more than one file

* Confirm a recorded PID is a Studio server before signalling it

* Pin PID records to process start time and check every listener on a port

* Keep every recorded start time per PID and accept in-process Studio servers

* Match the blocking listener address and stop trusting unverifiable PID records

* Never delete a PID record that cannot be verified

* Detect our own server from our own records instead of a psutil listener scan

* Match a pre-upgrade studio.pid to the blocked port before falling back

* Never signal PID 0 or 1, and verify a per-port record before trusting it

* Stop unverifiable records instead of skipping them, and record every bind address

* Drop the command-line guess, fix Windows liveness, and free the PID record last

* Studio: harden the per-port PID records against the cases that lose a server

Follow-up on the per-port PID files. Each item below is a case where the new
code either lost a server the old code could still stop, or stopped something
that was not ours. All were reproduced against real Studio servers.

studio/backend/run.py

- Write the per-port record and the legacy studio.pid independently. They shared
  one try, so a studio root that could not take a new directory entry left the
  server recorded nowhere at all and unstoppable from the CLI; the old code
  still recorded it in studio.pid, which is an overwrite of an existing path and
  can still succeed. _remove_pid_file now also checks studio.pid when the
  per-port write failed.
- Write the record through a temp file and os.replace. `stop` reads these
  concurrently and treats a truncated read as a corrupt record.
- A failed Windows tasklist probe now means "alive", matching the CLI. Treating
  it as dead pruned a live server's record and let the next launch fall back
  past it, which is the orphan this work exists to fix.
- Guard the unlink in _own_studio_on_port. Pruning is a courtesy and must not
  abort startup.
- Extract _resolve_port so the requested-port abort is reachable from a test.
  Deleting that abort previously left the whole suite green.
- Keep the plain fallback for api-only callers. The desktop app hardcodes 8888
  and documents its reliance on the 8888-8908 range, and it reports a non-zero
  backend exit to the user as "Server stopped unexpectedly". It reads the bound
  port back from TAURI_PORT, as `studio run` does from app.state.server_port, so
  a fallback there is harmless and both servers are still recorded and
  stoppable. The interactive path prints the requested port, so it still aborts.
- isdigit() is not enough to gate int(): a superscript two passes it and the
  ValueError escaped into every caller of _read_pid_record.

unsloth_cli/commands/studio.py

- An untimed record no longer cancels a timed one for the same PID. Every
  current server writes both a timed per-port record and an untimed studio.pid,
  so the start-time check was inert exactly where it mattered, and after a crash
  plus a PID reuse `stop` sent SIGTERM to whatever unrelated process had
  inherited the PID.
- Distinguish an unreadable record from an invalid one. A root-owned record, or
  one caught mid-write, still belongs to a live server, and deleting it stranded
  that server.
- Route every PID-file removal through _unlink_quietly. One undeletable record
  raised PermissionError and left the remaining live servers running.
- Same isdigit()/int() guard as the backend.

Tests

- The requested-port abort, the recorded bind address, and the api-only
  fallback are now covered; all three previously survived deletion.
- tests/studio/test_studio_pid_file_contract.py pins run.py's filename scheme to
  the CLI's glob and keeps studio.pid parseable by an older CLI. It lives under
  tests/studio because unsloth_cli/tests is not run by any workflow.
- test_cli_studio_stop_windows.py now checks _signal_stop as well as stop. The
  kill moved into _signal_stop, so the os.kill(pid, 0) guard passed vacuously.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: let a caller that follows the port keep the fallback, and never take studio.pid from a live server

Two problems with keying the own-server abort on api_only.

`unsloth studio run` is not the bare-banner path: it stores `app = run_server(...)`
and reads `app.state.server_port` back, then uses it for the health wait, the
model load and the printed base URL. Gating on api_only aborted it, so starting
a second model while the first was up stopped working, where before it landed on
the next port and printed the right URL. Replace the proxy with an explicit
abort_if_own_studio, defaulting to the old api_only behaviour so the exec'd
`run.py` path is unchanged, and have `studio run` opt out.

The api_only exemption also reopened the orphan from the other side.
_write_pid_file overwrote studio.pid unconditionally, and a pre-upgrade server
is recorded there and nowhere else, so an exempt launch falling back past one
erased its only record. Take the file over only when it is free, already ours,
or held by a dead PID.

Also resync _pid_is_studio_backend with the CLI copy: an untimed record next to
a timed one carried no information but cancelled the start-time check, which is
what let a reused PID be treated as ours.

Tests: 51 backend, 26 CLI, 9 under tests/studio. Real Studio servers still abort
the bare same-port relaunch, still fall back past a foreign listener, and one
`unsloth studio stop` still stops every server in all five scenarios.

* Studio: hand over the legacy PID pointer, and fail stop on unreadable records

Two follow-ups from review of the previous commit.

Only one backend owns studio.pid at a time. When that server exited it
deleted the file, so an older CLI, which reads nothing else, could no
longer stop a sibling that was still serving. _remove_pid_file now hands
the pointer to a live sibling instead of dropping it.

_pid_file_entries skipped records it could not read, for instance one
written by a server started under sudo. When that was the only record,
stop printed "No running Unsloth server found" and exited 0 while the
server kept serving. Unreadable records are now reported and make stop
exit 1, so a partial stop is never mistaken for a complete one.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <unslothai@gmail.com>
2026-07-29 01:56:13 -07:00
Nilay
52609fb890
Studio: reset-password rotates the credential in place instead of deleting auth.db (#7573)
* reset-password: rotate the admin credential in place instead of deleting auth.db

* reset-password: fix the CI callers and error handling for the in-place rotation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* reset-password: narrow the CI change to the jobs that read .bootstrap_password

* reset-password: stop over-claiming what the reset revokes and when it takes effect

* auth: bind token issuance to the credential version that was verified

* auth: bind credential-creating writes to the version the request authenticated with

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* auth: bind the change-password and workflow-key writes to their own credential version

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* auth: read the credential version inside the transaction that validated it

* data-recipe: answer 401 when a reset revokes the credential mid job start

* Fix lint blocker and Windows path assertion for PR #7573

Drop the now-unused validate_api_key import from studio/backend/auth/authentication.py.
Every call site moved to validate_api_key_with_credential, so the Source lint job's
import-hoist gate flagged it as a blocker. The wrapper itself stays in storage.py;
test_api_key_expiry.py still exercises it.

Make test_run_reexec_forwards_resolved_frontend_on_public_launch compare against
str(Path(...)) instead of a POSIX literal. _find_frontend_dist returns a Path, so on
Windows the forwarded value is \fake\studio\frontend\dist and the assertion could
never pass there. Pre-existing, surfaced by running unsloth_cli/tests on Windows.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-07-29 01:40:12 -07:00
Daniel Han
5cebc46124
Make the unsloth_cli studio tests pass in isolation (#7599)
* Make the unsloth_cli studio tests pass in isolation

Six tests in test_studio_run_parallel_flag.py and one in
test_studio_secure_flag.py only passed in a full-directory run. All of them
reach the in-venv branch of run(), which does `from state.tool_policy import
set_tool_policy`. That module lives under studio/backend, so it only imports
once something has put that directory on sys.path, and nothing in either file
does. They were relying on test_start.py, which calls
ensure_studio_backend_path() and leaks the sys.path entry, or on
test_studio_cloudflare_flag.py, which stubs the module.

Add a stub_tool_policy_state fixture in a new conftest and use it in the seven,
so the state comes from the test rather than from whatever ran first.

Every file in unsloth_cli/tests now passes on its own, and the suite is stable
across four pytest-randomly seeds.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-29 01:33:19 -07:00
Daniel Han
0ed26297ed
Run unsloth_cli/tests in Backend CI (#7598)
unsloth_cli/tests had no CI at all. unsloth_cli/** was a paths trigger and a
ruff target, so the Backend CI job already fired on CLI changes but never ran
these 673 tests, which cover the studio launcher, the pre-exposure gate and the
auth secret writers. Four had been failing on main unnoticed.

Two were stale rather than broken code:

- test_studio_default_exposes_parallel_option pinned the plain --parallel
  default to 1, but #7455 deliberately moved _PARALLEL_DEFAULT_PLAIN to 4 so a
  new chat does not queue behind the previous one. Assert against the constant
  so the two cannot drift again.
- test_reexec_forwards_api_only expected --secure --api-only to re-exec. The
  pre-exposure gate now refuses that combination, because api-only serves no
  login page and the bootstrap deadline does not apply, so a seeded password
  could never be changed. Drop the case and assert the refusal instead.

Two only passed when a built frontend dist happened to be present, which it is
not in a fresh clone or on a runner. Both reach a public-launch path where the
missing-dist gate exits first, so they never got to the backend check and the
run_server call they are about. Stub _find_frontend_dist the way their siblings
already do.

Own step rather than folding into the tests/ discovery: pyproject's testpaths is
tests/, and this suite needs no PYTHONPATH or CUDA spoof, importing neither
unsloth nor torch. Its deps are already installed by the job (pydantic and
uvicorn, which brings click, via studio.txt; pyyaml explicitly).
2026-07-29 01:15:06 -07:00
Nilay
7348a20497
Studio: Write auth secret files with a trailing newline (#7576)
* Write auth secret files with a trailing newline

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pin LF in the auth secret writers and migrate legacy files

Both writers used text mode, so on Windows the trailing newline became CRLF.
The Windows Studio smoke jobs run under bash and read the file with
OLD=$(cat ...), which strips the LF but leaves the CR attached, so the
credential goes into the login body as "<secret>\r" and the request fails.

Write bytes in the backend and pin newline in the CLI so the file is
"<secret>\n" on every platform. generate_bootstrap_password() also returned
early on an existing file, so upgraded installs kept the original problem;
it now rewrites anything that isn't already exactly "<secret>\n",
best-effort so a read-only auth dir cannot fail startup.

The raw test assertions used read_text(), which decodes CRLF back to "\n"
and would have stayed green on Windows. They read bytes now.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Run the newline migration on the path upgrades actually take

ensure_default_admin() short-circuits to _load_bootstrap_password() once the
admin row exists, so the normalisation added in the previous commit sat on
generate_bootstrap_password(), which only fresh installs reach. An upgraded
install kept its newline-less file. Both readers now share
_read_persisted_bootstrap_password().

Make the write atomic while it is here: it can now rewrite a live file, and a
partial write would destroy the only plaintext copy of the recovery
credential. Same mkstemp plus os.replace shape the CLI writer already uses.

Tests cover the upgrade path through ensure_default_admin(), a well-formed
file not being rewritten on every start, a failing migration not blocking
startup, and the atomic replace.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Normalise the bootstrap file in place so a cleared credential stays cleared

The rename-based rewrite could recreate the file: if a password change ran
clear_bootstrap_password(), or the CLI cleanup deleted it, between the read and
the write, os.replace put the revoked plaintext back on disk, where a later
auth.db reset would re-seed it.

Open the existing file without O_CREAT instead, so a deleted file cannot be
resurrected, and re-check the contents through that descriptor so an in-place
truncation or a rotated credential is not overwritten either.

That gives up the atomic rename, so the in-place path is restricted to
trailing-whitespace fixes. Every partial state is then the secret plus leftover
whitespace, which still strips to the same credential. Files with leading
whitespace are left alone; every reader strips, so they keep working.

Creation still goes through the atomic writer.

* Open the bootstrap file in binary mode and finish the write

Three defects in the in-place normalisation, all on the Windows upgrade path.

os.open does not add O_BINARY on Windows and CPython never changes the CRT
default of _O_TEXT, so the descriptor was in text mode: os.write turned the LF
straight back into CRLF and ftruncate then cut the LF off, leaving
"<secret>\r". That is the bug this PR exists to fix, reintroduced by the
migration itself, and it is a fixed point that never converges. os.read
translates in reverse too, so a genuinely CRLF file failed verification and was
silently skipped.

os.write may return having written fewer bytes than asked; ftruncate would then
NUL-extend the credential so it no longer matched the hash in auth.db.

os.fchmod only reached Windows in 3.13 and AttributeError is not OSError, so on
3.9 to 3.12 it escaped both handlers and aborted the first start after upgrade.

* Make the bootstrap normalisation append-only

clear_bootstrap_password() falls back to truncating the file through its own
descriptor when the unlink fails, which is what happens on Windows while this
one is open. That truncation could land after the equality check and before the
write, so the rewrite put the revoked plaintext back.

Append a single LF instead, and only to a file that is exactly the credential.
An append cannot restore a revoked secret: over a cleared file the result is a
lone newline, which strips to empty and reads back as no bootstrap password.
Releases before the newline wrote the password with no terminator at all, so
that is the only shape in the wild; anything else is left alone and keeps
working because every reader strips.

Never truncating also removes the short-write NUL-fill hazard entirely, so the
write loop is gone. O_BINARY stays: without it Windows would turn the appended
LF into CRLF.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix a typo in a bootstrap normalisation test name

* Tighten the bootstrap newline comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: danielhanchen <unslothai@gmail.com>
2026-07-29 00:50:19 -07:00
Lee Jackson
3230a10a9c
Fix Windows Codex temporary home path (#7519)
* Fix Windows Codex temporary home path

* Fix Codex ephemeral session cleanup

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Harden Codex temp home reclamation

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-28 03:14:43 -07:00
Lee Jackson
31699f9c04
Default coding-agent servers to reasoning off (#7521)
* Default coding agent servers to reasoning off

* Fix reasoning startup compatibility and attach warning
2026-07-28 03:13:54 -07:00
oobabooga
0b34377778
Studio: Expose GPU memory mode in unsloth run and unsloth start (#7421)
* Add CLI GPU memory mode selection

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Preserve manual GPU layer overrides

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
2026-07-27 05:54:50 -07:00
Daniel Han
7a9749eb4f
unsloth start: keep the local subagent unattended and out of plan mode (#7437)
* unsloth start: keep the local subagent unattended and out of plan mode

The local subagent child could stall waiting on a permission prompt, and a
parent session in plan mode could still reach the editing agent.

- Drop human-blocking tools from the child so it runs unattended. The
  read-only child also drops the file writers.
- Emit a PreToolUse hook that reads permission_mode itself and denies the
  editing agent under plan mode, so routing holds when the model ignores
  SKILL.md. Fails open, and is skipped under the WSL bridge where a Windows
  interpreter path is not runnable in the distro.

* Make the read-only subagent actually read-only, and drop stale WSL gates

From the first review of this branch, which drove the real code against a fake
HOME holding a pre-existing Claude install and diffed the tree before and after.
No config, agent, MCP server or CLAUDE.md of the user's was touched in either
arm, and the session dir is removed on exit, Ctrl-C and exception.

Three real findings came out of it:

- The read-only child could still write. Plan mode routes Bash through a safety
  classifier served by the same local model, so a small model saying yes is what
  authorised the write; a child spawned with read_only created a file. Denying
  Bash there makes the label true, at the cost of shell exploration while
  planning. Read, Grep and Glob still cover the search it needs.
- A persisted plugin dir kept a plan_gate.py from an earlier Windows run, so a
  later WSL run shipped a hooks.json naming an interpreter the distro cannot
  execute. Hook errors do not block, so this only ever wasted a spawn, but it
  accumulated and the branch had no test.
- The comment claimed the read-only child keeps ExitPlanMode "as Claude does
  under plan mode". A --print child is never offered the plan or prompt tools at
  all, so most of both deny lists is inert today. Kept as a guard against a
  version that starts offering them, but the comment now says so.

Also covers "auto" in the gate's non-plan modes, which is a real permission_mode
and the one the child's own Bash classifier runs under.

* Stop the gate failing closed, and bound a wedged child

Second review of this branch, driving real claude 2.1.219 against a mock
endpoint rather than reading.

The gate could fail closed. If plan_gate.py went missing the interpreter exited
2, which Claude treats as a blocking hook error, so the editing tool was denied
in every mode rather than just plan. Running the script through runpy instead of
handing its path to the interpreter turns that into an ordinary traceback, which
is exit 1 and allows. Verified both exit codes directly.

The hook also had no timeout, so a hung one stalled the parent for as long as it
hung, measured past 400s. Bounded at 10s.

The real stall this branch is named for was untouched: run_local_agent polled
communicate() forever, so a local server that accepts and never answers left the
child and the parent blocked indefinitely, measured past 400s. Added a wall-clock
deadline that kills the child and says the server looks wedged.
UNSLOTH_CLAUDE_SUBAGENT_TIMEOUT overrides it, 0 restores the old behaviour.

Also corrected the plan-mode comment. Claude already refuses the editing tool in
plan mode on its own, since it advertises readOnlyHint false; what the hook adds
is a reason naming the read-only tool to call instead. The WSL comment had the
direction backwards: the gate is the Linux path, not the Windows one.

Tests: the hook command's quoting and its behaviour with the gate deleted, both
previously unguarded, plus the timeout and its env override.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Keep the gate path out of the shell string

Codex review. The hook command is run by a shell, and the gate path was
interpolated into it, so a session-config root containing shell metacharacters
expanded before Python saw it. Verified on both: sh expands $(..), backticks and
$VAR; cmd expands %VAR%. In every case the path no longer resolves, the gate
exits 1, and because that intentionally fails open the routing message silently
stops appearing.

The path now travels as base64, whose alphabet has no metacharacter in either
shell. Parametrised over all four hostile forms, and the old interpolation makes
those tests fail.

One correction to the report: it says the editing agent becomes callable in plan
mode. It does not. Claude refuses that tool by itself, since it advertises
readOnlyHint false, which was checked earlier by deleting the hook entirely.
What a mangled path costs is the reason naming the read-only agent to call
instead, not the block.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
2026-07-27 04:18:22 -07:00
Leo Borcherding
1dd2fc4583
tests: read checked-in files as UTF-8 instead of the platform default (#7438)
* tests: read checked-in files as UTF-8 instead of the platform default

Path.read_text() with no encoding uses locale.getpreferredencoding(), which
is UTF-8 on the Linux runners and cp1252 on a stock Windows install. Nine
module-level reads of checked-in source files were relying on that default.

studio/backend/routes/inference.py carries the DeepSeek tool-call token
regexes, so it holds U+FF5C and U+2581. Under cp1252 that read raised
UnicodeDecodeError on byte 0x81 at position 97806, and because the reads run
at import time it took test_cancel_atomicity.py and test_cancel_id_wiring.py
out at collection, not as failures. Green on CI, permanently broken for a
Windows contributor running the suite locally.

Adds a guard: at module scope there is no tmp_path fixture, so a bare
read_text()/write_text()/open() there is always touching a checked-in file.
That makes the rule mechanical enough to enforce with no allowlist, while
staying quiet about temp-dir I/O inside test bodies where the platform
default is harmless.

The repo already spells this correctly in 464 other places; this only stops
the stragglers coming back.

* tests: cover import-time helper reads and keep the guard py3.9-safe

Follows up on the Codex review:

- add `from __future__ import annotations`, since `str | None` in
  `_offender` is evaluated at import on Python 3.9 and pyproject declares
  requires-python ">=3.9,<3.15".
- widen the guard from module scope to import time. Class bodies and the
  bodies of module-level helpers called from an executing statement run
  during collection too, so `CODE = _extract_mixed_precision_code()` was
  the same hazard as an inline read. `if __name__ == "__main__":` blocks
  are skipped: pytest never executes them.
- scan studio/backend/tests/ as well as tests/. Both trees are collected
  on Windows by separate CI jobs, and the offender that started this,
  test_tool_xml_strip.py reading routes/inference.py, lives there.

Widening it surfaced seven more import-time reads of checked-in sources;
all now name utf-8.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Harden the import-time encoding guard for PR #7438

Close the detector gaps raised in review, all of which I reproduced against
the actual AST before changing anything.

False negatives (the guard let a real hazard through):
- _is_main_guard ignored the comparison operator, so if __name__ != "__main__"
  counted as script-only even though its body runs at import.
- The else arm of a main guard was discarded with the rest of the If node.
- Decorators and argument defaults on a module-level def were skipped with the
  body, though both are evaluated when the def executes.
- Path.open() in text mode was invisible; only builtin open() was matched.
- encoding = None and encoding = "locale" both re-select the platform default,
  but the keyword merely being present counted as pinned.

False positives (the guard would have blocked a compliant contributor):
- A non-literal mode fell through to the "r" default, so open(p, mode) was
  flagged even when mode is "rb", where adding encoding= is a ValueError and
  there is no edit that satisfies the rule.
- Same for open(*args) and a **kwargs splat, which hide the mode and can hide
  an encoding.
- Lambda bodies and comprehension elements were walked even though neither runs
  at definition.

Verified: still reports the same 22 offenders on unpatched main, green on this
branch and on the tree merged with latest main (557 files), and an adversarial
corpus of 33 cases now scores zero false positives and zero false negatives.
Also corrected two docstring claims: neither collecting job runs on Windows,
and the read is governed by locale.getencoding().

* Walk eager comprehensions and treat io.open as the builtin

Two regressions from the previous commit, both reproduced against the AST
before changing anything.

Lumping list, set and dict comprehensions in with generator expressions was
wrong. Only a genexp is lazy; the other three run their element expression,
their filters and their nested iterators immediately, so
CONTENTS = [p.read_text() for p in PATHS] at module scope is an import-time
read the guard was silently missing. Comprehensions are now walked in full and
only the genexp keeps the outermost-iterable-only treatment.

io was also in the not-a-path-opener list, but io.open is the builtin, with the
same mode position and the same platform default. io.open(CHECKED_IN_FILE) is
exactly the hazard this guard exists for, so it is matched now, with binary
modes and a pinned encoding still exempt. tarfile.open and fitz.open stay
exempt since neither has an encoding to name.

Verified: 13 targeted cases covering all five eager comprehension forms and
io.open in text, binary and pinned shapes all classify correctly; still 22
offenders on unpatched main; green on this branch and on the tree merged with
latest main.

* Close three more walker gaps in the import-time guard

All three reproduced against the AST first.

A generator expression handed straight to a call is consumed there, so
DATA = "".join(p.read_text() for p in paths) runs its element at import. Only
an unconsumed genexp bound to a name stays lazy, so the walker now follows the
consumed ones in full and keeps the outermost-iterable-only treatment for the
rest.

if "__main__" == __name__ is an equivalent and accepted spelling of the main
guard, but requiring __name__ on the left meant its body was treated as
import-time code. That is a false positive on a block pytest never runs, so
both operand orders are recognised now.

The helper table was built from module-level defs only, so a def in a class
body invoked while the class is constructed was never followed, contradicting
the walker's stated coverage of class bodies. Helpers are now collected from
the module body and from class bodies at any nesting.

Verified: 15 targeted cases including all three fixes and the earlier ones
still classify correctly; still 22 offenders on unpatched main; green on this
branch and on the tree merged with latest main.

* Handle positional read_text encodings, lazy generators and nested helpers

* Guard reads reached from test bodies, unbound Path calls and __file__ paths

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Follow derived paths, skip lazy generator helpers, cover compressed openers

* Guard the CLI tests, helper parameters and unbound Path arguments

* Discover test roots and follow literal, in-place and tuple-derived paths

* Identify module openers by import, unwrap starred paths, pin subprocess snippets

* Resolve import origins, seed helper locals, follow named generators and parametrize

* Scope imports lexically, list tracked test files, bind unpacked names

* Resolve aliased openers, keyword-only params, destructured targets, next()

* Pin the encoding on subprocess snippets, workflow lint and CLI output for PR #7438

* Harden the CLI encoding guard against detached streams for PR #7438

* Tighten the encoding guard's path and scope analysis for PR #7438

* Resolve path provenance more precisely and keep POSIX stream encodings for PR #7438

* Resolve qualified path classes and scope conditional imports for PR #7438

* Scope CLI stream setup to the entry point and align two encoding pairs for PR #7438

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-07-26 23:31:56 -07:00
Lee Jackson
7f0910fcc6
Add interactive Agents command builder (#7312)
* Add Agents settings tab for unsloth start

Adds a Settings > Agents tab documenting the `unsloth start` command:
quickstart, supported agents with click-to-copy commands, model
selection, common options, remote Studio setup, argument pass-through,
and a dry-run preview. Agent CLIs found on PATH are badged as installed.

Also removes the "New" badge from the System and Chat tabs.

* Use official brand logos for agents, invert Ollama and OpenRouter in dark mode

Claude Code and OpenAI Codex now use the Anthropic and OpenAI logos from
the provider-logos registry; agents without an official asset keep the
monogram tile. Also inverts the Ollama and OpenRouter logos in dark mode
so their monochrome marks stay visible.

* Title Agents tab "Agents (unsloth start)" and move it below Connections

The in-tab header now reads "Agents (unsloth start)" while the sidebar
label stays "Agents". Reorders the tab to sit below Connections.

* Address review: guard PATH detection, fix copy timeout, OS-aware remote snippet

- Only probe agent PATH in the desktop app on a loopback backend, so
  Installed badges are not driven by a remote server's environment.
- Show the "none found" note only when detection actually ran and
  returned empty, not when the call failed.
- Share one copy hook that resets its timeout on rapid clicks and clears
  it on unmount.
- Render the Remote Studio snippet with PowerShell syntax on Windows.
- Note that --no-launch can still load a model when --model is set.
- Drop unused quickstart translation keys.

* Add interactive Agents command builder

* Add local subagent command guidance

* Add official coding agent icons

* Use client OS for remote commands, fix copy a11y and model wording (#7303)

- Pick the remote snippet shell from the client platform, not the server deviceType
- Single-line the model examples so they paste in POSIX, PowerShell and cmd
- Split the pass-through block into independent one-command copies
- Derive detection visibility instead of clearing state in the effect
- Announce copy success to assistive tech
- Correct the quickstart/model copy: bare start uses the loaded model

* Shell-quote the model, forward the HF token, and fix the quant placeholder

- Quote the --model value in the generated and subagent commands so a local
  path with spaces or metacharacters stays a single argument (client-OS aware)
- Pass the saved Hugging Face token to listGgufVariants so gated repos resolve
- Show 'No separate quantization' instead of a stuck 'Loading quantizations...'
  when a model has no variants; clear the failure once a later request succeeds

* Fix Agents command discovery and routing

* Unsloth start improvements: download progress, server reuse, and safe model switching (#7313)

* Improve unsloth start runtime lifecycle

* Remove speculative Gemma prompt override

* Polish model download progress output

* Refine unsloth start status output

* Clarify unsloth readiness banner

* Clarify model reuse and switching output

* Queue model switches behind active inference

* Tighten unsloth start model switching

* Reduce model switch bookkeeping

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix Studio re-exec compatibility

* Recheck sidecar reservation after inference drain

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pass start marker through child environment

* Fix key redaction, switch-waiter ordering, and stop/messaging gaps for PR #7313

- Redact minted sk-unsloth keys from the startup-failure log tail: the early
  key marker lands in the server log before the model load finishes, so a
  load-phase crash printed a live key to the terminal
- Deregister a finished switch waiter before releasing the swap gate so a
  swap on another event loop cannot count it as still queued and unload the
  model the finished request is about to generate against
- Warn on same-repo quant switches: an explicit variant replaces the resident
  weights for every attached session, but the repo ids match so no switch
  warning was printed
- Note the agent exit code when it is nonzero so the server keep-alive
  message does not read as a successful session
- Use taskkill /T in unsloth studio stop so llama-server children stop too

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten comments in start, studio, and inference changes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>

* Unsloth start: add local subagents for Claude Code, Codex, OpenCode and Pi (#7326)

Bring the local-subagent support onto main. The original change (#7316) merged
into the stacked pr/daniel-unsloth-start-audit branch rather than main, and #7313
reached main via squash, so these files never landed on main.

Adds --as-subagent for claude, codex, opencode and pi: the parent agent keeps its
own cloud model while a locally served GGUF is registered as a delegated subagent,
using ephemeral per-session config that never touches the user's real agent config.

* Fix Agents builder defaults and flag validation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix Agents variant and provider fallbacks

* Fix local model and Pi subagent edge cases

* Agents tab: flag the Codex row when the loaded model is not GGUF

* Agents tab: target the active Studio server, wrap narrow rows, index the tab's search terms

* Agents tab: build copied commands from the browser-reachable Studio and show the key placeholder

* Preserve cache load ids and path variants in built commands for PR #7312

A GGUF outside the active Hugging Face cache only loads by its snapshot
path, so keep that load_id for --model while still listing the row by repo
id. Path based models carry their quant in --gguf-variant rather than a
":variant" suffix, and the active selection now keeps the variant inference
status reports for them.

* Agents tab: index the intro for agent-name searches and keep long commands inside the panel

* List GGUF variants from the cache the command loads from for PR #7312

A snapshot outside the active Hugging Face cache was offering the remote
variant list, so a quant absent from that snapshot could be selected and
the generated command would fail to load it.

* Agents tab: omit --api-key so the CLI can replay a saved key for the base

* Agents tab: label the indexed heading rows and fall back to the active desktop API base

* Agents tab: name every supported agent in the indexed intro for PR #7303

* Send the cached GGUF load path and fix the agents tab search targets for PR #7312

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten the agents tab comments for PR #7303

* Build the agents tab example commands from the active Studio base for PR #7303

* Keep the resident model on its active cache load for PR #7312

* Tighten the agents tab and cached GGUF comments for PR #7312

* Take the agent command shell from the Studio host for PR #7303

* Stop emitting snapshot paths as --model and keep unsloth start searchable for PR #7312

* Pick the command shell from where the CLI runs for PR #7303

* Match a path load by its advertised id and follow the resident model for PR #7312

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Keep an explicit quantization and retire superseded native-grant labels for PR #7312

* Scope the remembered quant, stop following unloaded models and keep local GGUF paths for PR #7312

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Stop shadowing the path classifier, match snapshot ordering and sequence status polls for PR #7312

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Release stale native-grant picks, keep local GGUF identities and index snapshot aliases for PR #7312

* Index inactive-cache snapshots, widen local GGUF detection and clear retired quants for PR #7312

* Classify cached repos by snapshot, merge repo ids case-insensitively and keep loose GGUFs variantless for PR #7312

* Fix snapshot alias, partial split and mmproj-only handling for PR #7312

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Trust scanned model_format and drop incomplete snapshot ids for PR #7312

* Exclude mmproj and partial downloads, keep path case and drop duplicate scan for PR #7312

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Restrict revision aliases and require complete snapshot variants for PR #7312

* Index revisions individually and hide partial variants for PR #7312

---------

Co-authored-by: shimmyshimmer <107991372+shimmyshimmer@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: oobabooga <oobabooga4@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-26 17:09:19 -07:00
Daniel Han
629cc50f1a
Unsloth run/start: per-model recommended sampling and override flags (#7335)
Seed each request with the model's recommended sampling (matching the Chat UI), add per-field override flags, ignore oversized overrides, warn when sampling pins cannot apply to a reused server, and apply pins to the completions endpoint.
2026-07-23 20:49:54 -07:00
oobabooga
3875479803
Complete local subagent delegation for Codex, Claude plan mode, and Pi (#7329)
Add session-scoped MCP bridges so Codex, Claude plan mode, and Pi subagents run on the loaded local model, with cloud credentials and Codex state isolated per session and a process-wide Pi agent cap.
2026-07-23 20:48:30 -07:00
Daniel Han
a0f58c1128
Unsloth start: keep Claude subagents on the local model (#7333)
Add CLAUDE_CODE_SUBAGENT_MODEL=inherit to the session-only claude settings overlay so built-in subagents stay on the loaded local model.
2026-07-23 20:47:29 -07:00
Lee Jackson
a26692612d
Normalize PWD for POSIX agent launches (#7110)
Keep the child process environment consistent with the cwd used to launch native POSIX coding agents. Some Node-based agents use PWD during project-root discovery, so inheriting a stale PWD can make them edit files in a parent or unrelated directory even when the wrapper process cwd is correct.

Only apply this normalization for native POSIX launches. WSL-launched Windows shims stay on the existing WSLENV bridge path so path translation behavior is unchanged.

Add regression coverage that launches an agent with a deliberately stale inherited PWD and asserts the child environment is normalized to os.getcwd().

Co-authored-by: Leo Borcherding <borchborchmail@gmail.com>
2026-07-23 17:54:09 -05:00
Daniel Han
4fedb51b73
unsloth start/run: tool-call flags, positional model, and grouped help (#7328)
* unsloth start/run: tool-call flags, positional model, grouped help

Expose the existing tool-call controls as first-class CLI flags on both
unsloth run and unsloth start, add positional model detection with a GGUF
quant default, and group --help into rich panels.

Flags (unsloth run): --enable-tool-call-healing/--disable-tool-call-healing
(default on), --enable-tool-call-nudging/--disable-tool-call-nudging
(default on). Resolved before any re-exec and written to the existing env
controls (UNSLOTH_DISABLE_TOOL_CALL_HEALING, UNSLOTH_TOOL_CALL_NUDGE) so the
in-venv server reads them at import; an omitted flag respects a value the
parent already set.

Flags (unsloth start): --enable-tools/--disable-tools (default off, passthrough),
plus the same healing/nudging flags (default on). start conveys them to the
auto-started run via the child env and the tools flag, so it stays correct even
if run re-execs into an older Studio venv.

Positional model: a leading org/name(:variant) token routes to --model when
--model is absent, without stealing an option value or an agent passthrough arg.
A bare GGUF repo with no variant defaults to UD-Q4_K_XL for the unsloth namespace
and Q4_K_M elsewhere, applied only on the fresh auto-serve path so attaching to a
loaded model never reloads.

Help is grouped into rich panels (Model / Server / Session for start; Model /
Server and network / Tool calls / Advanced for run) so --help reads cleanly.

Adds unit coverage for the helpers, the start command-and-env forwarding, the
positional/quant defaulting, and the run env resolution.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Positional model: reuse _is_hub_model_id so local dirs and paths are not stolen

Route a bare org/name positional to --model only when it resolves as a hub id
(via the existing _is_hub_model_id, which rejects local paths and existing
dirs), so an OpenCode project dir like owner/repo is left for the agent. Apply
the same guard to the auto-serve GGUF quant default so a local -GGUF path is
not forced to a quant it may not contain.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* unsloth start: typer floor, drop redundant GGUF quant default, respect inherited tool-call env

- Require typer>=0.12.0. The rich_help_panel options added here crash at import
  on typer<0.6, and the dependency was previously unbounded.
- Stop forcing a default GGUF quant for a bare org/name-GGUF on auto-serve. The
  server's own quant preference already picks UD-Q4_K_XL for Unsloth uploads and
  Q4_K_M otherwise, and falls back when that exact quant is missing, so forcing a
  fixed variant broke external repos that only publish Q5_K_M/Q8_0.
- Make the healing/nudging start flags tri-state so an omitted flag keeps an
  operator's inherited UNSLOTH_DISABLE_TOOL_CALL_HEALING / UNSLOTH_TOOL_CALL_NUDGE
  instead of overwriting it with the start defaults.

* Fix start passthrough and inherited tool settings

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
2026-07-23 01:44:57 -07:00
Daniel Han
968e6230a0
Unsloth start: add local subagents for Claude Code, Codex, OpenCode and Pi (#7326)
Bring the local-subagent support onto main. The original change (#7316) merged
into the stacked pr/daniel-unsloth-start-audit branch rather than main, and #7313
reached main via squash, so these files never landed on main.

Adds --as-subagent for claude, codex, opencode and pi: the parent agent keeps its
own cloud model while a locally served GGUF is registered as a delegated subagent,
using ephemeral per-session config that never touches the user's real agent config.
2026-07-22 04:34:58 -07:00
oobabooga
8b3c37246c
Unsloth start improvements: download progress, server reuse, and safe model switching (#7313)
* Improve unsloth start runtime lifecycle

* Remove speculative Gemma prompt override

* Polish model download progress output

* Refine unsloth start status output

* Clarify unsloth readiness banner

* Clarify model reuse and switching output

* Queue model switches behind active inference

* Tighten unsloth start model switching

* Reduce model switch bookkeeping

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix Studio re-exec compatibility

* Recheck sidecar reservation after inference drain

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pass start marker through child environment

* Fix key redaction, switch-waiter ordering, and stop/messaging gaps for PR #7313

- Redact minted sk-unsloth keys from the startup-failure log tail: the early
  key marker lands in the server log before the model load finishes, so a
  load-phase crash printed a live key to the terminal
- Deregister a finished switch waiter before releasing the swap gate so a
  swap on another event loop cannot count it as still queued and unload the
  model the finished request is about to generate against
- Warn on same-repo quant switches: an explicit variant replaces the resident
  weights for every attached session, but the repo ids match so no switch
  warning was printed
- Note the agent exit code when it is nonzero so the server keep-alive
  message does not read as a successful session
- Use taskkill /T in unsloth studio stop so llama-server children stop too

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten comments in start, studio, and inference changes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-07-22 02:36:24 -07:00
Long Yixing
3d379cdb81
Fix local CLI streamed generation error handling (#7135) 2026-07-20 23:14:58 -03:00
Daniel Han
c7b17c455b
Fix unsloth start on Windows: agent install, PATH resolution, and local model selection (#7257)
* unsloth start: fix Windows agent install/launch and local model selection

- claude: pin availableModels to the served model in the session --settings
  overlay so a user's ~/.claude/settings.json allowlist no longer substitutes
  the org default for the local Unsloth model. The allowlist covers --model,
  ANTHROPIC_MODEL and the model setting, and an empty [] is ignored, so the pin
  lists the model explicitly.
- installs: run the Windows installer under -ExecutionPolicy Bypass
  (process-scoped, nothing persistent) so npm's npm.ps1 and irm|iex scripts run
  under the default Restricted policy; on failure, hint at Set-ExecutionPolicy
  -Scope CurrentUser -ExecutionPolicy RemoteSigned for a hand-run retry.
- PATH: resolve agents installed to ~/.local/bin (claude) and %APPDATA%\npm (npm
  agents) in-process, so a fresh install launches without opening a new shell and
  an already-installed agent is not re-prompted for install.
- load message: "Loading <model> - please wait" while a model loads.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* unsloth start: resolve agent version against the launch PATH

The claude/codex/opencode version probes ran shutil.which while building the
command, before _launch augments PATH with the known install dirs. An agent
present only in ~/.local/bin or %APPDATA%\npm was therefore missed, assumed to
be a current build, and launched with flags an older build rejects (claude
aborts on the unknown flags). Route the three probes through a new
_which_with_install_dirs() so each resolves the same binary _launch will,
restoring PATH afterward so only _launch persists the augmentation.

Add regression tests for the three probes (POSIX and the Windows npm dir) and
make the Windows-branch tests run on POSIX hosts (pinning Path to the native
flavour so a simulated os.name does not make pathlib build WindowsPath).

* unsloth start: keep os.defpath when augmenting an unset PATH

_augment_path_with_install_dirs collapsed an unset PATH to just the install
dirs, dropping the os.defpath fallback (/bin:/usr/bin) that shutil.which and
exec*p* use when PATH is absent. A system-installed agent then looked missing
and the launched child lost its normal PATH. Seed os.defpath when PATH is
unset; an explicitly empty PATH is left as-is (search nothing), matching
shutil.which. Add regression tests for the augment helper and the version-probe
wrapper.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-20 06:05:28 -07:00
Lee Jackson
39497e6516
Translate PWD for WSL-launched Windows agents (#7111)
Bridge PWD through WSLENV /p when launching a Windows npm shim from WSL so project-root discovery uses the live cwd. The no-launch recipe adds PWD/p without freezing PWD; the concrete cwd override applies only on direct launch.
2026-07-19 21:05:22 -07:00
Lee Jackson
e0132b6d6c
Pin the Hermes remote installer and harden consent (#7179)
Pin the fetched Hermes install.sh/install.ps1 and the checkout they perform to an immutable upstream commit, and distinguish pinned from unpinned sources in the consent warning.
2026-07-19 21:04:28 -07:00
Lee Jackson
8fab1c5310
Route OpenCode yolo aliases to native auto mode (#7187)
Route --yolo to OpenCode native --auto for the default TUI and run; keep the config permission fallback for no-auto subcommands (including hidden console/generate) and for --mini, which ignores --auto.
2026-07-19 21:03:50 -07:00
Michael Han
6d8c18cd1a
Replace standalone Studio wording with Unsloth (#7221)
* Replace standalone Studio wording with Unsloth

Replace the single word Studio with Unsloth wherever it is used as
shorthand for Unsloth Studio in docs, CLI output, UI strings, i18n
locales, workflow display names, comments and docstrings.

Kept unchanged: the full name Unsloth Studio, third party product
names (LM Studio, Visual Studio, Mac Studio), feature names
(Recipe Studio, Fine-tuning Studio and its translations), and all
identifiers such as env vars, commands, paths and filenames.

* Address review feedback on the Studio wording rename

Use "an" before Unsloth where the rename left the article as "a".
Restore the split brand where Unsloth and Studio render as two halves
of the full product name: the onboarding sidebar subtitle and the
IPv6 localhost warning. Scope two messages to the full name Unsloth
Studio where plain Unsloth was misleading: the AMD README bullet and
the CLI studio setup error.
2026-07-19 00:47:04 -07:00
Leo Borcherding
91a0df9514
Studio: make the Cloudflare tunnel opt-in (off by default) (#7046)
* Studio: make the Cloudflare tunnel opt-in (off by default)

A wildcard bind (`-H 0.0.0.0`) auto-started a public trycloudflare.com
tunnel, so exposing Studio on the LAN also published it to the public
internet. Flip the default so the tunnel is opt-in.

- `--cloudflare` is now tri-state (Optional[bool], default None = off),
  mirroring the existing --enable-tools/--disable-tools handling. Pass
  --cloudflare to expose a public HTTPS link for a wildcard bind; --secure
  still implies the tunnel.
- --secure + --no-cloudflare is still rejected as a contradiction.
- Update the parent-command guard, re-exec forwarding, startup-banner
  wording, the colab comment, README, and tests.

* Studio: update installer/setup launch hints for opt-in Cloudflare

The post-install launch hints only mentioned --secure for a public link.
Now that the tunnel is opt-in, clarify that -H 0.0.0.0 exposes the raw
port on the LAN (not a public URL), and surface --cloudflare as the
explicit opt-in for a public HTTPS link (--secure keeps the raw port
private). Applied to install.ps1, install.sh, and studio/setup.sh.

* Studio: address review - keep cloudflare tri-state + harden run re-exec

Two review points from the bots:

- Gemini: keep `cloudflare` as Optional[bool] in run_server instead of
  casting None -> False, so the startup banner can distinguish "OFF (default)"
  (unset) from "OFF (--no-cloudflare)" (explicit). `_cloudflare_flag` and the
  banner branch now carry the tri-state.
- Codex (P1): `unsloth studio run` re-execs the studio venv's console script,
  which can be an older build whose --cloudflare defaulted on; omitting the
  flag let it re-enable the tunnel. That path now forwards the default polarity
  explicitly (--no-cloudflare, or nothing under --secure since --secure implies
  the tunnel). The plain `unsloth studio` path runs the same-version in-tree
  run.py (resolved via _find_run_py), so it keeps forwarding only an explicit
  polarity and still shows the accurate "(default)" banner.

Tests updated for the tri-state banner labels, the None gate cases, and the
new re-exec forwarding.

* Studio: forward --no-cloudflare on plain re-exec too (mixed install)

Codex follow-up: _find_run_py falls back to STUDIO_HOME/.../studio/backend/
run.py when the package copy is absent, so the plain `unsloth studio` re-exec
can land on an older run.py whose --cloudflare defaults on. Forward the default
polarity explicitly there too (--no-cloudflare, or nothing under --secure),
matching the run subcommand. The common in-venv launch skips the re-exec and
still shows the tri-state "(default)" banner.

* Studio: fix launch hint - --cloudflare needs the wildcard bind

Codex P3: the launch hint listed --cloudflare next to the loopback
`unsloth studio -p 8888` command, but the tunnel only starts for wildcard
binds, so `--cloudflare` alone on 127.0.0.1 does nothing. Show
`-H 0.0.0.0 --cloudflare` in the hints (install.ps1, install.sh,
studio/setup.sh) and clarify the same in the README.

* Studio: cross-platform masked terminal password prompt helper

Per-keystroke '*' echo (POSIX termios cbreak / Windows msvcrt.getwch),
backspace editing, Ctrl-C abort, EOF handling, confirmation loop with
re-prompt on mismatch or policy failure. Pure should_prompt gate for the
--secure/--cloudflare exposure paths.

* Studio CLI: force a terminal password change before public tunnel exposure

When a launch will start the Cloudflare tunnel (--secure, or --cloudflare on
a non-api-only wildcard bind) and the admin account still has its seeded
bootstrap password, prompt for a new password in the terminal (masked with
'*', confirmed, re-prompting until valid) before any re-exec or server
exists. The change is committed in the parent so it never crosses argv or
the environment and older studio-venv children see it immediately. Without
a terminal, warn and fall back to the backend bootstrap shutdown timer.
Mirrors backend update_password semantics in one transaction: rehash,
rotate the JWT secret, clear must_change_password, revoke refresh tokens,
drop the desktop secret, then remove the stale credential files.

* Studio: terminal password gate before the public tunnel (backend backstop)

Never publish a trycloudflare URL while the seeded admin password is
active: run_server now runs a terminal password-change gate after the
tunnel decision and strictly before start_studio_tunnel. Interactive
refusal fails closed (shutdown + exit 1, mirroring the secure gate);
without a tty it warns and keeps the bootstrap deadline. Success applies
the same effects as the change-password route (update_password +
revoke_user_refresh_tokens) and drops the stale
app.state.bootstrap_password. MIN_PASSWORD_LENGTH centralised in
auth/storage.py and referenced by the HTTP schema. terminal_prompt.py
carries the pure gate helper (interactive loop stubbed; supplied by the
masked-input module). Also migrates the studio/setup.ps1 launch footer
that still showed the bare wildcard hint.

* README: reconcile remote-access section with opt-in Cloudflare tunnel

* Studio: harden the terminal password gate after review

- run.py: run the gate BEFORE the uvicorn socket binds. On a wildcard
  --cloudflare launch the served HTML injects the bootstrap credential
  for first login, so a pre-gate listener would hand the default
  password to anyone who reaches the raw port while the operator is
  still typing. The gate now also seeds the admin row itself (it can
  run before lifespan startup).
- Headless launches that nothing would protect now fail closed: the
  bootstrap deadline never arms for api-only serving and
  UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT=0 disables it, so warn-and-proceed
  would have promised a shutdown that never comes. Both the CLI and the
  backend refuse to publish in that case; the ordinary headless path
  still warns and relies on the 1h deadline, and no longer auto-fills
  the default credential into HTML served on a public URL.
- storage.update_password gains revoke_refresh_tokens to delete the
  user's refresh tokens in the SAME transaction as the password commit;
  the change-password route and the backend gate use it (a separable
  follow-up delete could fail after the commit and leave a stale
  refresh token able to mint access tokens under the rotated secret).
- clear_bootstrap_password is best-effort: a locked/undeletable file
  must not surface as a failed password change.
- CLI masked reader: disable ISIG like the backend so Ctrl-Z cannot
  suspend the process with the shared terminal stuck in no-echo mode;
  handle Ctrl-C/Ctrl-Z as characters; treat stream EOF mid-line as an
  abort instead of submitting a partial password. Both readers restore
  terminal attrs from a SIGTERM/SIGHUP handler since a finally block
  cannot run when a default-disposition signal terminates the process.
- Backend reader: decode byte-at-a-time through an incremental UTF-8
  decoder so multi-byte characters split across read boundaries are no
  longer dropped; isatty checks tolerate closed/None streams.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: persist bootstrap suppression through lifespan startup

The pre-bind password gate nulled app.state.bootstrap_password, but the
FastAPI lifespan runs after it and re-reads the bootstrap password into
app.state on both admin paths, so a headless public launch could still
serve the injected credential in HTML. Carry a persistent
suppress_bootstrap_injection flag that the lifespan honors instead.

Also drop the quoted Tuple annotation on _terminal_password_gate that
tripped the import-hoist lint (the typing import looked unused).

* Studio CLI: keep the pre-exec auth DB private (0700 dir, 0600 db)

On a fresh install the pre-exposure password gate creates auth/ and
auth.db through the CLI before the backend ever runs, and
sqlite3.connect leaves the DB 0644 under a 022 umask. Mirror backend
storage.get_connection's chmod so the committed password hash and JWT
secret are never world-readable, even if the launch aborts before the
backend applies its own modes.

* Tighten pre-exposure password gate comments

* Studio: delete seeded bootstrap password before headless public re-exec

The headless warn-and-proceed path returns with the default admin
password still active, then re-execs a child Studio process. An old
studio-venv child (mixed-version install) predates the pre-bind gate and
its injection-suppress flag, so its lifespan reads .bootstrap_password
and injects the seeded credential into the public HTML for up to the
bootstrap deadline. A CLI-flag handshake cannot fix this uniformly: the
studio run path uses ignore_unknown_options and an old in-venv child
runs in-process, so it would never reject the flag.

Delete the seeded .bootstrap_password file in the parent before re-exec
so a fresh child of any version reads None and never serves it. This
covers both re-exec paths and both child versions. must_change_password
stays set, so the login page still forces a change and the bootstrap
shutdown timer still arms; only the plaintext-on-disk copy is removed.
Recovery is via a terminal-attached run or reset-password. Backend gate
and CLI warnings updated to match.

* Studio: commit the seeded admin before headless public re-exec

The headless-warn path deletes the seeded .bootstrap_password so a
re-exec'd child cannot inject it, but _ensure_cli_default_admin's INSERT
was never committed and rolled back on conn.close(). On a fresh
STUDIO_HOME an old studio-venv child then found no admin, regenerated a
fresh bootstrap password + file, and injected THAT into the public page,
defeating the deletion.

Commit the seeded admin right after _ensure_cli_default_admin so any
re-exec'd child sees the existing account and does not regenerate.
Regression tests cover both re-exec paths on a fresh (unseeded) DB.

* Studio: fail closed when the bootstrap password file cannot be removed

On the headless public path, deleting .bootstrap_password is the
protection against an old re-exec'd child injecting the seeded
credential. If unlink fails (locked file, read-only auth dir) the file
is still on disk, so warning and proceeding would still leak it for the
bootstrap-timeout window. Abort with a clear error instead. Regression
test covers the unlink-failure fail-closed path.

* Studio: hold no-echo for the whole password line, not per keystroke

The POSIX masked reader set cbreak/no-echo inside _getch_posix and restored
the terminal to echo-on in a finally after every single keystroke, because
_read_password calls _getch once per character. Between one char returning and
the next call re-entering cbreak, ECHO was on, so a keystroke arriving in that
window echoed the password in cleartext.

Move the terminal mode into a _prompt_raw_mode context that _read_password
holds around the entire line (mirroring unsloth_cli/commands/_password_prompt.py,
which already did this), restoring once when the line completes or aborts.
_getch_posix now only reads, since the mode is held by the caller. The context
is a no-op when stdin is not a real terminal, keeping the _getch test seam.

Add a regression test asserting the raw-mode context wraps the read exactly
once and every keystroke is read while it is active.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: strip the seeded bootstrap password when the auth DB check fails

The pre-exposure gate returned early on two auth-DB inspection failures and
proceeded to re-exec without removing the seeded .bootstrap_password:

- _connect_auth_db() failure: a seeded credential from a prior run may still
  be on disk.
- the must_change_password read-back failure: worse, _ensure_cli_default_admin
  had already seeded the admin and the code committed it (writing
  .bootstrap_password) right before the failing SELECT.

In the mixed-version case (a new outer CLI re-execing an old studio-venv child
that predates the pre-bind gate), that child would read the file back and
inject the default admin credential into the public Cloudflare page. The
sibling headless branch already deletes the file for exactly this reason, so
these returns were an inconsistent gap.

Factor the delete-or-fail-closed logic into
_strip_seeded_bootstrap_password_or_exit and call it on both inspection
failures (and reuse it in the headless branch): strip the seeded file first
(version-independent protection), failing closed if the removal itself fails.
must_change_password stays set, so the login page still forces a change and the
bootstrap shutdown timer still arms.

Add tests for both new paths (connect failure and post-commit read-back
failure strip the file and proceed; a failed strip fails closed).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: fail closed when the seeded admin cannot be committed before exposure

The pre-exposure gate wrapped _ensure_cli_default_admin (the INSERT), its
conn.commit(), and the must_change_password read-back in one try, and the
except recovered by stripping .bootstrap_password and proceeding to re-exec on
the assumption the admin was already committed. That assumption only holds when
the failing statement is the SELECT. When the INSERT or the commit itself fails
(e.g. a write lock held past the busy timeout on a fresh install), no admin row
is committed: it rolls back on conn.close(), and a re-exec'd old studio-venv
child (no pre-bind gate) then finds no admin, regenerates a fresh bootstrap
password + file, and serves that default credential on the public Cloudflare
page. Stripping the file cannot stop a regeneration.

Split the seed+commit into its own try that fails closed (refuse the public
launch, best-effort removing any half-written seed file) since we cannot prove a
committed admin; keep the separate read-back failure on the strip-and-proceed
path, where the admin is committed so an old child finds it and will not
regenerate. Add a test for the seed-commit-failure path.

* Studio: decode the CLI masked password reader with errors="replace"

The CLI reader read keystrokes with text-mode sys.stdin.read(1), which raises
UnicodeDecodeError on a pasted non-UTF-8 password (e.g. Latin-1 bytes), or under
PYTHONUTF8 yields a lone surrogate that later crashes the pbkdf2 encode -- either
aborts the launch with a traceback. The backend mirror (terminal_prompt.py)
already reads raw bytes through an incremental decoder with errors="replace".
Mirror that here: read with os.read and an incremental decoder so invalid bytes
map to U+FFFD, iterating over each emitted char (one byte can complete a
replacement plus the next char).

* Studio: resolve the child launcher before the pre-exposure gate

The gate strips the seeded .bootstrap_password on a headless public launch, and
it ran before the re-exec launchability check (studio venv / run.py / console
script present). So a headless launch with an incomplete studio setup would seed
the admin, delete the bootstrap password, then abort because the child could not
be found, leaving the admin at must_change_password=1 with no password ever
shown or injectable: locked out until `unsloth studio reset-password`.

Resolve and validate the child launcher first, in both `studio` (studio_default)
and `studio run`, and only then run the gate, so an unlaunchable setup exits
before anything is stripped. Add a regression test that a missing venv exits
without removing the seeded file.

* Studio: fail closed when the auth DB cannot be opened before exposure

The connect-failure branch of the pre-exposure gate stripped .bootstrap_password
and proceeded, on the assumption a committed admin from a prior run made an old
child find it and not regenerate. But on a fresh public launch whose
_connect_auth_db() itself fails (transient lock during the schema/seed step, or
an unwritable home), no admin is committed, so a mixed-version re-exec child that
predates the backend gate can find no user, generate a fresh bootstrap password,
and serve it on the public Cloudflare page. Stripping a file we cannot vouch for
cannot stop a regeneration.

Make this branch fail closed like the seed/commit failure path: we only continue
past the DB inspection once a committed admin is confirmed. The existing file is
left untouched so a retry (after a transient lock clears) can still prompt.

Update the connect-failure test to assert fail-closed, and give the in-venv
--secure flag test a real STUDIO_HOME with an already-changed admin so the gate
is a no-op rather than relying on a DB-open failure.

* Studio: invalidate seeded bootstrap files before deleting auth.db on reset

reset-password deleted auth.db first, then best-effort unlinked the seeded
.bootstrap_password and desktop secret. unlink() only ignores
FileNotFoundError, so a locked or read-only file (Windows AV, read-only auth
dir) survived while auth.db was gone. The next server start then re-seeded
from that stale plaintext and re-validated the exact credential the reset was
meant to revoke.

Invalidate the credential files first, truncating any that cannot be
unlinked, then delete the DB, so a surviving file can never carry a reusable
secret. clear_bootstrap_password now truncates on unlink failure for the same
reason, and its warning says the contents were cleared rather than claiming
the stale password is already invalid.

* Studio: require a servable frontend before the pre-exposure gate can strip the seeded password

A headless public launch strips the seeded .bootstrap_password before the
re-exec'd child starts. If the child then cannot serve the login page (the only
in-band way to change the seeded password) the admin is locked out
(must_change_password=1, no file, no UI) until reset-password.

Add _require_servable_frontend_or_exit and call it before the gate on both
`unsloth studio` and `unsloth studio run` public launches: fail closed if a
non-api-only public launch has no built frontend dist, before anything is
stripped. A user-supplied --frontend is validated to contain index.html so a
bad path cannot silently bypass the check; an auto-resolved dist is trusted
(_find_frontend_dist already requires index.html) and forwarded to the child.

Model-load aborts on `studio run` remain a residual: the parent must strip for
mixed-version safety (an old studio-venv child has no pre-bind gate) and model
loadability cannot be proven before exec, so that path stays recoverable via
reset-password.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: harden reset-password ordering and validate the in-venv backend before the strip

Three follow-ups to the pre-exposure hardening:

reset-password now deletes auth.db FIRST and proves it is gone before touching
the seeded credential files. If the DB cannot be removed (a running Studio or
Windows holds it open, or a read-only auth dir) it aborts with the credential
files untouched, so a forgotten-password reset is not left half-done with the
recovery credentials deleted while an un-resettable must_change_password=1 DB
survives. After the DB is gone it invalidates the stale credential files
(unlink, else truncate) and fails closed if a file can be neither removed nor
truncated, since a surviving plaintext would be re-seeded by
generate_bootstrap_password() and re-validate the revoked password.

The in-venv (in-process) launch path had no analogue of the re-exec launcher
check: a headless public launch would seed the admin and strip the seeded
.bootstrap_password in the gate before _load_run_module() later failed on a
broken/partial venv, leaving must_change_password=1 with no password to log in.
Add _validate_inproc_backend_before_strip, called on the in-venv path (both
`unsloth studio` and `unsloth studio run`) before the gate on the headless
public path, so a broken backend fails cleanly before anything is stripped. It
is scoped to the headless path so an interactive prompt is not delayed behind a
full backend import.

* Studio: validate the frontend and tunnel before the strip on every public path

Five follow-ups closing the remaining pre-exposure-strip lockouts:

The in-venv (in-process) paths of both `unsloth studio` and `unsloth studio
run` validated the backend but not the frontend before the gate, so a headless
public launch with a missing/bad dist would strip the seeded .bootstrap_password
and then abort in run_server() during frontend setup, leaving
must_change_password=1 with no login page. Both now validate a servable frontend
before the strip (cheap check first, backend import after) and serve the
resolved dist in-process.

The `studio run` re-exec discarded the dist that satisfied the pre-strip check
and only forwarded a user-supplied --frontend. In a shadowed install where the
parent finds a built dist the child cannot, it stripped and exec'd without the
path, and the child aborted during frontend setup. It now forwards the resolved
dist, matching `unsloth studio`.

On a headless --secure launch the bind is loopback, so the Cloudflare tunnel is
the only public exposure. If cloudflared is provably unavailable (found nowhere
and undownloadable) the tunnel cannot start, so stripping the recovery
credential would just lock the user out with no public URL ever served. Add
_tunnel_binary_confirmed_unavailable and, on --secure only, refuse the launch
with the credential preserved rather than strip. Wildcard --cloudflare binds
0.0.0.0 publicly regardless of the tunnel, so it still strips; any uncertainty
(helper not loadable) also still strips, since a possible credential leak
outweighs a recoverable lockout.

clear_bootstrap_password no longer claims it cleared the file's contents when
both unlink and truncate failed; it now reports the stale password is still on
disk and asks the user to remove it manually.

* Studio: fix cloudflared probe path and skip the bootstrap strip for a self-suppressing child

Two follow-ups to the --secure pre-exposure hardening:

The cloudflared availability probe loaded cloudflare_tunnel by file path but not
its backend deps: ensure_cloudflared() -> _cache_path() lazily imports
utils.paths.storage_roots, which only resolves when studio/backend is on
sys.path. From the outer CLI it is not, so the probe saw ensure_cloudflared()
return None (cache unresolvable) and wrongly treated the tunnel as unavailable,
refusing --secure even when cloudflared was cached or downloadable. Add the
backend dir to sys.path for the probe (and remove it after) so the cache path
resolves as it will in the child.

A headless --secure launch stripped the seeded .bootstrap_password before the
child proved the tunnel could actually connect, so a cloudflared that is present
but cannot establish the tunnel (blocked connectivity, Cloudflare outage) left
must_change_password=1 with no recovery credential. But the strip is only needed
when the re-exec'd child is an OLD studio-venv backend with no pre-bind
suppression: this install's own run.py sets app.state.suppress_bootstrap_injection
before binding and never serves the seeded credential publicly. Add
_child_self_suppresses (true in-process, or when the re-exec target is this
install's own run.py by path identity) and skip the strip in that case, keeping
.bootstrap_password as a local recovery credential; the strip stays fully in
force for the studio-venv console-script path and any venv-fallback run.py, where
an old child is actually possible.

* Studio: reword the pre-exposure terminal password prompt

* Studio: warn when -H is overridden by --secure; align pre-exposure prompt wording

- --secure/--secure run: emit a Note (not an error) when -H is a non-loopback
  host, since --secure forces the loopback bind and would otherwise discard -H
  silently.
- Reword the pre-exposure terminal prompt to 'exposed on the public internet'
  in both the backend gate and the CLI mirror.
- Align the CLI success line with the backend ("Password updated for '<user>'.").
- Tests for the new -H warning (present when overridden, absent on loopback).

* Studio: add non-interactive --password to set the initial admin password

Headless hosts (CI, containers, systemd units) have no TTY, so the forced
first-exposure password change could not be completed unattended. Add a
non-interactive way to set the INITIAL admin password before the server binds:

- --password <value>, the UNSLOTH_STUDIO_PASSWORD env var, or --password -
  (read one line from stdin). Off by default; unset falls back to the normal
  interactive terminal prompt / browser setup.
- Applies on any launch (public --secure/--cloudflare or a headless -H 0.0.0.0
  bind), only when the account still has its seeded bootstrap password. An
  already-set password is a hard error, never an override; an invalid value
  (too short, or equal to the bootstrap) fails closed before bind.
- The CLI applies the change in the parent, never forwards --password to the
  re-exec child, and strips UNSLOTH_STUDIO_PASSWORD from the child env so the
  secret never crosses to the child. run.py does the same on the direct path and
  strips the env var so spawned subprocesses (cloudflared, llama-server, tools)
  cannot inherit it.

Mirrors resolve_supplied_password across the CLI and backend, documents the
option in the README (including the argv-visibility caveat), and covers all
flows (env/stdin/literal, fail-closed cases, no-forward, env-strip,
reset-password roundtrip) in the CLI, backend, and unit suites.

* Studio: truncate the stale bootstrap file when unlink fails on a CLI password change

The post-change cleanup in _cli_update_password only warned when
.bootstrap_password could not be unlinked but was still writable (locked file,
read-only auth dir), leaving the old plaintext on disk. If auth.db is later
reset or removed, generate_bootstrap_password() reads that file back and
re-validates the revoked bootstrap password. Truncate the file on unlink
failure so its stale plaintext cannot be re-seeded, mirroring the backend
clear_bootstrap_password(); the password change is already committed, so this
never rolls it back. The warning now states truthfully whether the contents
were cleared or the file must be removed manually.

* Studio: tighten comments

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-15 06:13:25 -07:00
Wasim Yousef Said
1bf3509fea
Fix agent workspace isolation and Hermes one-shot resume (#7103)
* Fix coding agent workspace and resume handling

* Handle attached Hermes flags and OpenClaw paths

* Add Codex model metadata catalog

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix Codex reasoning summary metadata

* Preserve Hermes hook approval on resumed one-shots

---------

Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-15 15:06:03 +02:00
Daniel Han
c1e06e9ddf
unsloth start: add --persist to keep and reopen agent sessions (#7014)
* unsloth start: add --resume to persist and reopen agent sessions

`unsloth start <agent>` launches a coding agent whose home is a throwaway
temp dir wiped on exit, so codex/openclaw/hermes/pi (which relocate their
whole home there) cannot resume a conversation after you quit. opencode and
claude keep their session data in a fixed user dir, so they already resume.

Add an opt-in --resume/--no-resume flag: it routes the launch to the stable
Unsloth agents dir (the same one --no-launch already uses) so the session
survives the exit, never touching the user's own ~/.<agent>. A bare --resume
also reopens the last conversation via the agent's native flag (codex
`resume --last`, opencode/claude/pi `--continue`). The default is unchanged:
a plain launch still uses a temp dir and persists nothing.

Add a dispatch-only `resume` job to the Local Agent Guides CI that drives the
real launch path and asserts the split: codex/pi are wiped without --resume
and persist with it, while opencode/claude persist either way.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* unsloth start: rename --resume to --persist

The session flag collided with agents' own resume flags. `unsloth start
claude --resume <id>` used to forward `--resume <id>` straight to Claude
(which keeps its history in ~/.claude regardless), so a boolean --resume on
unsloth start would have swallowed the session id and turned it into a stray
prompt. Name the persistence flag --persist instead, so every agent's native
resume flag (claude --resume <id>, codex resume, opencode --continue, ...)
still passes through untouched. Behavior is otherwise identical: --persist
keeps a launched agent's session under the Unsloth agents dir, and a bare
--persist reopens the last conversation.

Add a regression test that `--resume <id>` passes through verbatim, and in the
CI resume experiment skip the redundant second pass for opencode/claude (they
persist either way, and a second CPU turn only risks a timeout).

* unsloth start: correct --persist help and drop the buggy auto-resume

Reword the --persist help to be accurate: claude and opencode keep sessions in
the user's own stores and resume regardless, so --persist only stabilizes the
otherwise-ephemeral relocated home of codex/openclaw/hermes/pi. Drop the
bare-launch auto-append of native resume tokens: it errored on a first launch
with no prior session, and was inconsistent between launch and no-launch.
--persist now only keeps the session dir; resume via the agent's own command
(e.g. `unsloth start codex --persist resume`), which now finds it.

In the CI resume experiment, fail the pass when the launched turn exits
non-zero, so a write-then-error is not misread as PERSISTED.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-09 11:47:59 +02:00
Daniel Han
6d674e5cc9
unsloth start: warn before running an agent's remote installer (#7024)
When a coding agent is missing, `unsloth start <agent>` offers to run the
vendor's own installer (curl | bash, irm | iex, or npm) after an interactive
confirm. Those installers execute with the user's privileges and there is no
signature or hash check on the fetched content, so a blind "yes" is a
supply-chain risk if the delivery path is compromised.

Keep the auto-install convenience but make consent informed: before the prompt,
name the exact remote source the installer fetches (or the command it runs for a
package installer) and state that nothing verifies a signature or hash. Behavior
is otherwise unchanged: non-interactive stdin still never executes anything, and
the confirm still defaults to no.
2026-07-09 11:08:39 +02:00
Lee Jackson
6ef0936180
Fix OpenClaw start default to local TUI (#6937)
* fix: launch OpenClaw local TUI by default

* Fix/adjust OpenClaw launch paths for PR #6937

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Default OpenClaw to the local TUI only on a bare invocation

The first-arg startswith('-') branch rewrote passthrough globals into a broken
command: OpenClaw's grammar is openclaw [--dev] [--profile <name>] <command>, so
'unsloth start openclaw --profile test' became 'openclaw tui --local --profile
test', but tui does not accept --profile (or --dev), so the invocation failed.

A leading '--flag value' is ambiguous between a global (--profile test) and a tui
option (--message hi), so it cannot be reinterpreted safely. Default to the local
TUI only when no passthrough args are given, and forward everything else verbatim
so OpenClaw parses it under its own grammar. The bare-launch default (the point
of this change) is preserved; explicit subcommands and global flags pass through.

---------

Co-authored-by: wasimysaid <112766706+wasimysaid@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
2026-07-08 04:25:42 -07:00
Long Yixing
2a6abe2ff5
feat(cli): support MLX distributed inference (#6845)
* feat(cli): detect MLX distributed launch context

* feat(mlx): wire distributed inference backend

* feat(cli): broadcast MLX distributed chat turns

* fix(cli): wait indefinitely for distributed chat turns

* fix(cli): report MLX distributed load errors cleanly

* fix(mlx): route distributed vlm through loader

* fix(cli): detect inline MLX host JSON

* fix(studio): harden distributed object sharing

* fix(studio): select JACCL distributed backend

* fix(cli): abort distributed error paths

* Distinguish real stream errors from model text via GenStreamError in distributed CLI

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fail loud when MLX distributed init returns a singleton group

The worker only reaches this block when distributed was explicitly
requested. A singleton (size 1) group means the launch failed to form a
real group (MLX built without distributed support, or an invalid launch
env/hostfile); silently continuing leaves nonzero ranks looping forever
on share_distributed_object. Raise instead so the surrounding handler
returns a clear load error.

* Tighten MLX distributed inference comments

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: danielhanchen <unslothai@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-08 03:25:39 -07:00
Lee Jackson
df6b5a57d9
Fix case-variant model matching and GGUF cache reuse in unsloth start (#6900)
* fix: handle case-variant GGUF cache hits for unsloth start

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* gguf cache: keep split shards co-located and isolate cache tests properly

When a cached main shard was reused from an older snapshot, the extra shards
were resolved independently and could come from a different snapshot dir (or a
fresh download into the current ref), leaving llama.cpp unable to load a
multi-shard GGUF whose pieces are split across directories. Only reuse a cached
main shard when every sibling shard sits in the same snapshot; otherwise fetch
the whole set together so they stay co-located.

Also patch huggingface_hub.constants.HF_HUB_CACHE (not just the HF_HUB_CACHE env
var) in the two cache tests that seeded a temp cache: the snapshot lookup reads
the module constant, so the env-only override let the real cache leak in and
skip an asserted download.

* Do not let a companion-only cache snapshot shadow real GGUF variants

When listing GGUF variants from the local HF cache, a newer snapshot may
contain only a companion file (for example a vision projector fetched on
demand) while the actual quant files live in an older snapshot. The prior
scan returned the first snapshot whose vision flag was set, yielding an
empty variant list and hiding the real quants. Keep scanning older
snapshots for actual variants and carry the vision flag across snapshots.

Also record the disk-space fallback variant's size in expected_sizes so
the later cache-reuse probe can size-verify the fallback main shard
instead of only checking for its existence.

* Propagate cached repo casing to companions and preflight split co-location

Two fixes to the case-variant GGUF cache reuse:

- Resolve the requested repo id to its cached canonical casing once in
  load_model, up front, and pass it to the main GGUF and its companions
  (mmproj / MTP drafter). Previously only _download_gguf resolved the
  casing internally, so a case-variant request loaded the main file from
  the canonical cache dir while the companions kept the requested casing
  and missed the cached vision projector / drafter offline. Extracted the
  resolution into a shared _resolve_repo_id_casing helper.

- Apply the split-shard co-location check in the disk-space preflight. When
  a split GGUF's shards are cached across different snapshots the whole set
  is refetched later, so counting them as cached made the preflight read 0
  bytes to download, skip the smaller-variant fallback, and then fail the
  full download on a low-disk machine.

* Reuse a co-located split GGUF snapshot and fix split fallback size probe

- When reusing a cached split GGUF, scan snapshots for one that holds the
  whole set co-located instead of taking the newest snapshot's first shard.
  A newer snapshot with only the first shard no longer shadows an older
  complete snapshot, so an already-cached split model is reused rather than
  refetched (which would fail offline).

- The disk-space fallback records its size in expected_sizes only for a
  single-file fallback. _find_smallest_fitting_variant returns the whole
  variant size, so using it as the first shard's expected size rejected a
  valid cached first shard of a split fallback and forced a re-download.

* Scan for a complete split snapshot in the preflight; require a loaded catalog hit

- The disk-space preflight now uses the same co-located snapshot scan as the
  download path (_cached_colocated_split_main) instead of the newest-snapshot
  probe, so a newer snapshot holding only the first shard no longer masks an
  older complete one and trips the smaller-variant fallback for a fully cached
  split model.

- _resolve_model only attaches to a /v1/models entry that is actually loaded
  (loaded != False). /v1/models also lists cached-but-unloaded catalog entries,
  and matching one by case skipped /api/inference/load and left the agent
  pointed at a model that is not resident.

* Restrict cross-snapshot GGUF cache reuse to offline

Reusing a same-name blob from an older or case-variant snapshot bypasses the
Hub revision/etag check, so a repo that updates a GGUF in place could serve
stale weights online. Gate the cross-snapshot and case-variant reuse (both the
disk-space preflight accounting and the download path) on HF_HUB_OFFLINE.
Online, hf_hub_download fetches the current revision and resumes a partial
download, so the reuse is unnecessary there; offline it remains the resilience
fallback. Marked the two reuse regression tests as the offline scenarios they
represent and added an online test asserting a fresh fetch.

* Harden offline cache reuse and hub-id detection

Three follow-ups on the case-variant GGUF cache path:

- Honor every truthy HF_HUB_OFFLINE spelling (1/true/yes/on), not just "1", when
  gating the cross-snapshot and case-variant cache reuse. With HF_HUB_OFFLINE=true
  the Hub calls are already offline, so the reuse must trigger or the cached GGUF
  fails to load; route both the preflight accounting and the download path through
  the same offline parse the rest of the backend uses.
- Resolve mmproj/MTP companions from the actual cached snapshot when offline.
  resolve_cached_repo_id_case can keep a partial lower-case spelling when any dir
  exists under the requested casing, so an hf_hub_download on that casing misses the
  canonical companion; scan every case-variant snapshot and return the cached path.
- Restrict the case-insensitive model-id match to syntactically valid hub ids
  (a single namespace/name over the HF charset). A server-side relative path such
  as models/Llama/Foo.gguf is no longer treated as a hub id, so it cannot
  casefold-match a differently cased path on a case-sensitive filesystem. This is
  host independent, unlike the local-existence probe which cannot see a server path.

* Only casefold-match model ids against a loopback Studio

A two-segment string like Models/Foo is indistinguishable from a hub id, and the
local Path.exists() probe in _is_hub_model_id cannot see a path that exists only
on a remote Studio host. So against a remote server, casefolding could attach to
a distinct server-side path (Models/Foo vs models/foo) on a case-sensitive
filesystem. Gate the case-insensitive match on is_loopback_url(base): only a
local Studio, where the existence probe is authoritative, casefolds. For a remote
Studio the match is exact and a case-mismatched request falls through to
/api/inference/load, whose already-loaded dedup resolves it correctly.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
2026-07-08 02:32:06 -07:00
Lee Jackson
baacbd025d
Fix Hermes install hint on Windows (#6903)
* fix: use Windows Hermes installer from unsloth start

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Skip the Hermes setup wizard during unattended start-install

unsloth start hermes auto-installs Hermes and then writes its own
session-scoped Hermes config. The install commands, as written, drop into
the installer's interactive setup wizard (hermes setup), which prompts for
global API keys and model choice and points the user at a different global
provider than the one Unsloth just configured, blocking the launch.

Pass the installer's skip flag on both platforms: the PowerShell scriptblock
form with -SkipSetup, and bash -s -- --skip-setup for the piped POSIX
installer.

* Refresh PATH from the registry after a Windows agent install

A Windows installer persists the agent's directory to the User/Machine PATH
in the registry and updates only its own process, so the current process
keeps a stale PATH until it restarts (the installers print 'restart your
terminal'). The post-install shutil.which then misses the just-installed
agent and unsloth start fails with 'installed but isn't on PATH yet',
forcing a re-run in a new shell.

Merge the registry PATH hives back into the process before re-resolving so a
freshly installed agent launches in the same invocation. No-op off Windows
and on any read error; only ever augments PATH.

* Fix/adjust PATH refresh for PR #6903

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
Co-authored-by: wasimysaid <112766706+wasimysaid@users.noreply.github.com>
Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
2026-07-08 02:23:25 -07:00
Lee Jackson
393d7e9c2b
Fix opencode Unsloth provider selection (#6906)
* fix: force Unsloth provider selection for opencode

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* opencode: pin the model without clobbering the user's disabled providers

The session overlay wrote disabled_providers unconditionally and the inline
OPENCODE_CONFIG_CONTENT set disabled_providers to an empty list. Since that
inline layer outranks the user's global and project config and opencode
replaces the array rather than merging it, every provider the user had
disabled was silently re-enabled for the session. Only strip 'unsloth' from an
existing disable list, and drop disabled_providers from the inline config.

Also insert --model only on a bare launch: it is a global flag for the TUI, so
placing it before a passthrough subcommand (serve/run) breaks arg parsing; a
subcommand takes the model from the pinned config instead. Parse the printed
OPENCODE_CONFIG_CONTENT with shlex.split in the test so it round-trips under
POSIX shell quoting.

* Re-enable a globally disabled opencode unsloth provider for the session

A fresh OPENCODE_CONFIG overlay omits disabled_providers, and opencode
replaces that array across config layers only when a higher layer sets the
key, so a user's global disabled_providers of ['unsloth', ...] survived the
merge and left the session provider disabled even though the overlay defines
provider.unsloth and pins the model.

Consult the user's global opencode config (XDG_CONFIG_HOME/opencode, or
%APPDATA%/opencode on Windows) when the overlay has no list of its own, and
when the effective list disables unsloth write it back to the overlay minus
unsloth. The provider loads while the user's other disabled providers stay
disabled. Best-effort read: a missing or unparseable global config is a
no-op.

* Override opencode disabled_providers in the inline layer; keep model flag for TUI flags

Re-enabling a disabled unsloth provider now rides in the inline
OPENCODE_CONFIG_CONTENT layer instead of the session overlay. The overlay
sits below a project opencode.json, which could re-disable the provider; the
inline layer outranks both global and project configs and is recomputed each
run, so no-launch reruns never reuse a stale generated list. The effective
disabled list is read from the project config if the repo sets one, else the
global config, across config.json/opencode.json/opencode.jsonc (JSONC
tolerated), and written back minus unsloth only when unsloth is disabled.

Also keep the pinned --model when the opencode passthrough starts with a
top-level TUI flag such as --dir or --continue; only a real subcommand
(serve/run/...) takes the model from config, so a leading '-' now still gets
--model injected.

* Discover the opencode project config by walking up from the cwd

opencode finds a project config by searching ancestor directories, not just
the cwd. Walk from the cwd up to the filesystem root and use the nearest
directory that sets disabled_providers, so running unsloth start opencode
from a subdirectory of a repo whose root config disables unsloth still gets
the inline override.

* Only inject opencode --model on a bare launch; rely on the inline model pin

Injecting --model whenever the passthrough started with a flag could place it
before a subcommand (e.g. opencode --print-logs serve), which opencode can
misparse. --model is unnecessary for any passthrough because the inline
OPENCODE_CONFIG_CONTENT pins the model in the highest-priority layer, so the
session model is forced without the flag. Restrict --model to the bare launch
and pass any other invocation through untouched.

* Register the session provider under a dedicated OpenCode id

Selecting the Unsloth model reliably required the wrapper to re-enable a
user-disabled unsloth provider, which meant reconstructing OpenCode's full
disabled_providers resolution (global, OPENCODE_CONFIG overlay, project config
discovered via --dir or an ancestor walk, .opencode directories,
OPENCODE_CONFIG_DIR, config.json/opencode.json/opencode.jsonc precedence, and
{env:} variable substitution) and overriding it in the inline layer. That is
unbounded and cannot be kept correct.

Register the session provider under a dedicated id (unsloth-studio) instead. A
user's disabled_providers list would never target it, so the session model is
always selectable and the overlay no longer reads or writes disabled_providers
at all: the user's own disables, in whatever config layer, are left exactly as
they are. This removes the JSONC parser, the config-directory scan, and the
ancestor/global resolution helpers, and the tests that exercised them.

* Scope the opencode session to the Studio provider

opencode filters every provider, including a config-defined custom one, through
its enabled_providers allowlist and disabled_providers denylist, and pinning the
model does not bypass that gate (a filtered provider resolves to a not-found
error). The provider arrays are also replaced, not merged, across config layers.
So a user with an enabled_providers allowlist that omits the session provider
would still have the Studio model filtered out.

Set enabled_providers to just the session provider and clear disabled_providers
in the inline OPENCODE_CONFIG_CONTENT overlay (the highest-priority layer, which
replaces these arrays). This guarantees the Studio model loads regardless of the
user's provider filters, without reading or reconstructing their multi-layer
config. It is session-only: the overlay lives in the env for this launch and
never touches the user's config files, so their normal opencode is unchanged and
only this session is limited to the Studio provider.

Also drop the redundant --model on --no-launch so the printed command stays
append-safe for drivers that append a subcommand (the inline pin forces the
model), and parse both POSIX and PowerShell no-launch output in the opencode
tests so they are not shell-specific.

* Pin opencode small_model to the session provider

The session allowlists only the Studio provider, but opencode's separate
small_model (used for lightweight tasks) could still point at another provider
from the user or project config; under the allowlist that provider is filtered,
so the lightweight task would resolve a not-found error mid-session even with the
main model pinned. Pin small_model to the session model in the same inline
overlay so every model use stays on the enabled provider. The session serves one
model, so it is the only valid target, and this stays session-only like the rest
of the overlay.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
2026-07-08 02:22:36 -07:00
Daniel Han
69f8e0b228
Clear stale yolo approval state on no-launch reruns (#6868)
* Clear stale yolo approval state on no-launch reruns

The no-launch session config dir is deliberately reused across runs, but
the config writers only ever added the --yolo auto-approval settings and
never removed them. After one --yolo --no-launch run, every later run
without --yolo kept OpenClaw's tools.exec security=full/ask=off policy
plus exec-approvals.json, and OpenCode's permission allow block, so tool
execution stayed silently pre-approved.

Non-yolo runs now reset that state: OpenClaw drops the exec policy keys
and the yolo defaults in exec-approvals.json (approvals OpenClaw itself
recorded are kept; the file is removed when only the yolo payload is
left), and OpenCode drops the permission block. Launch mode is untouched
since it already uses an ephemeral temp dir.

* Strip only yolo-written values on non-yolo cleanup

Match each field against the exact value the yolo path writes before
removing it, so a stricter exec policy, approvals defaults set by the
user or the OpenClaw UI, and deny/ask OpenCode permission entries all
survive a plain no-launch rerun. An unparseable exec-approvals.json is
left in place, matching how an unparseable config is handled.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Write a prompting policy on non-yolo instead of deleting to a permissive default

OpenClaw and OpenCode both treat an omitted policy as permissive: OpenClaw's
effective exec policy for an unset tools.exec is security=full/ask=off on the
gateway host, and OpenCode defaults an unset permission to allow. So clearing
the yolo values on a non-yolo run did not restore prompting, it fell back to
those permissive defaults and left tool execution auto-approved.

A non-yolo run now writes an explicit prompting policy: OpenClaw gets
security=allowlist/ask=on-miss (verified to prompt even with the approvals file
removed, since the stricter of config and approvals wins), and OpenCode gets
edit/bash/webfetch=ask. Only a permissive/yolo value is tightened; a stricter
deny (or an ask the user set) is preserved, and the yolo approvals defaults are
still stripped. The file-edit CI path opts opencode/openclaw into --yolo, since
those agents now prompt by default and the headless test needs auto-approval.

* Respect existing exec mode, sandbox/node host, and global permission rules on non-yolo reset

The non-yolo reset for openclaw/opencode assumed an omitted policy was the
permissive yolo default and rewrote it, which corrupted or weakened stricter
setups it should have preserved:

- OpenClaw tools.exec.mode is the normalized policy knob and cannot be combined
  with explicit security/ask (OpenClaw rejects the whole config), so writing
  security+ask alongside a mode:deny/ask policy both broke the config and
  relaxed it. Leave a mode-based policy untouched.
- host=sandbox defaults to security=deny and host=node routes to a paired node;
  neither is written by --yolo (which only writes host=gateway). Treating the
  missing security as full and popping host broadened those into gateway/auto
  exec. Only rewrite a gateway-routed permissive policy, and never pop a
  non-gateway host.
- OpenCode permission can be a string ("deny") or a {"*": ...} catch-all.
  The old code dropped a string form and overrode a catch-all by writing
  per-tool ask, weakening a stricter user rule. Now a string is left in place,
  a catch-all governs absent tools, and only an effective allow is tightened.
- The non-yolo ask policy only lived in OPENCODE_CONFIG, which loads below
  project opencode.json, so a project config allowing edit/bash/webfetch still
  auto-approved. Carry the ask policy in OPENCODE_CONFIG_CONTENT (above project
  config) too, symmetric to how yolo carries its allow.

Also harden the openclaw path against a malformed non-dict tools value.

Adds tests for mode/sandbox/node hosts, string and catch-all permissions, and
the inline ask policy over a project config.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Scope non-yolo resets to the exact yolo fingerprint and preserve granular denies

OpenClaw: reset only the exact host=gateway/security=full/ask=off policy --yolo
writes, so an omitted or host=auto/sandbox/node policy (which can resolve to a
sandbox security=deny default) is no longer broadened to allowlist/on-miss, and a
deliberate tools.exec.mode is left alone (OpenClaw never migrates our security/ask
write into a mode).

OpenCode: carry a granular object or a deny inline verbatim so a per-tool user rule
is not collapsed to a blanket ask, but floor any object that grants allow anywhere to
the string ask (which fully replaces a project object) so no inline allow pattern can
leak through into a silent auto-approve on a non-yolo session.

* Stop overriding project config on non-yolo; require full approvals fingerprint

The non-yolo OpenCode reset carried a session permission in
OPENCODE_CONFIG_CONTENT, which outranks the project opencode.json we
cannot read. That inline override could not correctly reflect the project:
it weakened a project deny to a prompt, mishandled global string rules,
leaked through a granular object's permissive default when no catch-all
was present, collapsed an object with an allow (losing its deny), and
missed per-agent permissions. All of these stem from forcing a value over
an unknown project config.

A non-yolo run now only undoes what --yolo wrote: it flips our own
explicit per-tool allow back to ask in our config file and carries no
permission inline, so the project's own permissions are honored as
written. Clearing our persisted yolo state is the actual fix; --yolo still
carries its allow inline so it works over a project config.

OpenClaw approvals cleanup now strips the yolo defaults only when the full
fingerprint (security=full, ask=off, askFallback=full) is present, so a
mixed user policy that merely shares askFallback=full (whose omitted
default is deny) is kept intact.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-07 00:06:48 -07:00
Nilay
b8400f40df
CLI: Rename unsloth connect to unsloth start (#6613)
* replaced connect with start

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix

* Studio: build the coding-agent command from the selected server

The API keys panel showed a hardcoded `unsloth start claude`. `unsloth start`
defaults to 127.0.0.1:8888 and only mints a key for a loopback server, so a
non-default port or a tunnel/remote base would target the wrong server or fail
to mint. Build the command from the panel base/key (and emit a key for
non-loopback), matching the other snippets in the panel.

* CLI: keep `unsloth connect` as a hidden alias for `unsloth start`

Avoids breaking existing scripts and docs that still call `unsloth connect`.

* Tests: stub _unstarted_cleanup in same-task disconnect test

The test builds _SameTaskStreamingResponse via __new__, so set the attribute
that __call__ now reads.

* Match coding-agent command loopback check to the CLI 127.0.0.0/8 rule (#6613)

* Keep unsloth_cli.commands.connect importable as a deprecated shim (#6613)

* Format the new coding-agents panel strings and import per biome (#6613)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Drop the unsloth connect alias and shim; unsloth start is the only command (#6613)

* Route unsloth connect to unsloth start as a hidden backward-compatible alias (#6613)

* Forward unsloth run model-load flags to unsloth start (gguf-variant, context-length, load-in-4bit, tensor-parallel) (#6613)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Session-scope coding agent config in unsloth start

Configure each agent for the current session instead of writing the Studio endpoint, key, and default model into the user's own config. Codex, OpenCode, OpenClaw, and Hermes get a private config relocated through their config-path env vars (CODEX_HOME, OPENCODE_CONFIG overlay, OPENCLAW_CONFIG_PATH plus OPENCLAW_STATE_DIR, HERMES_HOME). Claude Code suppresses the attribution header for the session via the CLAUDE_CODE_ATTRIBUTION_HEADER env var plus a --settings overlay, with no ~/.claude write. --launch uses an ephemeral temp dir removed after the agent exits; --no-launch uses a stable Unsloth-owned dir and prints the matching export lines.

* Read relocated agent session config in Local Agent Guides CI

The contract crosscheck and the openclaw/hermes patch helpers now read each agent's config from the relocated path printed by unsloth start --no-launch (CODEX_HOME, OPENCODE_CONFIG, OPENCLAW_CONFIG_PATH, HERMES_HOME) instead of fixed home paths. The Claude attribution A/B toggles the header for the session only (shipped-config HIT vs vanilla MISS) instead of editing ~/.claude/settings.json.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Skip the POSIX-only --no-launch parser test on Windows

test_no_launch_output_is_parseable mirrors the #6547 bash CI parser, which greps export/unset lines and only runs on Linux/macOS runners. On Windows --no-launch prints PowerShell ($env: / Remove-Item), so the export-line assertion does not apply there. Cross-OS staging CI surfaced this.

* Size Claude Code's auto-compact window to the loaded model's context

Claude Code auto-compacts against its native (~600k token) window, so against a smaller local model it overflows the server's context (silent truncation) long before it compacts. Set CLAUDE_CODE_AUTO_COMPACT_WINDOW to the loaded model's real context length (the value codex/openclaw already get via model_context_window / contextWindow). Omitted when the model reports no context length.

* Pin OpenCode/Hermes context window and set 90% compaction across agents

Feed every agent the server-determined sequence length (the value /v1/models reports from runtime_context_length) and a ~90% compaction threshold. OpenCode: a custom-provider model with no limit defaults to context 0, which silently disables auto-compaction, so set limit.context/output and scale the compaction buffer to 10% of the window. Hermes: pin model.context_length (it otherwise falls back to a 256k default when the server's /v1/models omits the field) and set compression.threshold 0.9. Claude: add CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=90 alongside the window. Codex (model_context_window) and OpenClaw (contextWindow) already carried the window and auto-manage off it.

* Add `unsloth start pi` recipe

Pi was the only agent without a built-in recipe, so the agent-guides CI
hand-wrote ~/.pi/agent/models.json. Add a first-class `pi` command mirroring
the others:

- write_pi_config writes the session-scoped OpenAI-compatible provider config
  (key in the config, like openclaw/opencode).
- pi() launches `pi --provider unsloth --model <id>` (Pi defaults to the google
  provider, so the provider/model are pinned on the command line) with HOME
  relocated for the session. Pi has no config-dir env var and resolves ~/.pi off
  $HOME, so HOME-scoping keeps the user's ~/.pi untouched.

Migrate the agent-guides CI off the hand-written config onto the
`unsloth start pi --no-launch` path (connection + file-edit), with a crosscheck
for the provider api, so the documented recipe is exercised.

* Harden unsloth start for Windows and WSL agent launches

Address the Codex review on PR 6613:
- write_pi_config now pins the loaded contextWindow and a sane maxTokens so Pi
  compacts instead of overflowing a small Studio context (it otherwise assumes
  its 128000 default), matching the other agents.
- pi() sets USERPROFILE (and HOMEDRIVE/HOMEPATH when present) alongside HOME on
  native Windows, where Node resolves ~/.pi via USERPROFILE rather than HOME, so
  the session no longer reads or writes the user's real ~/.pi.
- The WSLENV bridge flags path-valued vars with /p so a Windows npm shim under
  /mnt receives translated paths, while scalar vars (the numeric context window)
  pass through untranslated. WSLENV is deduped on the bare name.
- _print_env prints the launch command with PowerShell-safe quoting so the inline
  --settings JSON survives copy-paste on native Windows --no-launch.

Add tests for the WSLENV path flagging, PowerShell quoting, the Pi context
window, and the Pi USERPROFILE relocation.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Set CLAUDE_CODE_NO_FLICKER for the Claude session

A local server streams in bursts, so Claude Code's full-screen TUI redraw
flickers between tokens. Disable it for the session via CLAUDE_CODE_NO_FLICKER,
alongside the other CLAUDE_CODE_* session env knobs.

* Add a normalized --yolo flag routed to each agent's auto-approve mode

It is easy to forget which agent spells "run tools without prompting" which way,
so `unsloth start` now accepts all three spellings as one option (--yolo,
--dangerously-skip-permissions, --dangerously-bypass-approvals-and-sandbox) and
routes to the agent's own mechanism:

- claude:   --dangerously-skip-permissions
- codex:    --dangerously-bypass-approvals-and-sandbox
- hermes:   --yolo
- pi:       --approve (Pi's only approval gate is project trust)
- opencode: a permission allow block in opencode.json (no CLI flag exists)
- openclaw: tools.exec security=full / ask=off / host=gateway (no CLI flag exists)

Because the option is parsed by `unsloth start`, the "wrong" spelling for an
agent still routes correctly instead of leaking through to the agent and erroring.
IS_SANDBOX is deliberately left unset for Claude so its root/sandbox safety gate
still applies. Adds routing, cross-routing, and per-config tests.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix review findings: IPv6 loopback command, pi USERPROFILE under WSL, yolo guard

From a 10-reviewer pass over the PR:

- studio/frontend agent-command.ts: normalize bracketed IPv6 hosts. URL.hostname
  returns "[::1]" for http://[::1]:8888, which never matched the "::1" loopback
  checks, so the copied command embedded the placeholder API key for a local IPv6
  server instead of the bare auto-minting command. Now [::1] is treated as loopback
  like the CLI's is_loopback_url, so the command matches the CLI contract.

- pi(): also relocate USERPROFILE (and HOMEDRIVE/HOMEPATH) when running under WSL
  against a /mnt Windows shim, not just on native Windows. Windows Node resolves
  ~/.pi via USERPROFILE, and the WSLENV bridge translates the path, so pi no longer
  falls back to the user's real ~/.pi in that case.

- _yolo_command_flags: use .get so a config-based agent (or a typo) yields no flag
  instead of a latent KeyError.

Adds tests for the WSL pi USERPROFILE relocation, the yolo unmapped-agent guard,
and that opencode/openclaw --yolo stays config-only (no argv flag).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix round-2 review findings: WSLENV /p upgrade, agent help text

- _merge_wslenv now upgrades a user's pre-existing unflagged WSLENV entry (e.g. a
  bare HOME or USERPROFILE) to the path-translated form (HOME/p) instead of leaving
  it as-is, so a Windows agent shim under WSL receives the translated session path
  rather than the raw Linux path.
- Generalize the `unsloth start` registration help to list all six agents (was only
  "Claude Code, Codex").

Adds a test for the WSLENV unflagged-entry upgrade.

* Fix round-3 review findings: complete openclaw --yolo, refresh stale copy

- openclaw --yolo now also writes the host approvals file (exec-approvals.json with
  defaults security=full / ask=off / askFallback=full) alongside the tools.exec
  config. OpenClaw gates tool execution on both layers (the stricter wins), so the
  config alone could still leave it prompting or denying. Mirrors `openclaw
  exec-policy preset yolo`. ask=off means nothing is ever prompted, so the runtime
  socket block is unnecessary.
- Studio API panel copy: clarify that a local server auto-mints the key while a
  remote one embeds it in the command, and add pi to the swap hint.
- Local Agent Guides CI: drop the stale "pi has no start.py recipe" note now that
  all six agents are driven via `unsloth start <agent> --no-launch`.

Adds the openclaw approvals-file assertions and a no-yolo openclaw test.

* start: parse claude --version with a regex so a format change does not drop optimization flags

* start: offer to install a missing agent (prompt then run its install command)

* start: auto-start a Studio server for --model when none is running, and stop it on exit

* inference: surface an actionable message when llama-server cannot compile a tool grammar

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix review findings: kill the auto-started server tree on Windows; apply the tool-grammar message to the OpenAI passthrough too

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* start: split --model org/repo:variant so a running session is not evicted

`unsloth start <agent> --model org/repo:QUANT` failed against an already-running
Studio server and, worse, killed whatever model another session had loaded.

/v1/models lists a loaded GGUF under its bare repo id (e.g. unsloth/Qwen3-1.7B-GGUF),
so _resolve_model never matched the `:QUANT`-suffixed request. It then POSTed
/api/inference/load with model_path=org/repo:QUANT, which (a) Hugging Face rejects
("Repo id must use alphanumeric chars, '-', '_' or '.'") and (b) evicts the model the
other session was using, so a second 'unsloth start' in a new tmux/terminal tore down
the first. Re-running the command then attached to the now-empty server, which is why
it 'worked the second time'.

Mirror the org/repo:QUANT -> org/repo + --gguf-variant QUANT shorthand that
'unsloth run' and llama.cpp already accept, splitting it in _connect before we match or
serve. Matching now resolves against the loaded bare repo id (no spurious reload, no
eviction), and any real load uses a valid repo id plus gguf_variant. An explicit
--gguf-variant still wins; local paths and Windows drive letters pass through untouched.
The auto-serve path likewise spawns 'unsloth run --model org/repo --gguf-variant QUANT'.

* start: harden auth-key handling, codex teardown, and CI transcript redaction

Three review findings:

1. CI could leak a live key. agent-guides-drive.sh printed the raw
   'unsloth start --no-launch' transcript (which carries export UNSLOTH_API_KEY /
   ANTHROPIC_AUTH_TOKEN lines) to the Actions log on both the failure path and the
   success path before redact() ran. Add cat_redacted() and use it for those two
   prints, so the key is scrubbed on the way to the log while the on-disk file stays
   intact for the env parsing that follows.

2. Outages masqueraded as bad keys. _key_accepted caught a broad Exception and
   returned False, so a 5xx or timeout while checking a cached key looked like a
   rejection: it discarded a good key and minted extra ones (local) or reported 'no
   saved key' (remote). Only treat HTTP 401/403 as a rejection; let other errors
   propagate so a real outage surfaces.

3. Codex preflight could leave the auto-started server up. _require_gguf_for_codex
   runs after _connect may have auto-started Studio but before _run installs its
   teardown finally, so a preflight rejection (e.g. a transformers-backend model) left
   the server holding the port/GPU until the atexit backstop. Tear it down explicitly
   at the point of failure.

Tests: a 5xx on a saved key surfaces without minting; a non-GGUF codex preflight
tears down the auto-served server.

* start: fix IPv6/portless studio URLs, Pi config-dir isolation, and Pi install recipe

Four review findings:

1. Pi ignored the session config when PI_CODING_AGENT_DIR was already set. Pi's
   getAgentDir() reads process.env.PI_CODING_AGENT_DIR before falling back to
   $HOME/.pi/agent, so a value inherited from the user's shell sent Pi to their real
   config and skipped our provider/key (the HOME relocation alone was not enough). Pin
   PI_CODING_AGENT_DIR at the session's .pi/agent dir; it is path-valued so the WSL
   bridge translates it automatically.

2. Pi install hint dropped Pi's documented --ignore-scripts. Pi's README installs with
   'npm install -g --ignore-scripts @earendil-works/pi-coding-agent' and notes it needs
   no install scripts, so accepting the prompt now follows that safe recipe.

3. Auto-start ignored a portless UNSLOTH_STUDIO_URL. unsloth run binds to
   'parsed.port or 8888', so http://127.0.0.1 launched the child on 8888 but the health
   poll (and the returned base) still used port 80, stalling until the startup timeout.
   Normalize the base to host:8888 (IPv6-safe) before starting and polling.

4. API-panel command mistook IPv6 loopback for the bare default. The bare 'unsloth
   start' only probes 127.0.0.1:8888 on the IPv4 stack, so http://[::1]:8888 must carry
   an explicit UNSLOTH_STUDIO_URL. Drop ::1 from the bare-default host set while keeping
   it a loopback host (URL emitted, no key needed).

Tests: PI_CODING_AGENT_DIR is set to the session dir; _effective_base normalizes
portless/IPv6 bases; a portless UNSLOTH_STUDIO_URL auto-serves on :8888.

* start: apply fresh-review findings across CLI, CI, and the API-panel command

From a fresh multi-reviewer pass over the merged head plus the latest Codex bot review:

1. Load knobs now always consult the server. _resolve_model matched on model id alone,
   so --gguf-variant / --context-length / --no-load-in-4bit / --tensor-parallel were
   silently ignored whenever the id was already loaded (asking for UD-Q4_K_XL kept a
   Q8_0 serving). With any explicit knob the CLI defers to /api/inference/load, whose
   already-loaded dedup answers without reloading when variant and settings match, so a
   second session running the same command still attaches without evicting the first.

2. OpenCode --yolo and the session model pin now ride in OPENCODE_CONFIG_CONTENT. A
   project's own opencode.json outranks OPENCODE_CONFIG, so a repo config could silently
   override the session model and the --yolo permission block; OPENCODE_CONFIG_CONTENT
   outranks project config. The API key stays in the private file, never in printed env.

3. The --no-launch recipe's last line is a self-contained one-liner (inline VAR=value
   assignments before the command, conflicting vars blanked). People copy just the last
   line, and a bare codex/claude there ran against the user's real ~/.codex or Anthropic
   credentials with zero isolation, e.g. inheriting a pre-existing damaged ~/.codex
   state DB and blaming the recipe. The CI drive script scrubs the key from the one
   'invoking:' echo this adds.

4. The auto-serve log is 0600 and the parent handle is closed. It sat world-readable in
   the shared tempdir under a predictable name while carrying the minted sk-unsloth-
   key from the unsloth run banner.

5. _key_accepted fails with a clean message on outages. Non-auth errors (5xx, network,
   timeout) surfaced as a raw traceback; 401/403 still mean a rejected key.

6. _effective_base strips URL paths, and https loopback targets never auto-serve.
   http://127.0.0.1:8888/studio polled /studio/api/health (404) and https://127.0.0.1
   polled the wrong scheme, both spinning until the 15-minute startup timeout.

7. API-panel command: only literal 127.0.0.1:8888 earns the bare command. localhost can
   resolve to ::1, which the bare CLI never probes, so it keeps UNSLOTH_STUDIO_URL.

8. CI artifact sweep covers redacted-configs/ and agent-workdir/, not just logs/.

Tests: 125 CLI tests pass (new coverage for each fix), 156 backend tests pass, ruff
clean. Adds an unsloth connect alias regression test.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* start: hand Pi a clean screen at launch

Pi paints inline from wherever the cursor sits: its first render assumes a
clean screen instead of clearing or entering the alternate screen itself
(current Pi never emits a clear at startup). Launched under unsloth start,
that left the session starting mid-scroll beneath the connection output.
Clear the screen (click.clear, cross-platform, no-op without a TTY) right
before the Studio banner so Pi opens exactly one line down on a clean
viewport. Launch path only: --no-launch recipes and piped output are never
wiped, and alternate-screen agents are left alone.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* start: auto-override hermes' 64K context floor for small model windows

Hermes refuses to initialize when the served model's context window is
under 64,000 tokens, and a second copy of the same check rejects the
compression model mid-session. write_hermes_config previously pinned the
real window, so any small local model (e.g. 40,960) failed at startup
with manual config.yaml instructions.

For windows below the floor the recipe now claims 65,536 in
model.context_length, scales compression.threshold so compaction still
fires at 90% of the real window, and sets
auxiliary.compression.context_length to cover the mid-session check.
Windows at or above the floor keep the exact previous behavior.

* ci: install pi with --ignore-scripts, matching the start.py hint

The pi cell predates the pi recipe in start.py and still installed the
package with lifecycle scripts enabled, so CI stopped exercising the
exact command users are prompted to run. npm_retry now passes extra
flags through, the pi branch mirrors the install hint verbatim, and the
stale no-recipe comment is refreshed.

* ci: fail loudly when a relocation var is missing from connect output

The empty-string guards ran after appending /config.toml or /config.yaml,
so they could never fire: crosscheck_contract silently skipped its
contract checks and patch_hermes_tools died on the root path with a bare
traceback. Check the raw variable first and guide_fail with the real
cause.

* staging: 6613 round 6 (https elision, no-launch home reuse, auto-start key fallback)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: shimmyshimmer <107991372+shimmyshimmer@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
2026-07-03 08:17:27 -07:00
Daniel Han
b72a8c4263
studio: explicit Cloudflare tunnel notice and public-exposure warning at startup (#6515)
* studio: announce Cloudflare tunnel state and warn about public exposure on startup

The startup banner only printed a line when a tunnel URL was up, so a plain
`unsloth studio -H 0.0.0.0` launch silently created a public trycloudflare.com
URL with no indication that Studio had become reachable from the internet. The
only hint at the tunnel was the CLI help, shown when an invalid command was typed.

Make the banner always state the tunnel state for wildcard binds:
- ON: the public URL plus a warning that anyone with it can reach Studio from
  outside the network, and that --no-cloudflare keeps it local-only.
- FAILED: requested but did not start (local network only).
- OFF: --no-cloudflare was passed (local network only).
Secure mode keeps its existing wording (the authenticated tunnel is intended and
--no-cloudflare is not valid there). Clarify the --cloudflare help text in both
the argparse and typer definitions. Default behavior is unchanged.

Also surface the state on the `unsloth studio run` banner, which runs the server
with silent=True and prints its own banner: it now calls _print_cloudflare_line
too, so the ON/OFF/FAILED notice and public-exposure warning are no longer
skipped on that path (previously it only echoed the URL when a tunnel was up).

For the OFF and FAILED notices, do not claim "local network only" when the
reachability probe just confirmed the raw port is reachable from the public
internet: --no-cloudflare and a failed tunnel disable only the Cloudflare link,
not the wildcard bind, so the message is reworded to flag the public raw port.

* Fix/adjust Cloudflare banner warnings for PR #6515

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix/adjust Cloudflare banner comments for PR #6515

* Fix/adjust IPv6 Cloudflare tunnel gate for PR #6515

* Fix/adjust Cloudflare review comments for PR #6515

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix silent run Cloudflare notice

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: wasimysaid <wasimysdev@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-30 17:47:48 +02:00
Avaya Aggarwal
cb274484a6
Add GGUF --tensor-parallel CLI option (#6561)
---------

Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
2026-06-26 15:30:10 -03:00
Daniel Han
76cbddb859
Studio: allow --secure with --api-only (headless secure API server) and add --api-only to unsloth studio run (#6591)
* Studio: start the Cloudflare tunnel for --secure even in --api-only, and add --api-only to `unsloth studio run`

--secure exposes ONLY the Cloudflare link (it forces a loopback bind), but
_cloudflare_tunnel_should_start gated the tunnel on `not api_only`, so
`run.py --secure --api-only` started no tunnel and then fail-closed with
"A secure Cloudflare link is not allowed". That blocked the natural headless
use: serve just the API (no web UI) over the authenticated tunnel.

Make --secure start the tunnel regardless of api_only (the non-secure path is
unchanged: tunnel only a 0.0.0.0 bind, never api-only Tauri or Colab). Then
expose --api-only on `unsloth studio run` and forward it through both the
re-exec args and the in-venv run_server call, so
`unsloth studio run --secure --api-only --model ...` is a one-liner secure API
server.

Verified end to end: `run.py --secure --api-only` now brings up the tunnel and
serves /api/health over it (200), with / returning 404 (no UI).

Tests: update the tunnel-gate truth table (secure+api-only now tunnels;
secure+colab still does not) and add --api-only registration + re-exec/in-venv
forwarding coverage to the run CLI tests.

* Trim comments to be succinct (no behavior change)

* studio: address review on parent --api-only and secure api-only CORS

- Reject --api-only on the parent `unsloth studio` group when a subcommand
  is invoked, with the same redirect guidance used for --parallel/--secure;
  otherwise the flag was silently dropped and the UI served anyway.
- Keep CORS any-origin for secure api-only serving: that mode publishes the
  API over Cloudflare for remote browser clients, so the Tauri-only lockdown
  (still applied to plain local api-only) would break preflight. Factored the
  decision into cors_origins_for_mode() and gate it on api_only and not secure;
  run_server exports UNSLOTH_SECURE before importing main.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: suppress TAURI_PORT and de-dup test for headless run --api-only

- run_server gains emit_tauri_port (default True, unchanged for the Tauri/
  desktop path). The new headless `run --api-only` path passes False so the
  Tauri-only TAURI_PORT= line no longer prepends the documented URL/API key
  banner (it ran even under --silent and could break one-liner parsers).
- Remove a duplicate test_reexec_forwards_api_only that shadowed the
  parametrized one; fold the --secure --api-only case into it so the secure
  headless path is actually collected.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-23 05:44:56 -07:00
Daniel Han
6254ab37c3
Studio: accept --not-secure as a back-compat alias for --no-secure (#6568)
* Studio: accept --not-secure as a back-compat alias for --no-secure

PR #6560 renamed the negative secure flag from --not-secure to --no-secure
to match argparse.BooleanOptionalAction. Re-add --not-secure as a hidden,
deprecated alias at both CLI layers so existing scripts and muscle memory
keep working, while --no-secure stays the documented spelling.

- studio/backend/run.py: extract the CLI parser into _build_arg_parser() so
  the flag wiring is unit-testable, and register --not-secure as a hidden
  store_false alias for --no-secure. Last flag wins, matching
  BooleanOptionalAction semantics.
- unsloth_cli/commands/studio.py: add a hidden --not-secure option to
  `unsloth studio` and `unsloth studio run`; it forces secure off and
  forwards the canonical --no-secure to the backend.
- Tests at both layers for the alias and its polarity.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: address review on --not-secure alias

- run.py: use argparse.SUPPRESS for the --not-secure default so the alias
  never contributes a namespace default (the canonical --secure owns it).
- studio.py: resolve --not-secure last-wins from argv via _resolve_secure()
  so `--not-secure --secure` keeps secure on, matching the backend's
  BooleanOptionalAction and how --secure/--no-secure already behave.
- Add a CLI last-wins test covering both flag orders.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-22 07:20:50 -07:00
PChemGuy
52c2cf8b63
Correct wrong negative argparse.BooleanOptionalAction argument name (#6560)
studio.backend.run.__main__ adds "--secure" argument via argparse.BooleanOptionalAction, which automatically creates negative --no-secure, that is with **NO** prefix, instead of **NOT**.
2026-06-22 05:31:15 -07:00
Daniel Han
9b5c94df32
CLI: stop unsloth connect from leaking Studio credentials to unverified servers (#6479)
* CLI: stop `unsloth connect` from leaking Studio credentials to unverified servers

`unsloth connect` (and `unsloth chat`) discovered a Studio base URL from
UNSLOTH_STUDIO_URL or the default localhost port after only an unauthenticated
/api/health probe, then sent credentials to it:

- keyless connect iterated every cached API key and sent each as a bearer token
  to {base}/v1/models, so a malicious or port-preempting endpoint could harvest
  all of them;
- with no cached key it self-issued a Studio JWT and POSTed it to
  {base}/api/auth/api-keys;
- unsloth chat sent the same self-issued JWT to the discovered base.

The key cache was a flat, global list with no binding to a server identity, so a
key minted for one Studio could be replayed to any other.

Changes:

- Scope the agent key cache per base URL so a key is only ever replayed to the
  exact server it was minted for. Pre-scoping flat caches are ignored rather
  than replayed (at most one extra local mint on the next launch).
- Gate every automatic credential flow to loopback bases. A non-loopback
  UNSLOTH_STUDIO_URL now requires an explicit --api-key and nothing is sent
  automatically. SSH-tunnelled Studios that land on 127.0.0.1 keep working.
- Mint the API key locally against the Studio auth DB instead of POSTing a
  self-issued JWT over the network, so no bearer token leaves the process on the
  local path.
- Apply the same loopback gate to connect_studio_server (used by unsloth chat).

Fully closing same-host loopback-port preemption needs a signed /api/health
handshake so the client can verify the server identity before sending anything;
that is tracked as a server-side follow-up.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* CLI: verify Studio server identity before auto-sending credentials

Adds a challenge-response so `unsloth connect` and `unsloth chat` can confirm a
discovered loopback endpoint is really this install's Studio (not a process
that preempted the port) before sending it a cached or freshly minted
credential. This closes the same-host loopback-preemption gap left open by the
previous commit, which could only limit the blast radius.

Server:

- storage.get_or_create_identity_secret(): a dedicated server-wide secret in
  app_secrets (kept separate from the per-user JWT secret), readable only by
  the same OS user.
- storage.compute_identity_proof(nonce) = HMAC-SHA256(identity secret, nonce).
- GET /api/auth/identity?nonce=<base64url>: unauthenticated, returns the proof.
  The nonce is opaque to the server and the proof reveals nothing about the
  secret, so answering is safe.

Client:

- verify_studio_identity(base): sends a fresh 32-byte nonce, recomputes the
  expected HMAC from the local same-user secret, and constant-time compares.
  Fails closed on any error.
- connect._agent_api_key gates the loopback cached-key replay and the local
  mint on it; connect_studio_server (used by unsloth chat) gates the
  self-issued JWT on it.

A server that cannot read this install's secret (a different OS user, or a
remote/fake endpoint) cannot produce a matching proof, so the client refuses
and falls back to an explicit --api-key.

Tests: studio/backend/tests/test_identity.py (proof determinism, secret
persistence and caching, route response and nonce validation) and additions to
test_connect.py (the verify gate refuses when unverified, an explicit key skips
the check, and an end-to-end client plus server handshake against a stub HTTP
server).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* CLI: mint through the verified server instead of the local auth DB

CodeQL (py/clear-text-storage-sensitive-data) flagged the API key written to
the per-server cache and the agent config files once it was sourced from
storage.create_api_key(); the original HTTP-minted key did not trip the query.

Now that the identity handshake cryptographically confirms the loopback
responder really is this Studio before anything is sent, minting through the
server's /api/auth/api-keys endpoint with a self-issued JWT is safe again and
restores the original, CodeQL-clean data flow. The local-DB mint path is
removed.

The security properties are unchanged: discovery is still loopback-gated and
identity-verified, the key cache is still scoped per server, and a credential
reaches the server only after the handshake has proven its identity. The only
difference from the previous commit is that the self-issued JWT is sent to the
already-verified loopback server rather than the key being minted in-process.

Tests updated to mint through the fake server again.

* CLI: address review feedback on connect credential handling

- Reuse a saved per-server key before the loopback/identity gate. Keys are
  scoped per base URL, so a key the user saved with --api-key for a remote or
  SSH-tunnelled Studio (whose identity secret the local handshake can't match)
  is replayed only to that exact server. The loopback + identity-handshake gate
  now guards just auto-minting (self-issuing a JWT and creating a new key),
  which is the path that needs a cryptographically verified local Studio. Fixes
  keyless reuse being impossible for remote/tunnelled Studios the user had
  saved a key for.

- connect_studio_server (unsloth chat / inference): when the user explicitly set
  UNSLOTH_STUDIO_URL but the server can't be safely attached (non-loopback, or
  identity unverifiable), fail with a clear message instead of silently loading
  the model locally. Opportunistic discovery of the local default still falls
  back to a local load.

- Harden cache parsing: tolerate a corrupt or hand-edited cache where a base
  maps to a non-list (which would otherwise iterate a string into
  single-character "keys"), and read the cache as UTF-8.

Tests updated and added: saved-key replay without the handshake for both local
and remote bases, keyless mint still refused when the loopback server is
unverified, and connect_studio_server erroring on an explicit remote while
falling back locally on default discovery.

* CLI: harden connect handshake against relay and gate cached minted keys

Addresses review feedback on the credential handshake:

- Refuse HTTP redirects on credential-bearing requests (the identity handshake,
  /v1/models, key minting, and the chat HTTP backend). A process squatting the
  discovered port could 302 /api/auth/identity to the real Studio and relay its
  valid proof, or bounce a bearer-token request to another base, and urllib
  follows redirects by default. A shared no-redirect opener now treats any 3xx
  as an error.

- Give cached keys provenance. Keys the user supplied with --api-key are "saved"
  and replay without the handshake (needed for remote or SSH-tunnelled Studios
  whose secret the local handshake can't match). Keys we auto-mint are "minted"
  and replay only after the identity handshake, so a port squatter can't collect
  a previously minted localhost key just by answering the health check. New cache
  shape: servers[base] = {"saved": [...], "minted": [...]}.

Known residual: a different-OS-user process that squats the port and can also
reach a genuine same-secret Studio elsewhere on loopback can still manually relay
the identity challenge. Fully closing that needs the proof bound to the server's
real listening port, or OS-level peer-credential checks; tracked as follow-up. A
same-user attacker is out of scope, since it can already read the 0600 key cache.

Tests: redirect rejection in the handshake, minted-cache requiring the handshake
while saved-cache bypasses it, and the existing suites updated for the new cache
shape. unsloth_cli (206) and test_identity.py (5) pass.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* CLI: keep urllib imports function-local in the no-redirect opener

The repo's import-hoist safety linter (scripts/verify_import_hoist.py) flags
hoisting urllib to module level because it re-points the 'urllib' name in the
pre-existing HttpChatBackend._request scope. Build the no-redirect opener lazily
with function-local urllib imports instead, matching this module's convention,
and restore the local 'import urllib.request' in _request and
verify_studio_identity.

* test(identity): skip route tests when routes.auth import chain is unavailable

The identity route tests build a TestClient from routes.auth, which pulls the
whole routes package (routes/__init__ -> inference -> llama_cpp, ...). In a
minimal test matrix without the heavy backend deps, or when another test in the
same process has already broken that import chain, importing it raised and the
two route tests hard-failed. Skip in that case instead: the proof crypto is
covered by the storage-level tests, and the full backend CI still exercises the
route. No behaviour change where the deps are present (5 passed in isolation).

* test(connect): make connect tests pass on native Windows

unsloth connect supports Windows: --no-launch prints PowerShell ($env:X =
"v" / Remove-Item Env:X) instead of POSIX (export/unset), and the launch
path bridges env into a Windows agent .exe over WSLENV. The tests hardcoded
the POSIX shell forms, so on a real windows-latest runner 12 of them failed on
the assertion string even though every command exited 0.

Add OS-aware assertion helpers (_assert_env_set / _assert_env_unset) that check
the right shell syntax for the host OS, and skip the two WSL-from-Linux shim
tests on native Windows (os.name is 'posix' inside WSL, so that path can't run
there). No change on Linux/macOS (57 passed); the connect command's behaviour
is untouched. Validated on a windows-latest staging runner.

* style(connect): tighten comments in the credential-leak fix

Condense the verbose explanatory comments and multi-line docstrings added by
this PR to one or two lines each, drop a few that just restated the code, and
keep the security rationale where it is load-bearing. Comment/whitespace only;
verified with unslothai/scripts comment_tools.py (check --strip-docstrings:
6/6 'code unchanged'). Tests unchanged: connect 57 passed, identity 5 passed.

* CLI/Studio: harden the identity handshake (review round)

Addresses the latest Codex/Gemini review of the handshake:

- Store the identity secret privately. sqlite3.connect created the auth DB
  world-readable under a 022 umask, so another OS user could read app_secrets
  and forge proofs, defeating the same-user assumption the handshake rests on.
  The auth dir and DB are now restricted to owner-only (0700/0600); the JWT
  secret and password hashes there get the same protection.

- Bind the proof to the server's listening port. The stateless HMAC(secret,
  nonce) was relayable: a process squatting the discovered port could proxy the
  challenge to the real Studio on another port and pass it back. The proof now
  covers the port the server actually listens on (from the socket, never the
  Host header) and the client checks it against the port it connected to, so a
  relayed proof from a different port no longer matches. Closes the manual-relay
  residual left after the redirect fix.

- Cap the identity response read (the server is still unverified at that point)
  and serve the identity route from a sync def so its first-call SQLite read
  runs in the threadpool instead of the event loop.

Tests: port-bound proof + relayed-proof rejection added; identity (5) and
unsloth_cli (58) suites pass. Verified end to end against a real backend
(auth DB owner-only, handshake + mint still succeed).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* CLI/Studio: bind the identity proof to the connection address, not just port

Follow-up to the port binding from the last review. Binding only to the port
left a cross-address relay: a squatter on a different loopback address but the
same port (for example localhost resolving to a squatter on ::1 while the real
Studio is on 127.0.0.1) could proxy the nonce to the real Studio and pass back
a proof that still matched, since both share the port.

The proof now covers the address and the port the connection landed on:

- Server: takes the address+port from request.scope, which uvicorn populates
  from getsockname, so it is the real local address the client reached even
  when Studio is bound to 0.0.0.0 (verified empirically), never the
  client-controlled Host header.

- Client: resolves the base host to one concrete IP, talks to exactly that IP,
  and binds the proof to (IP, port). A proof relayed from a Studio on a
  different address or port was computed for that other endpoint and no longer
  matches the one the client dialed.

Both sides normalise the address through ipaddress so equivalent forms compare
equal. Together with the private-secret and redirect fixes, this closes the
cross-user loopback relay an attacker can mount without reading the secret.

Tests: proof now bound to host+port; relayed-proof rejection retained; identity
(5) and unsloth_cli (58) suites pass. Verified end to end against a real backend
(handshake + mint still succeed).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* CLI: pick the loopback address at discovery so localhost does not regress

find_studio_server now resolves a bare localhost base to its concrete loopback
addresses and returns the first that answers /api/health, IPv4 127.0.0.1 first
(where unsloth studio binds by default). The whole flow (health probe, identity
check, credential send) then targets that one address instead of racing
IPv4/IPv6 resolution, where localhost could resolve ::1-first and hide a Studio
bound to 127.0.0.1. A literal IP or remote name is unchanged.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-21 21:28:38 -07:00
Daniel Han
ce193c243d
Keep server-side tools enabled under --secure (#6403)
* Keep server-side tools enabled under --secure and on every bind

--secure binds loopback and exposes Studio only through an authenticated
Cloudflare HTTPS tunnel, but it was grouped with a raw 0.0.0.0 bind and
force-disabled all server-side tools (web search, Python, terminal). The
process tool policy overrode the client's enable_tools request, so the
model was never told the tools existed and answered in plain text. The
plain 'unsloth studio' command had no way to re-enable and printed nothing.

Tools now default on for every bind. The bind host and --secure no longer
change the tool policy; only an explicit --enable-tools/--disable-tools
forces it on or off. Both 'unsloth studio' and 'unsloth studio run' accept
the flags and the startup banner states the resolved policy.

- run.py: replace _apply_default_tool_policy(host, secure) with
  _apply_cli_tool_policy(enable_tools); add an enable_tools kwarg to
  run_server and --enable-tools/--disable-tools to the argparse.
- _tool_policy.py: resolve_tool_policy defaults to on for every host and
  no longer prompts on a network bind.
- studio.py: drop the secure-as-public tool gating, add the flags to the
  plain command, and reword the startup banner.
- Update and extend the secure-flag and tool-policy tests.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add tool-policy notice to plain server banner and refresh run --help

Follow-up to PR review:
- run.py: the plain 'unsloth studio' / --secure / direct run.py path went
  through _emit_startup_output without any tool-policy line, so a
  network-reachable launch was silent about code execution now that tools
  default on. Thread enable_tools through _emit_startup_output /
  _emit_secure_startup_output and print a one-line policy notice, followed by
  a single stop hint.
- studio.py: the 'unsloth studio run' --enable-tools/--disable-tools and --yes
  help still described the removed loopback-on/network-off default and the
  confirmation prompt; reword to match the new policy.
- Add tests for the banner notice and the refreshed help text.

* Update CI tool-policy resolver tests for default-on behavior

tests/python/test_unsloth_run_tool_policy_resolver.py still asserted the
removed network-bind policy (0.0.0.0 and LAN IP default off, explicit enable
prompts and aborts on a declined prompt), so it failed the Python CI jobs.
Rewrite the truth table: every bind defaults on, explicit on/off always wins,
and the resolver never prompts (yes/silent/prompt kept for compatibility).

* Trim comments for the tool-policy change

Shorten the verbose docstrings and block comments added for --secure tool
handling; keep the security-relevant intent. Verified comment-only via an AST
diff (code unchanged).

* Add deterministic test that server-side tools execute under --secure

Drive the GGUF agentic tool loop with a fake llama-server stream and let the
real execute_tool run: python counts 1..100, terminal returns a UTC datetime,
and web_search runs through real _web_search with only the ddgs network
boundary mocked. A policy assertion pins that the post-fix --secure path
(policy None + per-request enable_tools) is what keeps these executions
reachable. No model, GPU, or live network; runs in the existing backend CI.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Align _emit_startup_output banner test with the moved stop hint

The tool-policy notice now prints between the access banner and the stop
hint, so the stop hint is emitted once at the end instead of inline in the
banner (include_stop_hint is False and print_studio_stop_hint runs once).
Update the plain-localhost case to match; the mismatch and wildcard cases
already asserted this wiring.

---------

Co-authored-by: Michael Han <michaelhan2050@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-18 05:52:40 -07:00
Nilay
fbed3f258c
CLI: add unsloth connect to point coding agents at a local Studio server (#6407)
* unsloth connect

* harden error paths, fix codex oss_provider routing, tighten key cache perms

* Increase timeout for studio server lookup and enhance key caching logic

* openclaw/opencode/hermes to connect

* improvements

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* error handling for requested models not loaded

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix claude connect env under WSL

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: imagineer99 <samleejackson0@gmail.com>
2026-06-17 19:26:55 +01:00
Daniel Han
9a966adf51
Studio: trim serving-log noise and surface llama-server engine stats (#6377)
* Studio: trim serving-log noise and surface llama-server engine stats

Studio prints one structured line per HTTP request, so the SPA's polling and
per-invalidation fan-out bury the lines that matter.

- Dedup identical successful GETs within a short window (default 300ms,
  UNSLOTH_STUDIO_ACCESS_LOG_DEDUP_MS) so a burst logs once. The dedup key
  includes the query string, so distinct query-driven GETs are not collapsed.
  Runs after the response is sent, so it adds no request latency; mutations,
  non-2xx, and loading polls are untouched.
- Collapse pure-liveness polls (/api/health, /api/auth/status,
  /api/inference/status, /api/inference/monitor) to a longer heartbeat
  (default 10s, UNSLOTH_STUDIO_ACCESS_LOG_POLL_DEDUP_MS). The API monitor
  console polls /monitor every 1.5s while open.
- Translate llama-server's Prometheus /metrics into a periodic vLLM-style
  engine_stats line (generation/prompt throughput and requests in flight) from
  a daemon poller, gated on UNSLOTH_STUDIO_ENGINE_STATS. Throughput uses
  llama-server's predicted_tokens_seconds / prompt_tokens_seconds gauges, with
  a tokens_predicted_total / prompt_tokens_total counter-delta fallback; it does
  not use n_decode_total (which counts llama_decode() calls, not tokens). No KV
  field is emitted, since llama.cpp does not expose kv_cache_usage_ratio.
  --metrics is added only when probe_server_capabilities reports the binary
  supports it, so older/custom binaries still load. The poller keeps retrying
  through transient scrape failures (stop() drives shutdown) and a malformed
  sample cannot crash its thread.
- api_monitor.append_reply: once the preview cap is reached, skip the per-chunk
  re-concat (avoids O(n^2) on long generations) while still recording the "..."
  truncation marker for a reply that lands exactly on the cap.
- unsloth studio --verbose and unsloth studio run --verbose both restore every
  per-request log; --verbose before a subcommand is rejected with guidance
  (matching --secure / --parallel). run --verbose still forwards --log-verbose
  to llama-server, preserving the pre-existing pass-through verbosity.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-17 05:37:57 -07:00
Daniel Han
40c8ad78b9
Studio: add --secure Cloudflare-only mode and revamp API usage examples (#6300)
* Studio: add --secure Cloudflare-only mode and revamp API usage examples

--secure / --not-secure on `unsloth studio` and `unsloth studio run`:
- --secure binds 127.0.0.1, requires the Cloudflare tunnel, and advertises only
  the Cloudflare link. cloudflared reaches the server over localhost, so the raw
  port is never exposed on a public interface.
- If the tunnel cannot start, fail closed with a clear message instead of
  silently leaving a raw 0.0.0.0 link.
- Default stays not-secure (no behavior change); coexists with the existing
  --cloudflare/--no-cloudflare flag. Host defaults are unchanged.
- /api/health (authed) now reports the live tunnel URL.

API usage examples (Profile > API):
- Example tabs for curl, Python, curl + tools, Python + tools, plus an OS row
  (Linux/macOS/WSL vs Windows) auto-detected from the platform.
- Windows curl passes the JSON body via a file so PowerShell does not strip the
  quotes when calling curl.exe.
- Python + tools forwards enable_tools/enabled_tools through extra_body and
  guards chunk.choices, since tool-lifecycle events carry no choices.
- Shows the loaded model name and the real API key while it is still revealed.
- A Cloudflare Tunnel toggle (default on) shows the public tunnel URL and uses
  it as the base_url in the examples when a tunnel is running.

Tests cover the tunnel start gate and the --secure flag on both commands.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: gate --secure tools on public exposure and harden API examples

In secure mode the server binds loopback but is reachable via the public
Cloudflare tunnel, so resolve the tool policy against the public exposure
(0.0.0.0) rather than the loopback bind. This keeps server-side tools off by
default and prompts before enabling them, instead of inheriting the loopback
default of on. The startup tool notice now names the public surface.

Also reject --secure with --no-cloudflare directly in run_server and the
run.py argparse (not only the CLI), JSON-encode interpolated model names so
Windows paths and quotes cannot produce invalid JSON or broken snippets, and
force-refresh /api/health on the API panel so a tunnel that starts after the
first health read still surfaces its URL.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: API examples show direct host when tunnel toggle is off; move Copy onto code

The Cloudflare Tunnel toggle had no visible effect when Studio was opened
through the tunnel: the off state fell back to window.location.origin, which
equals the tunnel URL in that case. /api/health now reports the direct
host:port (server_url), and the API panel uses it for the off state so it shows
the real non-tunnel base. Also move the Copy button out of the tab row and onto
the code block.

* Studio: highlight API examples, add advanced tabs, fix tunnel toggle row

Syntax-highlight the curl/PowerShell/Python snippets with the app's shared
shiki plugin (bash/powershell/python). Add 'curl + advanced' and
'Python + advanced' tabs that set temperature/top_p/top_k/min_p/
repetition_penalty/max_tokens, enable thinking, and turn on all tools.

The Cloudflare Tunnel row no longer shifts the code block: the tunnel URL is
always rendered (dimmed when off) so toggling keeps the row height constant.
Key the highlighted block on its content so it remounts when only the base URL
changes (the renderer's block memo otherwise kept a stale URL).

* Studio: rename API tunnel toggle to Secure HTTPS, hint --secure when exposed

Rename the API examples toggle from Cloudflare Tunnel to Secure HTTPS. When the
server was not launched with --secure, show an info tooltip noting the raw
0.0.0.0 port is still globally reachable and pointing at --secure. /api/health
now reports whether --secure was used so the hint is hidden in secure mode.

* Studio: force tools off for plain network/secure launches

The plain 'unsloth studio --secure' (and '-H 0.0.0.0') launcher re-execs run.py
and never installed a tool policy, so the process default (honor per-request
enable_tools) let any API-key holder run Python/terminal tools over the public
endpoint. Force the policy off at the run.py entrypoint when network-reachable
(0.0.0.0 or --secure); 'unsloth studio run' still installs its own resolved
policy and does not go through this path.

* Studio: apply default tool policy in run_server, not the run.py entrypoint

The plain launcher runs from the studio venv and calls run_server directly, so
it never hit the run.py __main__ guard. Move the network/secure default-off tool
policy into run_server so every launch path (plain, --secure, direct run.py)
gets it; the run subcommand still overrides it with its resolved policy.

* Studio: clarify --secure help text on the network exposure tradeoff

Spell out in --help (both unsloth studio and unsloth studio run, plus the
run.py argparse) that --not-secure also serves the raw 0.0.0.0 port reachable
from anywhere on the network, matching the API panel's Secure HTTPS hint.

* Studio: cache API-key PBKDF2 derivation to cut per-request /v1 auth overhead

validate_api_key re-ran the 100k-round PBKDF2 on every authenticated
request, adding ~15ms to each /v1 call made with an sk-unsloth- key.
Benchmarked against the bare llama-server it proxies to, API-key requests
carried ~22ms of fixed overhead vs ~7ms for the JWT path; the gap was
entirely this redundant key derivation (Pydantic validation measured
0.005ms, so it is not a factor).

The raw-key to hash mapping is a pure deterministic function of the fixed
server salt, so memoize it per process, keyed by a salted HMAC of the key
(never the key or a recoverable digest). The cached value equals what is
already stored at rest. Revocation and expiry remain enforced by the
SQLite read on every call, so a cache hit only skips the KDF, never the
active or expiry checks. Only keys that exist in the DB are cached, so
unknown-key spam cannot grow it.

After the change the API-key /v1 overhead drops to ~8ms, at parity with
JWT, while the at-rest PBKDF2 hashing is unchanged.

Adds test_api_key_expiry.py covering API-key and JWT expiry enforcement
and the new cache: it skips the KDF on repeat and still rejects revoked
or expired keys.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: tighten comments across the secure-tunnel and API-key changes

Condense multi-line comments and docstrings to one or two lines, drop the
ones that restate obvious code, and remove an orphaned test section header.
Comment-only: verified with comment_tools.py check (9/9 code unchanged), the
auth/secure-tunnel/CLI test suites, and a clean frontend typecheck and build.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-15 04:18:15 -07:00
oobabooga
4176448fb8
Studio: enable stdio MCP servers on a loopback bind (#6295)
* Studio: enable stdio MCP servers on a loopback bind

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: address codex review on stdio MCP loopback gate

* Studio: fix banner URL and preserve stdio MCP env opt-in on network binds

* Studio: scope loopback to exact aliases and honor force-disable on run_server reuse

* Studio: cover force-disable across a public re-bind and fix a stale test comment

* Studio: keep stdio MCP off on Colab loopback launches

* Studio: set tool policy before server startup

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: imagineer99 <samleejackson0@gmail.com>
2026-06-15 03:02:32 +01:00
James Dawdy
3e6920627c
fix(studio): load run.py by path for editable installs (#5909)
* fix(studio): load run.py by path for editable installs

`studio update` can leave a partial site-packages/studio/backend/ tree
(plugin build artefacts only). That shadowed tree wins over an editable
install and breaks `from studio.backend.run import ...`. Loading run.py
by file path via importlib sidesteps the conflict.

The module is cached in _RUN_MODULE so repeated calls are cheap.
If exec_module fails, the module is removed from sys.modules before
re-raising so a subsequent retry starts clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Handle None __file__ when checking cached run module for PR #5909

* Harden _load_backend_auth_storage against None __file__ and resolve cache-key path (PR #5909)

* Adapt studio run/cloudflare in-venv tests to _load_run_module loader (PR #5909)

---------

Co-authored-by: Jim Dawdy <jimdawdy@Jims-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-06-12 00:11:05 -07:00
alkinun
672d8f0581
Expose runtime context length for hub models (#6154)
* expose runtime context length for hub models

* runtime context helper review

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Etherll <61019402+Etherll@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-11 22:13:53 +03:00
Nilay
f64c3c8aba
Studio: add unsloth chat CLI command (#6170)
* Studio: add `unsloth chat` CLI command

Interactive chat REPL on the shared Studio backend: trained-model picker
when no model is given, /think and /compare toggles (adapter toggle on
CUDA, side-by-side base-model load on MLX), markdown streaming, and
connect-if-running Studio server mode so models stay warm across
sessions and are shared with the UI.

* fix settings

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix error handling and compare base precision

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix chat CLI backend imports and GGUF drafter loading

* Hide split thinking tags in chat CLI streams

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: imagineer99 <samleejackson0@gmail.com>
2026-06-11 16:09:34 +01:00
Daniel Han
1a99980b46
Studio: auto Cloudflare tunnel for 0.0.0.0 launches (#6204)
* Studio: auto Cloudflare tunnel for 0.0.0.0 launches

Binding Studio to 0.0.0.0 for remote access often leaves the raw
http://<ip>:<port> URL unreachable (https-vs-http, blocked high ports,
closed cloud security groups). On a wildcard bind, auto-start a free
cloudflared quick tunnel and show its https://*.trycloudflare.com URL in
the startup banner:

  Secure link access via Cloudflare: https://<random>.trycloudflare.com

- new studio/backend/cloudflare_tunnel.py: find or download+cache the
  cloudflared binary (per-OS/arch GitHub release, safe .tgz extract),
  start the tunnel, parse the URL, tear it down. Stdlib only; best-effort
  and non-fatal throughout (a missing binary or offline box never blocks
  or slows startup).
- run_server starts the tunnel for 0.0.0.0 only (skips loopback, api-only
  and Colab), prints the line in the banner, and _graceful_shutdown stops
  the child so it never orphans.
- --cloudflare/--no-cloudflare flag (default on) on `unsloth studio` and
  `unsloth studio run`, forwarded through the re-exec into run_server.
- tests for the helper, the CLI flag forwarding, and the run.py defaults.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio cloudflare: send a User-Agent on the cloudflared download

GitHub's CDN can 403 the default Python-urllib User-Agent on release asset
downloads. Set an explicit UA and pin it with a test.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio cloudflare: address review (opt-out for subcommands, tunnel teardown)

- reject --no-cloudflare placed before a subcommand (it would not reach the
  subcommand), mirroring the --parallel guard
- register the tunnel before waiting for its URL so a shutdown during the wait
  stops cloudflared instead of orphaning it
- tear the server + children down if `unsloth studio run` startup aborts
  (health timeout, model-load error, Ctrl+C) before the wait loop

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-11 07:10:08 -07:00