mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-24 00:04:14 +00:00
75 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
489fab4a71
|
Support OpenCode V2 in unsloth start (#9275)
* Support OpenCode V2 in unsloth start * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep stable OpenCode guide coverage * Follow the OpenCode V2 stable release * Honor OpenCode V2 policy and server semantics * Fix OpenCode V2 launch command recipes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
b69bfe0216
|
fix(studio): keep a slow install alive and name what it is downloading (#8805)
A desktop install was killed after two hours of wall clock regardless of progress. On a slow link the cu126 torch wheel takes longer than that on its own, so the install could never finish, and each kill wasted every byte already fetched because uv restarts an interrupted download from zero. The installer also keeps uv's output in a log it only prints on failure, so those two hours looked identical to a hang, both to the user watching and to anyone reading the logs afterwards. install.sh and install.ps1 now turn uv's announcements for downloads of at least 50 MiB into `[TAURI:DL]` / `[TAURI:DL_DONE]` protocol lines, while the per-package chatter stays in the log where it was: a full dependency set is dozens of announcements plus a line per installed package, which would bury the installer's own output. The app consumes the markers without displaying them, so a healthy install looks exactly as it did before. Once five minutes pass with nothing printed it reports the step, the package being fetched, its size and elapsed time, and it now gives up only at a twelve-hour backstop. Markers travel on stderr from install.sh, alongside the other protocol lines that function already writes there. That also keeps them clear of the verbose path's redactor, whose sed block-buffers its output: a marker queued behind it would reach the app only once the download it announces had finished. install.ps1 writes them on stdout, so both reader threads filter them out of the UI. Where awk is absent, which this script already supports elsewhere, the filter degrades to plain capture rather than closing the pipeline under the child. There is deliberately no rule that stops an install for being quiet. That needs evidence that work is happening, and the markers cannot supply it everywhere: the uv calls under `studio setup` capture their output rather than streaming it, so a silence rule would stop healthy installs in exactly the phase this change exists to protect. Extending the markers to those wrappers is follow-up work. Validated by driving both installers' real command wrappers against recorded `uv pip install` output -- `run_install_cmd` under sh, and `Invoke-InstallCommand` under PowerShell across both its quiet and verbose arms -- plus a timing check that a marker is readable while its download is still running, and the existing installer and watchdog suites. Both installers are pinned to the same 50 MiB threshold, so they cannot drift apart. Refs #8698 |
||
|
|
18dbedb889
|
studio: harden the launcher-refresh installer fetch (#8542)
* studio: ship install.sh / install.ps1 in the wheel so the launcher refresh stops fetching one `unsloth studio update` refreshes the desktop launcher by re-running the installer with --shortcuts-only. It looked for install.sh / install.ps1 in a checkout or at _PACKAGE_ROOT, which is site-packages for a wheel, found neither, and fell back to downloading https://unsloth.ai/install.sh and piping it into `bash -s` (or writing install.ps1 to a tempfile and running it with -ExecutionPolicy Bypass). That is the normal path for every PyPI install, so a routine update ran code from a second trust anchor: whoever can tamper with that response gets execution as the updating user, on top of the PyPI channel the package already came from. Rather than drop the fallback and lose launcher refreshes for PyPI users, ship the installers with the distribution. pyproject data-files puts them under <data>/share/unsloth, which pip and uv both resolve to the Studio venv root, so the local exec path now covers wheel installs and the network fallback is gone. - _installer_script_candidates(): checkout, _PACKAGE_ROOT, <data>/share/unsloth, sys.prefix, USER_BASE - both fetch-and-exec blocks replaced by a skip message that names the reinstall command - Windows wheel installs now take the existing `& 'path' --shortcuts-only` branch that checkouts already used, instead of the wrapper that regexed `Install-UnslothStudio @args` out of a downloaded script Verified: wheel and sdist both carry the installers, pip and uv place them at <venv>/share/unsloth, and `bash <venv>/share/unsloth/install.sh --shortcuts-only` regenerates the launcher and exits 0. Editable installs still resolve the repo root. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * studio: fetch the launcher installer from raw.githubusercontent, keep the bundled copy as fallback Follow-up to the previous commit, which pinned the launcher refresh to the installer that shipped with the wheel. That gave up hotfixing install.sh between releases, which is worth keeping: a launcher bug should be fixable without cutting a version. So the refresh fetches main again, with the parts of the old path that were actually dangerous removed. The fetch now goes to raw.githubusercontent.com/unslothai/unsloth/main directly. https://unsloth.ai/install.sh was only ever a Cloudflare 301 to that exact URL, so the bytes always came from the repo, but the hop put the unsloth.ai DNS and CDN control plane in the path of code execution on every updating machine. Going to the origin removes that without changing what runs. Also: - redirects off raw.githubusercontent.com, or to plain http, are refused rather than followed - a response that is not an installer (captive portal page, HTML error body, truncated transfer) is not executed. This is not a security control, anyone choosing the response can satisfy it, it is there because piping a hotel wifi login page into bash is a boring way to break a machine - the response is size-capped rather than read unbounded - the fetch falls back to the installer bundled under <data>/share/unsloth when the network is unavailable, so an offline update still refreshes the launcher instead of silently skipping it - UNSLOTH_NO_REMOTE_INSTALLER=1 pins the refresh to the bundled copy - a source checkout still outranks the network, so `update --local` tests its own installer rather than main's The Windows fetched path writes the script to a BOM tempfile and runs it with -File and the arguments appended, so install.ps1's own `Install-UnslothStudio @args` at EOF receives them. That deletes the regex that used to strip that line for the stdin path. Verified live on Linux: fetch returns 258755 bytes that validate and regenerate the launcher; UNSLOTH_NO_REMOTE_INSTALLER=1 regenerates it from the bundled copy; with neither, the refresh skips and prints the reinstall command. * studio: apply the installer host guard to the starting URL, not just redirects https://unsloth.ai/install.sh 301s to raw.githubusercontent.com, so a redirect-only check would have fetched straight through it if the constant were ever pointed back at the website. _is_allowed_installer_url now gates the first request with the same rule the redirect handler applies, and the redirect handler calls it too. Found by running the fetch against the unsloth.ai URL on an installed wheel: it succeeded rather than being refused. * studio: fall back to the bundled installer on more of the ways a fetch can go wrong Three from review, all real: A fetched installer that runs but exits nonzero consumed the fallback. If main is temporarily broken, or incompatible with the installed release, the launcher was left unrefreshed even though a release-matched copy was sitting on disk. Both fetched runners now report the exit code and return False so the bundled copy runs. HTTP framing errors escaped _fetch_installer. IncompleteRead from a truncated chunked transfer, and BadStatusLine / LineTooLong from a malformed proxy, are HTTPException and neither URLError, OSError nor ValueError, so they propagated out of an update that had otherwise succeeded instead of falling back. I had assumed IncompleteRead was covered by the ValueError clause; it is not on this Python. The bundled lookup missed the managed venv. A pip-installed CLI can drive an update into STUDIO_HOME/unsloth_studio (_studio_deps.running_outside_managed_venv), where setup writes the new data files; searching only the running interpreter's prefixes would have run the foreign CLI's older bundled installer, or none at all. Three tests added, one per case. * studio: tighten the comments around the launcher-refresh installer lookup * studio: let the managed venv lead the bundled installer lookup Follow-up to the previous commit, which added STUDIO_HOME/unsloth_studio to the bundled roots but appended it. A pip-installed CLI updating into the managed venv would then still find its OWN bundled installer first, at sysconfig/sys.prefix, and run that older copy rather than the release-matched one setup had just written into the target venv. Appending covered the case where the foreign CLI ships no installer at all, and missed the case where it ships an out-of-date one. The managed root now leads. Running inside the managed venv it is sys.prefix again and the existing dedup drops the repeat, so this only reorders the case it is for. * studio: tighten the two comments added after the last comment pass * studio: keep the launcher refresh on unsloth.ai and drop the bundled installer unsloth.ai and unslothai/unsloth are trusted, so the launcher refresh goes back to fetching https://unsloth.ai/install.sh and install.ps1, and nothing is shipped in the wheel. Fetching is the point: a launcher fix reaches users without waiting for a release, which shipping a copy would have given up. Reverted from the earlier approach here: - pyproject no longer ships install.sh / install.ps1 as data-files - no <data>/share/unsloth, sys.prefix, USER_BASE or managed-venv lookup, and no local fallback: a fetch that does not land skips the refresh, as it did before - no UNSLOTH_NO_REMOTE_INSTALLER, which only existed to pin to the bundled copy - site and sysconfig imports are unused again and go with it Kept, because none of it depends on distrusting the source and each one is a way an update could misbehave regardless: - a truncated or malformed response (IncompleteRead, BadStatusLine, LineTooLong) is an HTTPException, neither URLError nor OSError, so it used to escape and abort an update that had already succeeded. It is caught and the refresh is skipped. - the response is size-capped rather than read unbounded - a body that is not an installer, a captive-portal login page or an HTTP error page, is not piped into bash - https and the unsloth.ai -> raw.githubusercontent redirect chain are the only thing followed, checked on the first request as well as on redirects - a source checkout still outranks the network, so `update --local` tests its own installer rather than the published one Verified live: the fetch through unsloth.ai returns 260933 bytes that pass the shape check and regenerate the launcher; a fetch that 404s skips cleanly instead of raising. * studio: reject a truncated installer instead of piping half a script into bash Found by simulating the fetch against a local server that declares a Content-Length and then sends a third of it. response.read(amt) returns what arrived and does not compare it against the declared length; only a further read() does, raising IncompleteRead. The bounded read added here for the size cap therefore disabled the completeness check that the previous unbounded read() got for free. The markers this code looks for sit early in install.sh, so a body cut off a third of the way in still passed _looks_like_installer and was piped into bash as a half-written script: read(amt) -> 86251 of 258755 bytes, no exception, markers present read(amt)+read() -> IncompleteRead, caught, refresh skipped read() (pre-PR) -> IncompleteRead, uncaught, traceback So the bounded read was worse than what it replaced, not just different. The follow-up read() restores the check while keeping the cap, and returns b"" on the normal path. Two tests: a truncated prefix that still satisfies the marker check is refused, and a complete body still fetches. * studio: skip, do not abort, when the Windows temp script cannot be created tempfile.mkstemp() sat outside the try, and the write inside it was not guarded, so a full disk, a read-only or missing %TEMP%, or antivirus holding the handle raised OSError straight out of `unsloth studio update` after the package update had already completed. Same class as the HTTP framing escape fixed earlier: a best-effort launcher refresh must not turn a successful update into a traceback. Both are now reported and skipped, and the write failure still unlinks the file it created. Two tests cover it. Also corrects the docstring: the pre-PR fetched path already used a BOM tempfile with -File. What changed is that the args go after the path so the installer's own `Install-UnslothStudio @args` receives them, which is what removed the regex that used to rewrite that line. * studio: stop the shape check from rejecting a legitimate future installer Two ways the check could have disabled every wheel-based launcher refresh, silently and permanently, until new Python shipped. `<#` opens PowerShell comment-based help, an entirely ordinary way for install.ps1 to begin, and the HTML guard rejected any body starting with `<`. It now matches the markup it means (doctype, html, head, body, xml) and explicitly allows `<#`. The markers pinned internal names: create_studio_shortcuts is a shell function and Install-UnslothStudio a wrapper, both renameable without changing the command-line contract. Only `--shortcuts-only` is required now, which is the flag this code passes, so an installer lacking it cannot serve the request regardless. The size floor drops to 512 bytes for the same reason: a smaller legitimate wrapper should not be refused. Three tests: a help-block script passes, real HTML and XML error bodies are still refused, and a renamed internal helper does not break the check. * studio: close the fake handle's descriptor in the Windows tempfile test The stand-in handle took the descriptor from the patched os.fdopen and never closed it. Windows will not unlink a file that still has an open descriptor, so the cleanup assertion failed there for a reason unrelated to the code under test. The handle now owns and closes the descriptor, and the test asserts the descriptor is closed so a leak fails on Linux too. * studio: keep the refresh fallbacks the refactor dropped Two behaviour differences against main that had nothing to do with hardening. A local installer that cannot be launched no longer ends the refresh. The old candidate loop caught the exec OSError and carried on to the next candidate and then to the network; the helpers now report that with a return value so the caller falls through the same way. The fetch no longer discards a site-wide urllib opener. It used to call urlopen, which honours install_opener, so a machine whose proxy auth or corporate CA lives in a site handler reached the network through it. Validating the redirect chain needs our own opener, so the installed handlers are carried across instead of dropped. * studio: stop the installer opener touching global urllib state Carrying the site-installed handlers into the fetch opener was wrong twice over. OpenerDirector.add_handler assigns handler.parent, so sharing them repointed the installed opener's own handlers at this one and broke every later urlopen in the process, and copying them instead does not deliver the intended benefit either because the default HTTPSHandler in the new opener already answers first. The opener is private again. Proxy environment variables and the system trust store still work. A machine whose proxy auth or CA lives in a programmatically installed opener skips the refresh, which the update survives. * studio: try every on-disk installer before reaching for the network The bool fall-through restored the network fallback but not the rest of what the pre-refactor loop did: it selected only the first candidate that existed, so a second one was dropped when the first could not be launched. An update --local with a distinct checkout could therefore run the published installer while a usable one sat on disk. Candidates are collected rather than reduced to the first, and the caller stops at whichever one actually launches. * studio: tighten the comments added by the launcher-refresh change --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
098a6a0957
|
Studio: honor a request's enable_tools: false instead of overriding it (#8547)
* Studio: honor a request's enable_tools: false instead of overriding it The process-wide tool policy was an override, not a default. unsloth studio run installed set_tool_policy(True) at startup, and _effective_enable_tools returned that value whenever it was non-None, so the request's own enable_tools field was never read. The Studio UI sends its tool pills as an explicit request field, and expresses 'every pill off' by omitting enable_tools entirely. Against a True override that omission read as 'tools on', and with enabled_tools also absent the route selected ALL_TOOLS, so a chat with every tool switched off still advertised web_search, python, terminal and render_html. Thread-title generation, which posts to /v1/chat/completions with no tool fields, picked them up the same way. Only unsloth studio run installed the policy, so unsloth studio, the desktop app and Colab behaved correctly and the two commands disagreed on the same UI. Split the policy into two slots. The override still comes from an explicit --enable-tools/--disable-tools and still beats the request. The new default is what an omitted enable_tools falls back to, installed as True by every launcher, so tools stay on for every bind including --secure. A request that says enable_tools: false now turns them off. Frontend sends the off state explicitly rather than by omission, in the local chat, external provider and token-count paths, and pins enable_tools: false on title generation so a 24-token summarisation never carries tool schemas. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: do not let the tools-on default answer a request that stated its intent The safetensors/MLX path resolves _sf_tools_on straight from _effective_enable_tools, without the two withdrawals the GGUF router applies. So the launcher default introduced here reached requests that had already expressed their own tool intent: - tool_choice: "none" with enable_tools omitted resolved to tools on, and with no enabled_tools allowlist that selected every built-in, python and terminal included, turning a standard opt-out into server-side execution. - A client tools catalog with enable_tools omitted made _sf_client_tools false, so the request left the client-tool passthrough for Unsloth's own loop and the caller got built-ins instead of calls for its own functions. The GGUF router avoids both with _client_disabled_tool_calls and _explicit_studio_tool_loop_requested. Draw the same line on the safetensors gate: the default only answers a request that said nothing, so tool_choice: "none", a client catalog, or tool-result history withdraws it. An explicit enable_tools/mcp_enabled ask, and a CLI --enable-tools or --disable-tools, are unchanged. Also read the resolved _sf_tools_on in the _sf_client_tools gate rather than recomputing _effective_enable_tools, which would have hidden the withdrawal. * Studio: let a response_format contract withdraw the tools-on default too _takes_tool_passthrough already ends with _extract_response_format(payload) is not None, so on the GGUF router a structured-output request keeps the passthrough and never enters the server tool loop. The safetensors withdrawal missed it, so a request supplying response_format while omitting enable_tools still resolved _sf_tools_on to true and could select and run every built-in. response_format is not a declared field on ChatCompletionRequest; the model is extra=allow and OpenAI-SDK clients spread extra_body at the top level, so read it through _extract_response_format rather than an attribute. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: resolve the tool policy before protocol selection, and scope the default to unsloth studio run Two fixes for the same root cause: the tools-on default reaching code built around an omitted enable_tools meaning no tools. _sf_server_tool_intent read the raw policy while the withdrawal ran ~40 lines later, so a tool_choice: "none" or response_format request classified the response protocol on the template's tool_use branch and then generated on the plain one. On a model whose reasoning markers live only in the tool template the extractor starts in the wrong mode and can return the answer as reasoning_content. Resolve _sf_cli_policy / _sf_tools_on / _sf_mcp_allowed once, above the classification, and derive the intent from the resolved value. The default is also no longer installed by run_server. It belongs to unsloth studio run, the launcher that has always forced tools on, and which installs it itself. Installing it in _apply_cli_tool_policy extended it to unsloth studio, the desktop app and Colab, where paths that assume an omitted enable_tools means no tools started seeing it: n > 1 is rejected by the tool loop though the plain path implements it, max_tool_calls_per_message: 0 still advertises schemas and the nudge, and the pre-switch passthrough guard does not recognise tool-result history so it 400s a non-streaming continuation. Those paths predate this PR and are unchanged on unsloth studio run; scoping the default keeps them that way everywhere else. unsloth studio run still defaults tools on for every bind, --secure included, and a request's enable_tools: false is still honored. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: correct the tool-policy help and docstrings for the scoped default Scoping the tools-on default to unsloth studio run left three places claiming it applies everywhere. The --help for plain unsloth studio and for a direct run.py launch both said 'Default: on for every bind', which is now the opposite of what those launchers do, and the tool_policy module said 'Launchers install True'. A --help line about a tool-execution default is worth keeping exact. unsloth studio run's own help is unchanged, since it is the launcher that does default them on. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
a867496a28
|
Studio: launch a DFlash speculative drafter automatically (#8338)
* Studio: launch a DFlash drafter automatically Studio has recognised dflash-*.gguf since #7811, but only to hide it from the quant picker. Nothing ever launched it, so a model that ships a DFlash sidecar fell through to no speculative decoding at all. Add DFlash as the third launchable drafter kind beside MTP and DSpark: a _is_dflash_drafter_path predicate, local and Hub discovery, a supports_dflash capability parsed from llama-server --help, and the --model-draft / --spec-type draft-dflash emission. Unlike DSpark it is on under Auto, since the published sidecar is 1.52 GiB and ships in the model's own GGUF repo rather than being an ~11 GB opt-in fetch. DSpark keeps first refusal when a repo somehow ships both, matching llama.cpp's own downloader. Discovery confirms general.architecture = dflash in the header rather than pairing on the filename: the published sidecar is dflash-kquant.gguf, which names no model family, so the DSpark pairing rule would reject the one file this exists to find. The dflash/ directory is still not a drafter marker, and DFlash is still excluded from companion reclaim, both because the name doubles as a family a publisher puts on real weights. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Harden the DFlash drafter fallback, pairing and dedupe Four fixes from review of the auto-launch path. Strip user-supplied DFlash args on the drafterless retry. The gate that enters the retry counts a DFlash request, but the cleanup only recognised MTP and DSpark. llama.cpp accumulates speculative types, so prepending --spec-default while the DFlash group survived relaunched the drafter that had just failed, and a main model that loads fine without it was lost instead of recovered. Skip a DFlash sidecar that names another weight in the same folder. _drafter_matches_weight is False both for a sidecar naming no family and for one naming a different family, so ranking put them in one bucket and precision could float the foreign one to the top: loading model B beside dflash-model-A-Q8_0.gguf and dflash-kquant.gguf launched model A's drafter. Both files carry a real dflash header, so the architecture check behind the ranking cannot catch it. The decision is made against the weights actually present in the folder rather than by guessing which stems are precision tokens, which keeps the published unpaired sidecar eligible. Stand the Auto DFlash fetch down once DSpark has resolved. DSpark takes first refusal in the promotion, so for a repo shipping both kinds the DFlash sidecar could never launch and the fetch spent bandwidth and cache on a file that would not be used. An explicit dflash request still fetches. Keep Auto deduplicated after a failed DFlash drafter. _speculative_type is reset to "default" by a successful drafterless retry while the launch still records the resolved sidecar, so the next Apply compared the intent's empty MTP path against it and reloaded a healthy server. _spec_drafter_kind survives the fallback and now decides the comparison. test_mtp_drafter_companion.py, test_native_gguf_companion.py, test_llama_cpp_mtp_detection.py and test_resolve_quant_gguf.py: 489 passed, including two new tests for the foreign-sidecar case and for the paired sidecar still winning. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep DFlash discovery, the training guard and the hints in step Discovery now accepts the dflash- prefix only. The shared companion predicates recognise DFlash by that prefix, so a <model>-dflash.gguf accepted by discovery was also a selectable Q8_0 main model in the quant picker, and choosing that variant handed llama-server the drafter as the target. Teaching the predicate the suffix instead would hide a real model whose name merely ends in DFlash, which is the case #7811 exists to protect, so detection gives the form up rather than the picker giving up a model. No published sidecar uses it; the shipped one is dflash-kquant.gguf. The same mismatch exists for MTP on main and is left alone here. The training VRAM guard now sizes a drafter named through llama_extra_args. Discovery never fills gguf_dflash_file for a file outside the model directory, but load_model still passes that path to llama-server, so a load could be admitted beside a training run while nothing was charged for the sidecar it makes resident. The Speculative Decoding hint said Auto picks DSpark or else MTP / ngram and that everything but DSpark leaves output unchanged. Auto now picks DFlash too, and like DSpark it is not bit-identical on quantized targets. The Draft Tokens hint gained the DFlash default, which shares the MTP branch at 2 on GPU and 3 on CPU/Mac. 514 passed across the drafter, companion, detection, quant-resolution and picker suites, including two new tests pinning the suffix form out of discovery and the prefix form still in. * Pair the remote DFlash sidecars with the selected weight detect_dflash_file already refuses a sidecar named after a NEIGHBOURING weight, so a folder holding two families cannot attach a foreign drafter locally. The download picker and the offline cache reuse still ranked every dflash-*.gguf by precision and name alone, never comparing a candidate against the weight being loaded, so in a repo hosting more than one family dflash-model-A-Q8_0.gguf outranked the generic dflash-kquant.gguf and model B downloaded and launched model A's drafter. The pairing rule now lives in one place, dflash_repo_preference_key, built on the same _drafter_names_other_weight predicate the local scan uses: a sidecar naming this weight's family first (most specific stem first, as detect_mtp_file does), then one naming no weight present here, then one naming a neighbour. The last is demoted rather than dropped, so a repo whose only sidecar looks foreign still has a fallback. Deciding against the weights actually present is what keeps the published unpaired sidecar eligible: dflash-kquant.gguf has a precision token for a stem, not a family name, so "the stem is non-empty" cannot stand in for "this names another model". Nothing changes for a repo with one sidecar, and with no weight in hand the order is precision only, as before. Tests cover a multi-family repo picking the generic sidecar, the same repo picking the specific one for its own weight, the shipped Muse-Glimmer layout still resolving, and the cached path agreeing with the download path. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Validate DFlash candidates and size the extras drafter once Three fixes found in review of the DFlash drafter work. detect_dflash_file read a candidate's GGUF header before asking the caller's accept callback about it, so a dflash-*.gguf symlink in a directory reached through a native grant had its out-of-lease target opened before the grant check ran, and no later rejection takes a read back. The loop now resolves the launch path, runs accept, and only then parses the header and applies the architecture check. accept still receives the resolved launch path, and callers that pass no accept see the same candidates in the same order as before. The training admission guard charged the llama_extra_args --model-draft sidecar on top of the local one discovery had already found, so a 1.5 GiB drafter was billed as 3 GiB and the guard could refuse an inference load that fits. The effective draft path is now sized exactly once, with identity taken from the resolved path so a symlink or another spelling of the same file dedupes too. That same charge also satisfied the local-weights early return on its own. Loading a remote GGUF repo has no local main weight, so a local --model-draft made the guard return the drafter alone and skip the listing that prices the target model, which could admit a load that then exhausts VRAM next to a running training job. The local branch now fires only when a local weight is actually present, and the drafter is added to whichever branch produces the estimate, including the remote one. Regression tests for all three. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Validate remote DFlash files by header and stop charging unused DFlash bytes Two fixes to the DFlash sidecar paths. Remote and cached DFlash candidates are now confirmed by their GGUF header, not by their filename. _pick_dflash and _cached_repo_dflash_drafter selected with _is_dflash_drafter_path, a dflash- prefix test, while the local scan in detect_dflash_file also required general.architecture == dflash. A remote repo holding an ordinary weight whose basename starts with dflash- therefore had that full weight downloaded and handed to llama-server as --model-draft, which falls back at startup after the bytes are already spent. The architecture rule moves into is_dflash_architecture in model_config, beside the naming rules and shared by every path, the way dflash_repo_preference_key already is. The header is only readable once the file is on disk, so the download validates after the fetch and falls through to the next candidate instead of returning None; the prefix-only naming rule is unchanged. The training coexistence guard no longer charges DFlash bytes a load under Auto will never fetch. _remote_gguf_companion_bytes added the preferred DSpark and the preferred DFlash sidecar whenever the repo listed both, but the loader stands down on the DFlash fetch once DSpark resolves under Auto, so those bytes are never resident and the guard could 409 a load that fits. The new dspark_first flag mirrors that selection. Where the choice is genuinely unknown the deliberate over-estimate stands, and an explicitly forced DFlash still pays for its sidecar. Regression tests cover the fetch falling through an impostor to the real sidecar, an all-impostor repo recording a permanent absence, the snapshot reuse and offline cache lookups applying the same rule, and the Auto guard charging DSpark only when a repo publishes both kinds. * Gate the DFlash stand-down and the guard's sizing on what the load actually does The Auto DFlash fetch stood down whenever _download_dspark answered with a path, but that call deliberately reports an already-cached DSpark sidecar even on a binary with no usable --spec-type draft-dspark (so the route's reuse check does not reload the same server on every Apply). The promotion refuses such a path, so on a DFlash-capable binary a repo shipping both companions suppressed the DFlash fetch for a sidecar that can never launch and the load ended up with no drafter at all. The capability gate now lives in _dspark_wins_auto, shared by the fetch and the promotion so the two cannot disagree. _remote_gguf_companion_bytes still ranked DFlash candidates with the name-only dflash_preference_key while the loader moved to the family-aware dflash_repo_preference_key, so in a multi-family repo the guard could price a different, smaller sidecar than the one that lands. The selected weight name is threaded down and the guard now sorts with the downloader's key over the neighbouring weights from the same listing. * Apply the load's boundaries to drafter discovery, and size Auto's one drafter ModelConfig.from_identifier ran the local companion scan with no way for the caller to say what was in bounds, so a native-grant load read the header of a dflash-*.gguf symlinked out of the granted directory. The validated rescan on the load route rejected it afterwards, which does not take a read back. The boundary now travels into the scan, for all three drafter kinds, so the two passes cannot disagree about what is in bounds. Remote DFlash discovery matched the basename in any nested directory, but the local contract is root level only: a quants/dflash-*.gguf is an ordinary weight detect_dflash_file would never offer, and the header can only be read once the bytes are here, so the whole weight downloaded before the rejection. Checked through a separate predicate so the prefix-only naming rule the other callers share stays exactly as it is. A split companion is only usable as a whole set, since llama-server resolves the sibling shards from the first one's directory. Fetching just the picked shard left a drafter whose header reads fine and which the server cannot open, so the load fell back to no speculation with nothing to show for the download. The companion download now resolves its shards with the same helper the main-model download uses, and neither reuse path reports a half set as a cache hit. The remote sizing charged the first-ranked DFlash candidate, but a rejected candidate falls through to the next name in the ranking, which can be a larger file; headers are unreadable from a listing, so the bound now covers every candidate the fallback can reach. And under Auto the guard charged the MTP drafter on top of the DFlash sidecar that replaces it. Auto launches exactly one drafter, in a fixed order, so dspark_first now expresses the whole promotion: DSpark alone when the repo publishes one, otherwise the larger of the DFlash bound and the MTP drafter, since every DFlash candidate can still be turned away on its header and the load then keeps the MTP one. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Budget a split DFlash sidecar as a set, and reject half a cached one The remote sizing bounded the DFlash fetch with the largest candidate the post-fetch fallback could land on, but each entry is one shard, while _download_companion_gguf fetches the whole shard set the picked file belongs to and llama-server keeps every shard resident. A sidecar published as two 1 GiB shards was budgeted at 1 GiB, and under-charging is the direction that waves a load through and then exhausts VRAM beside a running training job. The candidates are grouped into their sets with _gguf_extra_shards, the same helper the download resolves shards with, and the bound is the largest set total. _cached_repo_dflash_drafter's offline fallback accepted a candidate on is_file plus its header, so a snapshot holding shard 1 alone was handed back as the drafter with no fetch left to complete the set. The header reads fine, then llama-server cannot open the siblings it resolves from that directory and the load falls back to no speculation. Same _drafter_split_is_complete rule the snapshot reuse already applies, and skipped rather than fatal like the header check, since another snapshot may hold the complete copy. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Move drafter naming, ranking and DFlash discovery into a drafters package Pure structural move, no behaviour change. The shared primitives, the ranking keys and the DFlash detector were spread through model_config alongside unrelated model handling, and the same rules are reached from four different paths, so they now live in one package. model_config re-exports every moved name, so callers and tests that import them from there keep working. The package deliberately does not import model_config at module import time. The GGUF split and quant naming helpers stay where they are, since non-drafter code shares them, and are imported per call instead. * Give the guard's DFlash bound a name and a home The bound was fifteen lines of generator plus the comment explaining why it is a max over shard sets rather than the best-ranked candidate, inline in the middle of a function that also sizes mmproj, MTP and DSpark. It is pure arithmetic over a listing, so it moves to drafters.budget with the reasoning attached to it. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: fix the DFlash lint gate, and carry over what #6747 got right Five changes on top of the DFlash drafter work. Lint gate. The compatibility shim re-exporting the moved drafter helpers from model_config tripped scripts/verify_import_hoist.py, whose __all__ exemption is scoped to package __init__.py and which ships a reexport_in_ordinary_module_is_still_blocked self-test. The shim is gone: the module imports only what it still calls, and every other call site imports from utils.models.drafters directly. dspark_preference_key stays reachable from model_config as a delegating def, because repointing routes/inference.py's pre-existing function-local import is the verifier's TARGET-CHANGED case and its relocation exemption only covers module-level imports. Download plan. preferred_dflash_sibling in hub/utils/gguf_plan.py, and the sidecar as an expected file on every GgufVariantPlan. The sidecar was fetched but the hub manifest never knew about it, so download progress under-counted by ~1.5 GiB. Ranked with dflash_repo_preference_key, so the plan and the loader cannot disagree, and per variant, so a multi-family repo does not hand variant B the drafter named after variant A. Capability-regained retry. A load that stood down because llama-server could not run the drafter told the user to update, then deduped the reload the update was meant to repair. spec_binary_fallback_can_retry re-reads the binary, asking about the capability the drafter kind actually needs rather than the reason code, since every kind records the same binary_no_mtp. Transient fetch retry. _download_companion_gguf gained on_transient_failure, so a listing that never answered or a download that dropped is worth one more Apply. Permanent Hub errors, a full or unwritable cache, offline mode and cancellation are unaffected, and a header rejection still falls through to the next candidate rather than counting as transient. The probe cache key moved from (path, int(mtime)) to (path, st_mtime_ns, st_size), so an update landing in the same second as the probe is not answered with the old build's capabilities. CLI. unsloth_cli/_inference.py passes gguf_dflash_file into the GGUF load, so the managed CLI path engages DFlash instead of silently running without it. No vision gate, now measured rather than argued. Muse-Glimmer-30B UD-Q4_K_XL with mmproj-kquant and dflash-kquant, llama.cpp b10342, one B200, n_max=2, greedy, on a prompt carrying ~545 image tokens: 92.1 to 114.2 tok/s at 0.646 acceptance, greedy output byte-identical to the drafter-free run, no load failure. The comment at the Auto promotion site cited llama.cpp #22673, which is an MTP result, for a DFlash decision; it now cites the measurement. Also fixes test_from_identifier_never_reads_a_sidecar_outside_the_boundary, which patched is_dflash_architecture on the re-exporting module rather than the one detect_dflash_file resolves it in, so its reads == [] assertion held whether or not the lease boundary worked. * Tighten the DFlash comments for PR #8338 * Apply ruff-format kwarg spacing for PR #8338 * Fix the DFlash download plan and two stale-state reloads for PR #8338 Five review items, all reproduced first. The download plan promised the wrong files. A split sidecar contributed only its first shard, so the variant read complete while the loader's completeness check then refused the companion; it now carries the whole shard family. The pairing weight came from the listing's first sibling while plan_from_expected_files keeps the lexicographically first family, so a two-family variant key planned the discarded family's sidecar; both now use the kept family. And a root-level dflash- prefix is one real weights carry, which a listing cannot tell apart from a drafter, so a 54 GB model was planned as a companion to a 15 GB variant; a candidate is now bounded by the weights it would draft for, since a drafter is a few layers of its target and cannot outweigh it. The training coexistence guard charged the Auto DFlash sidecar even when extra args owned --spec-type, which stops the loader's promotion, so a chat load could be refused with 409 for bytes nothing would open. Extra args asking for draft-dflash keep the charge. The diffusion early-return cleared the speculative fallback state but not the DFlash retry flag, and discovery runs before the metadata read that classifies the model, so a transient sidecar failure tore down a healthy diffusion server on every Apply. Each fix has a regression test that fails without it. * Carry the DFlash plan bounds into the runtime paths for PR #8338 Four review items from the second round, each reproduced first. The budget still charged a forced dflash mode when extra args owned --spec-type. _build_speculative_flags returns before any mode branch in that case, so neither the forced mode nor the Auto promotion reaches the sidecar; only extra args asking for draft-dflash themselves still pay. The runtime picker had no size bound, so a root-level ordinary weight carrying the dflash- prefix downloaded in full before its header could be read, which is exactly what the download plan now refuses. It applies the same bound, sized from the repo listing, and an unavailable size leaves the candidate eligible as before. A permanent listing error records no answer at all, so _dflash_sidecar_absent stayed False and the drafter_not_found arm relaunched a healthy drafter-free server on every Apply. DFlash asks through _dflash_retry_needed instead, which is set only for the failures worth another attempt. A listing holding part of a split companion returned its first shard as usable, contradicting the complete-set checks on snapshot and cache reuse and handing llama-server a set it cannot open. The filename carries the set size, so the listing is now checked before the download. Each fix has a regression test that fails without it. * Make the DFlash size and split rules agree across plan, fetch and guard for PR #8338 Six review items from the third round, each reproduced first. Five are places the previous round's rules had not reached. dflash_plan_files now filters candidate families before ranking rather than after, so a half-published split set or an oversized ordinary weight at the top of the order steps aside for a usable sidecar behind it instead of taking the plan down with it. It also applies the split-completeness rule the runtime got last round, since planning a set the listing only half carries reports the download complete and then loses DFlash. The runtime size bound compared the picked shard rather than its whole set, so a split ordinary weight whose halves each sit under the target still downloaded in full. It sums the family now, through a shared helper. The training coexistence guard took the maximum over every root candidate with no size bound at all, charging gigabytes for files the fetch itself refuses. dflash_budget_bytes takes the target size and drops them. The incomplete-split rejection added last round lands after outcome["listed"] is set, so DSpark read a settled answer as retryable and relaunched a healthy server on every Apply. It records absence explicitly. SpeculativeType omitted dflash, so Typer rejected --speculative-type dflash before any of the new loading code ran and the mode was reachable only through Auto. Each fix has a regression test that fails without it. * Price DSpark by shard set and share one split-listing rule for PR #8338 Four review items from the fourth round, two of them under-charges that could admit a load beside a running training job and then exhaust VRAM. The guard priced a remote DSpark sidecar as the single file the ranking picked, while llama-server maps every shard of a split set, so a two-shard sidecar was budgeted at roughly half its resident weight. DSpark candidates are grouped into shard families now and the selected family's total is charged, matching what DFlash already did. Auto granted DSpark first refusal on the strength of the listing alone. Since the fetch now refuses an incomplete split set, the load falls through to DFlash, which can be the larger of the two, and the guard had already returned the DSpark figure. Only a complete set settles it. The runtime DFlash picker filtered incomplete families after ranking rather than before, so a half-published set at the top of the order returned a shard, _download_companion_gguf refused it, and the loop ended instead of reaching the complete sidecar behind it. Extras owning --spec-type with their own --model-draft charged the discovered sidecar as well, though _build_speculative_flags returns before that one is emitted. Only the drafter that launches is charged now. Extras without --spec-type still charge both, since Studio emits its own and which lands is genuinely unknown. The listing completeness rule was about to have three copies, so it moved into utils.models.drafters as split_listing_is_complete and the plan, the fetch and the guard all call it. Each fix has a regression test that fails without it. * Tighten the DFlash review-round comments for PR #8338 Comments and docstrings only, no code change: verified with comment_tools.py check and the prepush gate's comment-only mode. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
5f5f1e3550
|
Count a RECORD path claim before the row is filtered out (#8181)
* Count a path claim before the row is filtered out * Tighten the comments on the reordered claim count --------- Co-authored-by: danielhanchen <danielhanchen@gmail.com> |
||
|
|
c2c16b7293
|
Studio: exempt shared top-level trees from the integrity check for our own wheels too (#8179) | ||
|
|
bece6d2ab4
|
Preflight the Codex GGUF check before loading the model (#7873)
* Preflight Codex GGUF check before the model load * Skip the Codex GGUF preflight for remote studio targets * Preflight owner-less shorthands and ignore auxiliary GGUFs * Filter unambiguous big-endian GGUFs in the Codex preflight * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Honor HF offline mode and skip Codex preflight for unverified loopback servers * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Stub the hub listing in fake_studio so codex tests stay offline * Defer bare names to attached servers and gate the hint probe to hub ids * Ask the attached server for GGUF variants and guard malformed HF_ENDPOINT * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Skip the attach variants probe for direct .gguf files * Probe shorthand candidates on attach and gate preflight to auto-starts * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Resolve gguf-variants with load-path parity and defend the attach probe * Run the Codex preload gate only when a load is imminent * Keep sibling quants and vision for a direct gguf in a marked directory * Settle live raw attach answers, complete direct-file variants, filter drafters * Track the existence-first local branch in the picker contract test * Refuse auxiliary direct ggufs, round-trip loose quants, gate the requested variant * Refuse companion paths, casefold variant match, keep local answers local, mark torn splits * Gate direct-path variants, mark empty and mis-indexed splits partial * Judge direct variants with parent context and require non-empty split siblings * Accept exactly the direct-file variant labels the server resolves * Mirror the variant fallback label and fail incomplete loopback direct files * Resolve symlinked shards, ask the server for foreign direct variants, match local answers strictly * Accept the bpw-stripped label when resolving a loose gguf variant * Report local resolution to the gate, settle explicit-path empties, accept full stems * Carry local resolution through the legacy route and label rows with the load extractor * Accept bpw-stripped labels at the gate and refuse big-endian direct files * Ignore torn local rows for variant matching and refuse broken direct symlinks * Scope readiness to the file the load uses and align label acceptance * Resolve advertised labels, prefer whole variantless picks, filter be rows by the loader * Probe remote direct paths, align split grammar, filter local be rows by the loader * Keep quant tokens to the basename and judge legacy local answers the same * Mirror basename tokens, settle local empties, and probe unpickable local shapes * Follow symlinked shard sets and let a named sibling answer for refused paths * Judge readiness by the load split grammar and keep hub-shaped gguf ids remote * Answer the pre-load gate with the load resolver instead of mirroring it * List every resolver-accepted label and let the probe judge direct variants * Follow split symlinks through the load, flag cleanable rows, and defer non-local raw answers * Bind completeness and aliases to the resolver's chosen file, one walk per row * Fail a direct gguf path this process can see is missing * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Offer the relative fallback label and ask the server about nested drafter folders * Ask the server about drafter folders and let the resident model answer first * Apply pinned ruff formatting * Resolve relative bases, compare resident quants exactly, trust loadable on empty answers * Run the preload gate when a run knob changes the load intent * Follow upstream: a direct file loads itself whatever quant was asked for * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Report a missing resolved path as not loadable * Refuse companions with any variant, defer foreign path syntax, keep symlink aliases * Confirm the loaded identifier before treating a direct path as resident * Tighten the comments added by this change * Cover the Windows side of the path syntax guard for PR #7873 * Check direct-file readiness even when a variant is supplied for PR #7873 * Judge the completion scan's big-endian filter with the loader label for PR #7873 * Normalize a local identifier before probing it for PR #7873 * Keep a read error unknown instead of a definite absence for PR #7873 * Confirm the server is this machine before judging its filesystem for PR #7873 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep a locked direct file unanswered instead of authoritative for PR #7873 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Judge WSL drive paths with the loader's normalizer for PR #7873 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Probe unjudgeable spellings and honor negative verdicts for PR #7873 * Tighten comments for PR #7873 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com> |
||
|
|
e0a2bd8317
|
Studio: preserve the Windows launcher during updates (#8092)
* Fix Windows Studio launcher updates * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix the remaining damaged-file reports and launcher recovery for PR #8092 The report this PR targets listed three damaged files. Preserving the launcher fixes one; the other two still fail the update on their own, since _fail_if_install_damaged exits 1 on any finding. Both are produced by our own installer, so the update they fail is the update meant to repair them. einx and torchao both ship a top-level test/conftest.py, and install_python_stack.py force-reinstalls torchao every update, so pip deletes the file and the pinned torchao does not ship it. package-lock.json is rewritten in place by setup.ps1 and setup.sh, which run npm install inside the installed tree; under legacy-peer-deps npm dedupes hoisted entries and the file shrinks below its recorded size, reproduced exactly as 28473 to 27225. Drop both classes while reading RECORD rather than when reporting, so a filtered row also stays out of the ownership tally and the limit budget and cannot crowd out a real finding. Mirrored into the sidecar scanner, whose docstring asks for the two predicates to be kept in sync. Also three fixes to the transaction itself: - Recover from the hardlinked bin/unsloth.exe shim, which survives the old updater's .deleteme unlink. - Warn instead of exiting when the launcher is missing or invalid. An install already broken by the old updater has neither launcher nor .deleteme, and exiting before setup stopped exactly those users from updating. validate_launcher still judges the result. - Gate recovery on validity rather than existence, and treat a failed backup as a missing safety net rather than a fatal error. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Resolve the launcher from the managed venv and keep a good backup Three follow-ups from review. The transaction resolved Scripts from sys.executable, but setup.ps1 installs into STUDIO_HOME/unsloth_studio (setup.ps1:3411). When a pip-installed or checkout CLI drives the update, those differ, so it backed up and --version validated the caller's launcher while the one actually being replaced went unprotected. Resolve the managed venv the same way _studio_deps._managed_root does for the damage scan. __enter__ overwrote the transaction backup unconditionally, guarded only by the two-byte MZ check. A backup outlives __enter__ only when a previous run died before validating, so it holds the last launcher known to run; overwriting it with a PE-shaped but unvalidated canonical file destroyed the only recovery copy. Write a backup only when there is no usable one already. package-lock.json was skipped outright, which dropped its existence check too. npm rewrites it in place but never deletes it, so keep the row and drop only its recorded size. Also add scripts/ to the shared non-runtime roots: unsloth_zoo ships a top-level scripts/, the same squatted-namespace shape as einx's test/, and it has no __init__.py so nothing imports it. * Move the launcher aside for setup, and restore it if nothing replaces it I rejected this on the strength of setup.ps1:4386-4391, which says renaming the running launcher "only ever failed (WinError 32)". That is not right. A probe on windows-latest builds a real console-script package, runs it, and renames the live launcher: the rename succeeds and a replacement can then be written at the freed path. RESULT idle-launcher: RENAME SUCCEEDED RESULT running-launcher: RENAME SUCCEEDED RESULT publish-replacement: WROTE a new launcher at the canonical path main moved the launcher aside before setup (studio.py:3182) and this branch had removed it, so uv could no longer replace Scripts\unsloth.exe. uv only self-replaces its own executable and deletes a third-party console script outright, and the pip fallback then no-ops on the already-satisfied bare unsloth, so the upgrade was silently skipped. Move it aside again, but keep what this branch was written for: when setup publishes no launcher, validate_launcher restores it rather than leaving the venv with none. That was the original bug, where the old updater renamed the launcher away and then deleted its own .deleteme. Restore prefers the backup over the moved-aside copy: the backup is the last launcher known to run, the moved-aside one is only this run's unvalidated canonical file. The mocked harness cannot reproduce a sharing violation, so the tests pin the invariant (the canonical path is free during setup, a recoverable copy always exists) while the CI probe covers the Windows semantics. * Tell a missing launcher from a broken one, and retry restores Two follow-ups, both from the restore path added in 724b274d4. Restoring before the health check could not tell setup publishing nothing from setup publishing something unusable. A zero-byte or non-PE replacement was quietly swapped for the previous launcher, which then passed --version, so the update reported success and deleted its own recovery copies. Sample whether setup published anything before any restore: nothing published and a good restore is the no-op update this transaction exists for, while a launcher setup did write and that cannot run stays a failure even though the previous one goes back. _restore_backup also picked the first candidate passing the two-byte header check and stopped there. Backups are taken after only that check, so an interrupted run can leave a PE-shaped but non-runnable one, and preferring it stranded the working launcher this run had moved aside. Split restoration: _restore_from puts one candidate back, and _restore_runnable walks the candidates until one actually runs. * Restore a runnable launcher on exceptional exit, narrow the exemption __exit__ restored the first PE-shaped candidate, so an interrupted run's non-runnable backup was installed over the working launcher this run had moved aside, and it could undo a restore validate_launcher had just made. It now uses _restore_runnable, which leaves an already-working launcher alone, walks the candidates until one passes --version, and falls back to the best candidate rather than whichever was tried last. The shared-namespace exemption was also too broad. I justified it on the grounds that tests/ and scripts/ ship no __init__.py, which is wrong: PEP 420 makes them importable, and this repo does 'from scripts import ...' itself. Restrict it to distributions Unsloth does not ship, so einx and torchao squatting on a top-level test/ is exempt while our own top-level trees stay checked. * Keep all four recovery copies as runtime candidates _recovery_candidates only offered the backup and the moved-aside copy, so when an interrupted run left a PE-shaped but non-runnable backup and the legacy .deleteme or the PATH shim was still good, the bad backup was accepted on its header alone and the good copy was never reached. The update then failed every time with the broken bytes canonical. All four are candidates now, deduplicated by normalised path, and _restore_runnable walks them until one passes --version. * Move the update lock out of the replaceable venv Resolving Scripts from the managed venv put the lock inside $VenvDir, and setup.ps1:3748 removes that whole directory to rebuild a stale torch. Windows refuses a recursive delete while a handle inside it is open, so an external CLI holding the lock for the whole setup run failed the repair with "Could not remove stale venv". Keep it under the Studio home instead, which is stable and is the right grain anyway: it is what names the managed venv. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com> |
||
|
|
d495a09bf0
|
Guard Windows Studio installs against active runtimes (#7764)
* Fix Studio installer runtime race * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Close remaining Studio installer races * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep Windows installer tests Windows-only * Stabilize Windows process guard test * Coordinate terminal Studio launches * Guard all managed Studio launches * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Coordinate custom Studio roots * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Handle direct GGUF settings rows * Apply repository formatter * Close remaining Studio update races * Handle Windows runtime gate CI edge cases * Verify the updater parent shim by image * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Allow verified updater shim chains * Stabilize Windows process guard test * Handle Windows console-script redirectors * Close remaining Windows updater guard gaps * Handle spaced and repeated Windows update shells * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Resolve Tauri root aliases before validation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix custom-root locks and updater ancestry * Align Windows runtime identity checks * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Scope desktop fallback to the current user * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Preserve drive-root mutex identity * Use ordinal semantics in Studio idle scans * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Guard standalone Studio setup mutations * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Restore the setup gate handoff independently * Version the Windows installer native helper * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use ordinal path comparison in Studio runtime scan * Tighten Studio runtime gate test comments * Exempt the venv Python redirector in the Studio runtime gate Windows venv Scripts\python.exe is a redirector that runs base Python as a child, so `unsloth studio setup` runs as unsloth.exe -> python.exe -> us. The ancestor walk only exempted the unsloth.exe shims and stopped at the redirector, so the installer flagged its own launcher and every Windows install failed with "The managed Studio environment is in use by unsloth.exe". Carry one redirector as pending and exempt it only when a shim sits directly above it, so a managed backend that spawns an update still blocks. Also stop gating the protected shim paths on exists(), so a shim renamed out of the way mid-update is still recognised. Drop "Studio" from the two runtime-lock messages so process.rs satisfies the desktop branding contract. * Close the redirector exemption when the updater is the managed image Only a base interpreter runs under a venv redirector. If our own executable is inside the managed root there is no redirector above us, so a managed parent is a real consumer and must keep blocking. Adds the regression to the redirector test. * Key the redirector exemption on sys.executable, not on a shim above it The Tauri updater runs `<venv>\Scripts\python.exe -I -c ... studio update` directly, so its chain is tauri.exe -> redirector -> base Python with no unsloth.exe in it. Requiring a shim above the redirector made every desktop update block on its own launcher. A venv redirector starts base Python as a child and waits, so when we are the base image and sys.executable still names the managed interpreter, our direct parent is that launcher. Exempt exactly that hop; ancestors above it must still be shims, and a managed image at depth two or more keeps blocking. * Stop the x86 guard test racing its own probe The 32-bit leg fired one scan against a probe that lives about five seconds, while a WOW64 shell start plus the Add-Type compile regularly costs more than that, so it read an empty list on a Windows runner. Give the probe a long life and retry like the 64-bit sibling already does. The assertion is unchanged. * Tighten comments in the Studio runtime gate changes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <michaelhan2050@gmail.com> |
||
|
|
e5c55e3577
|
Validate an explicit GGUF variant before unloading the resident model (#7862) | ||
|
|
2e593b3f85
|
Studio: add opt-in DSpark speculative decoding (#7968)
* Add opt-in DSpark speculative decoding * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix lint blockers, retry-guard test and DSpark broken-build gate for PR #7968 Three fixes for the failing CI on this branch: - Drop the unused `Literal` import from unsloth_cli/commands/chat.py and inference.py. The options are typed with `SpeculativeType`, so the added import was a leftover and tripped the import-hoist blocker. - Make test_the_startup_retry_drops_the_mtp_the_extras_and_the_env_carry whitespace insensitive. The guard now names both drafters, so formatting wrapped the call and the literal substring assertion no longer matched. It asserts on both `_extra_args_requests_mtp` and `_extra_args_requests_dspark` now, so the DSpark half is covered too. - Gate DSpark on the whole broken build window instead of one tag. The reshape regression is ggml-org/llama.cpp#26531 and the fix is #26577, so every prebuilt based on b10259 through b10268 aborts on a DSpark load, not only b10265-mix-89aa77b. Matching the base build number keeps source builds unaffected, since those carry no install marker. * Probe DSpark support before downloading the sidecar The ~11 GB DSpark sidecar was fetched at llama_cpp.py:8664 while the first supports_dspark consumer sat ~480 lines later, so a binary that cannot run draft-dspark paid for the whole download and then fell back without ever opening the file. probe_server_capabilities is already called just above for supports_kv_unified, so the answer is in scope and cached and the check costs nothing. This is the default path right now, not an edge case: the shipped unslothai/llama.cpp prebuilt b10265-mix-89aa77b sits inside the known-broken b10259..b10268 window, so supports_dspark is False on a standard install. Also swaps the order of the first two DSpark fallbacks. Now that the fetch is gated on the same answer, a gated binary leaves no sidecar, and checking the drafter first reported "no matching dspark-*.gguf sidecar was found" and told the user to place a file that was never the problem, while re-loading on every Apply through the drafter_not_found dedup branch. Adds three regression tests, all of which fail without this change. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Enforce fixed fit for pass-through DSpark, size split sidecars, correct the hint Three fixes from the latest review round. Extras that own --spec-type return from _build_speculative_flags before _speculative_type is set, so the --fit strip keyed only on that field never fired for a pass-through DSpark launch and a user --fit on survived. DSpark's layout cannot be reshaped, so that aborts the load. The strip now also reads the accumulated spec types, which covers both the flag and the env. The training coexistence estimate sized the drafter with a bare stat(), while the main weight beside it already used the split-aware helper. Discovery hands back shard 1, so a split sidecar was counted at one shard and the guard could admit a load that evicts the training run it exists to protect. The Speculative Decoding hint promised "no accuracy hit" unconditionally, which DSpark does not meet: on a quantized target its greedy output can differ from a non speculative run (ggml-org/llama.cpp#25618). Measured here on DeepSeek-V4-Flash-0731 UD-Q4_K_XL, where the same greedy conversation produced 10570 tokens without a drafter and 14687 with one. The claim now stays with Auto, and DSpark carries its own caveat. Both backend fixes have regression tests that fail without them. * Pin fit off for pass-through DSpark, keep cached sidecars visible, reclaim them on delete * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Allow DSpark under --fit on: fitting only skips the sidecar reserve * Gate the training-guard DSpark estimate on binary support, fix the picker contract marker * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Default Auto to DSpark when a sidecar is available, and stop the two reload loops it exposed * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Charge no drafter when forced DSpark is gated off by the binary * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Gate remote Auto DSpark sizing, retry only a failed sidecar fetch, refresh the Auto hint * Apply ruff kwarg-spacing formatting --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <unslothai@gmail.com> |
||
|
|
4a4509c156
|
feat(studio): add settings-managed remote access (#7875)
* refactor(studio): make cloudflare tunnels runtime-managed * feat(studio): add public access controls * fix(studio): preserve public stop responses * feat(studio): show public access settings * fix(studio): report settings-managed public links * fix(studio): respect launch tunnel ownership * fix(studio): apply public runtime trust policy * refactor(studio): rename public access to remote access * feat(studio): serve web ui through desktop remote access * feat(studio): set the remote password from desktop The desktop app signs in with a local secret and never sees the seeded administrator password, which is only printed to a terminal. Remote access refuses to open a tunnel while that seeded password is still in place, so a desktop user had no way forward: the change-password flow needs a current password they do not have, and the General tab hides its password row on desktop. Add a Remote password row to the Remote access section, desktop only. While the seeded password is pending it sets the first password through a new POST /api/auth/desktop-initial-password; once a password exists the same row changes it through the existing endpoint. The new route accepts only a desktop-issued JWT (web sessions and API keys are refused), applies only while the seeded credential is unchanged, and binds its write to the credential version it read so a concurrent web change or CLI reset is not overwritten. Both password routes now keep the local desktop credential valid and mint desktop-flagged tokens when the caller is the desktop app, so auto-auth stays passwordless and the session survives the change it made. Browser callers keep their existing behavior: the credential is revoked and ordinary tokens are issued. Remote access status reports password_pending on its own, independent of block precedence, so the row is correct even when another block hides the reason. Re-reading status after either operation clears the pending-password block and lets the tunnel start; remote browsers then sign in as unsloth with the new password. The action reuses the account password dialog in an initial-password mode rather than adding a component, and adds no file. * feat(studio): redesign remote access settings as a status card The remote access controls rendered as flat settings rows, so the feature read as hidden and the tunnel URL was squeezed into a description cell. Rebuild remote-access-section.tsx as a bordered card: - header with globe icon, title, live status dot plus state text and the start/stop action together - online state gets a dedicated URL panel with a copy button, a QR button that opens a scannable code for phones (react-qr-code, svg, zero deps), and a note that the URL plus remote password grants sign in - block reasons and tunnel errors render as their own strip, destructive styling on error - shine sweep on the status text while starting or stopping - password and auto start rows move below a divider inside the card Polling and start/stop/auto-start logic are unchanged. * fix(studio): stop tunnel dns wait from stalling on negative caches Starting remote access took 45-48s while a manual tunnel came online in about 8s. _wait_for_dns queried DoH immediately after the edge connection registered, before the fresh trycloudflare hostname propagated, and the resolver negative-cached the NXDOMAIN (trycloudflare.com publishes a 1800s SOA negative TTL; 1.1.1.1 was observed serving the stale negative for about 40s while the loop re-asked it every 2s). - delay the first lookup 3s so it cannot seed those negative caches - ask two independent resolvers per round (cloudflare and google DoH) - cap the dns wait at 20s so it can never starve the health probe, which is what actually gates advertising the URL Start to online now measures about 12s end to end against a live backend. The consecutive-error bailout for blocked DoH is unchanged. * fix(studio): drop stale stopping status after a finished stop A stop worker outlives its completed stop by up to about a second while draining stop responses. During that window remote_access_status kept reporting state stopping and forced can_start false, so a quick stop-then-start flashed "Stopping" in the UI and the start request could 409 as operation_in_progress. Once the tunnel controller reports off with no pending stop handle the stop is done, so a still-alive stop worker no longer masks the idle state or a newly admitted start. Unconfirmed terminations (stop_pending) keep reporting stopping so retry stays visible. * fix(studio): resolve the tunnel hostname through one resolver Asking a second DoH resolver in the same round cannot rescue the poll: both queries land milliseconds apart, so the round that seeds one resolver's negative cache seeds the other's too. It also disclosed the freshly issued hostname to a third party on nearly every start, where the provider that handed the name out already knows it. The hold-off and the wait cap stay, which is where the measured speedup comes from. The cap trades one case away: a hostname that only propagates after 20s now meets the OS resolver while it is still missing, and because the tunnel has registered by then a failed probe ends the attempt instead of retrying over http2. In exchange every start whose record exists by 20s spends the rest of the deadline on probe attempts rather than a single one at 45s. * fix(studio): report a stop only while its teardown is outstanding A stop worker outlives the teardown it performed, so reporting stopping until that thread exits hid the tunnel already being off and refused a start that would have succeeded. The worker's recorded admission sits behind the control token once its teardown advanced the generation, but a start advances the generation the same way, so only that advance together with an off tunnel and no pending stop shows the teardown actually happened. * fix(studio): keep the remote access card legible The status text goes back to full opacity. The shimmer needs partly transparent glyphs, which put the transitional states below the contrast that same text had before, and a viewer who asked for reduced motion paid that cost with no sweep to show for it. The status dot already marks the transition. The auto-start switch's accessible name matches its visible label again after the rename, and the block reason no longer borrows the error colour when a failed tunnel is not what it is reporting. Moving the URL panel and the block-reason strip out of the section body keeps the section at the complexity it had before the card, and SettingsRow drops labelAccessory along with the row that used it. * perf(studio): verify a new tunnel at the edge instead of through DNS Cloudflare routes quick tunnels by TLS SNI, so the edge serves a tunnel as soon as its connection registers, before the hostname resolves anywhere. Probing an edge address with the tunnel's name in SNI and Host takes that hostname's DNS off the startup path, along with the wait that existed only to keep an early lookup from caching the miss. Measured over four fresh tunnels each: verification after registration falls from 10.97s to 6.61s on average, and a whole start from 22.9s to 16.8s. It also removes the case behind the slowest starts, where one DoH query sent before the record propagates is negative-cached, blinding the poll until its own cap expires and pushing a start past 30s. The hostname path stays as the fallback for networks that block direct addresses or intercept TLS. It is entered as soon as the edge looks unreachable rather than merely unready: an error 1033 page is an answer, a failed connection is not. * fix(studio): bound the edge probe and dial distinct frontends Verifying a quick tunnel at Cloudflare's edge shipped with two defects. The wait checked the clock only after trying every address, so a full pass always ran: verify_public_url with a 0.05s timeout still took 12s, against 1.65s before the edge probe existed, and a pass of two real TLS handshakes overshot the 15s cap to about 19s, taken out of the hostname fallback's share of the same deadline. The clock is now read before every attempt, and no attempt is given more time than the deadline leaves. The addresses were also not distinct. macOS reports the A records again as IPv4-mapped under AF_INET6, so taking one per family dialled a single frontend twice, as 104.16.230.132 and ::ffff:104.16.230.132. That doubled the cost of a pass, halved the poll rate, and let two dependent samples of one address exhaust the unreachable counter. Deduplicating by frontend yields 104.16.230.132 and 104.16.231.132 here. An intercepting proxy answering with its own page is an answer rather than an unreachable edge, so it resets that counter exactly as error 1033 does and the wait ends at the cap instead. The comments claimed interception left through the early exit; they now describe what it does. * fix(studio): clear the python floor, read encoding and locale parity gates The tunnel test annotated a helper with `str | None`, which evaluates on the 3.9 floor that pyproject declares and pushed the studio union ratchet from 35 files to 36. Six new test reads took the platform default encoding, so they break on Windows the moment those files gain a non-ASCII byte. The remote password dialog's six new strings existed only in en, which strict locale parity rejects; they are now translated into every overlay. * Fix settings-managed tunnels dying on Linux, and stop-during-retry orphans for PR #7875 PR_SET_PDEATHSIG is a parent-THREAD death signal, not parent-process: the kernel fires it when the thread that forked the child exits. start_remote_access forks cloudflared from a short-lived worker thread, so the connector was SIGTERMed about a second after it came online, every time. Reproduced 3/3 on a real install: the URL is logged, then state goes to error with "cloudflared exited". Auto-start shares the same worker, so it was affected too. Only Linux and WSL are hit. macOS arms no per-thread signal and Windows uses a process-wide Job Object, so both were fine, as was the launch path, which forks from the main thread. Changes: - process_lifetime: spawn_on_lifetime_thread() performs the fork on one process-lifetime daemon thread, so PDEATHSIG means "die with the parent process" again. Non-Linux spawns directly. Falls back to an inline spawn when no helper thread can be obtained, so it can never block. - cloudflare_tunnel: spawn through that helper. The abort branch now tears the connector down instead of returning the URL of a process it just detached, which could leave a live public tunnel no later stop_studio_tunnel() could reach. Restore _tunnel_state to "starting" after attempt 1's teardown so the http2 retry no longer advertises "stopping", which made stop_studio_tunnel() early-return and stop nothing, and made the Settings Stop route a no-op. - run.py: an explicit --cloudflare/--no-cloudflare/--secure on this invocation now beats an inherited _UNSLOTH_CLOUDFLARE_INTENT, so a stale export, Docker ENV or systemd Environment= cannot re-enable a tunnel the user opted out of. The marker still softens the compatibility --no-cloudflare into "unset". - Add tests for remote-access-state.ts, which had no coverage. No bugs found there; the logic is correct, it simply was not exercised. Verified: 701 passed in the focused backend slice (the one failure is a pre-existing missing optional dep, and fails on main too), 347 frontend tests, typecheck, biome and 80 simulation tests including a real-child-process tunnel lifecycle and Chromium/Firefox/WebKit CORS runs. * Surface the unstoppable-connector error, bound the Stop wait, unlatch polling for PR #7875 Follow-ups from reviewing the remote access flow on Linux. The tunnel lifetime fix landed already; these are the smaller things around it. - remote_access_status collapsed "cloudflared could not be stopped" into the generic "Cloudflare tunnel failed". That is the one error the user can act on: the connector's exit was never confirmed, so it still holds the runtime slot and Start stays disabled. Pass it through like the other known messages. - The Stop worker waited on a live start worker with no deadline, polling at 100 Hz. A start that never claims settings ownership (foreign owner, or one that bailed on admission) deferred the user's Stop for the full probe deadline, up to about 170s. Bound it at 5s and poll at 50 Hz. - setPollEnabled(false) had no path back to true. It fires when you stop a tunnel from a browser connected through that tunnel, which is correct, but the section then stayed dark until it unmounted. Resume on any later action or on a password-change refresh, since both prove the origin is reachable. - test_change_password_policy called change_password positionally, so the new is_desktop parameter defaulted to the Depends object, which is truthy. Harmless today because the assertions fire earlier, but it silently exercises the preserve-desktop-secret branch. Pass False explicitly. Verified: 230 passed in the focused backend slice, 347 frontend tests, typecheck, build, locale parity, and no new biome warnings. Re-checked on a real install: auto-start brought a tunnel up at boot and it stayed online, and Stop through that tunnel returned 200 in 0.07s. The one backend failure (test_health_response_reports_desktop_capability_fields) is a pre-existing environment gap and fails on main too. * Correct streaming_supported, shorten the CORS preflight window, guard the spawner across fork for PR #7875 Findings from simulating the remote access paths across platforms, browsers and upgrade scenarios. Three small fixes, each measured rather than reasoned. - streaming_supported was a hardcoded True. Every tunnel Studio opens is a Cloudflare Quick Tunnel, and Cloudflare documents that Quick Tunnels do not support Server-Sent Events. Measured against a real tunnel: a local SSE endpoint delivers 6 events over a 5.0s spread, and the same endpoint through the tunnel answers 200 with text/event-stream and then delivers nothing at all before the read times out. Chunked non-SSE streaming was blackholed the same way, so no transport change works around it. The field now reports whether a tunnel is currently carrying the traffic, so it is true locally and false while a tunnel is live. Nothing branches on it yet, which is exactly why it should be right before something does. - The CORS middleware inherited Starlette's 600s Access-Control-Max-Age. is_allowed_origin closes the instant the tunnel URL clears, but a preflight the browser already cached does not. Measured across four engines: after remote access was stopped, WebKit reused its cached preflight and the state-changing POST still reached the server, while Chromium, Firefox and Edge re-preflighted and were refused. Pinning max_age to 60 keeps preflight caching useful and makes revocation nearly as immediate as every other trust signal here. - spawn_on_lifetime_thread keeps a module-level lock and a helper thread. A fork child inherits the lock in whatever state it was in, and a spawner whose thread does not exist in the child. Reproduced: forking while the lock is held deadlocks the child permanently. Not reachable today, since the backend only uses the spawn start method and the one fork start method lives in the training worker, which never imports the tunnel. Registering an at-fork reset is three lines and closes the class of bug rather than the instance. Also verified and unchanged: no schema change in any store, so an old studio.db with no remote_access_auto_start row reads as false and fail-closed on corrupt or non-boolean values; the argument parser is byte-identical to main, so no launch flag default moved; the PR touches no hardware, GPU or desktop files, and all twelve OS x accelerator combinations produce an identical remote-access status. Host and Origin are forbidden request headers in all four engines, so no page script can forge the Cloudflare provenance gate. Verified: 95 simulation cases, 41 adversarial cases, 109 in the directly affected backend slice, 347 frontend tests, typecheck, build and locale parity. On a real install: auto-start brought a tunnel up at boot and it stayed online, Stop through that tunnel returned 200 in 0.09s, and the live status now reports max-age 60 and streaming_supported false. * Keep the desktop backend up without a dist, and settle self-origin Stop on off for PR #7875 The desktop spawns "studio --api-only" with no --frontend, and install.sh --tauri skips the frontend build outright. The new desktop-owned branch made _serve_frontend true for that launch, so an unresolvable studio/frontend/dist raised SystemExit before TAURI_PORT was emitted and the app never got a backend. The SPA only backs the optional remote web UI there, so warn and carry on API-only; a web UI launch still aborts loudly. A Stop sent from the tunnel's own origin is answered with a terminal off, then polling restarts in perform()'s finally and the first poll can still land inside the ~50ms teardown drain, where the backend reports "stopping". That overwrote the terminal off, cloudflared then exited, and the card latched on a permanent "Stopping" for a tunnel that was already down. Teardown frames no longer overwrite it, while a poll that can still stop the connector proves teardown was abandoned and takes the card back over. --------- Co-authored-by: Maheswar Kumar <110882203+mahiatlinux@users.noreply.github.com> Co-authored-by: danielhanchen <michaelhan2050@gmail.com> |
||
|
|
957b994299
|
Studio: simplify the GGUF loading backend (#7663) | ||
|
|
f706214383
|
studio update: fail instead of reporting success on a damaged install (#7691)
* studio update: fail instead of reporting success on a damaged install
pip considers a distribution with intact metadata already satisfied, so an
update reinstalls nothing when a package's FILES are damaged. It printed
"Unsloth Studio Installed", exited 0, and Studio then died at boot with
`cannot import name 'Depends' from 'fastapi'`. That is the shape behind the
advice to re-run the full installer, and it is only actionable if the update
says so instead of claiming success.
Reproduced by emptying fastapi/__init__.py in a real install: before this,
update exited 0; now it exits 1, names the file, and points at the installer.
A full install repairs it, which the message says.
A missing-package check cannot catch this, because the damaged module still
imports at top level. Comparing each RECORD entry against the filesystem does,
imports nothing, and takes under a second on an install with torch (44k files
in 1.5s).
Two things the check deliberately does not do, both from real installs:
- it does not flag a file LARGER than recorded, which means two
distributions claim one path (descript-audio-codec ships a top-level
tests/__init__.py) rather than damage
- it does not read Distribution.files, which drops entries whose file is
gone and so can never report a deletion; RECORD is parsed directly
Zero findings on four independent healthy installs, including two updated from
older tags. A CLI living outside the venv it is updating skips the check rather
than describe the wrong tree, and --no-verify turns it off.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Cap deleted-file findings and skip the check on system Python
Two review findings on the damaged-install check.
The finding cap was not applied on the deletion branch: the `continue`
after appending "is missing" skipped the `len(found) >= limit` test, so
only truncated files were ever capped. A wiped package takes the deletion
branch, and torch alone has ~11.8k RECORD entries, each of which the
desktop updater streams out as its own IPC event. The existing cap test
used truncated files and so could not see this.
`running_outside_managed_venv` also returned False on a plain system
Python. On Colab there is no Unsloth venv -- studio/setup.sh installs the
backend into the system interpreter on purpose -- so the check ran over
distro dist-packages, whose RECORDs legitimately list files the distro
never installed (PEP 627). Reproduced on Ubuntu system Python, where it
reports an apt-owned `markdown-it-py: ../scripts/markdown-it is missing`
and would have aborted the update. It now returns True when sys.prefix
has no pyvenv.cfg.
Both regression tests fail against the unfixed source.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Carry a custom Studio root into the reinstall command
The check's only actionable output pointed at the wrong installation.
A custom root is inferred from sys.prefix when the user runs the shim
directly, but the shim is a bare symlink (install.sh:4367) and
_ensure_studio_env_exported only sets os.environ for this process, so the
shell that runs the printed command sees no UNSLOTH_STUDIO_HOME. Both
installers then fall back to ~/.unsloth/studio and build a fresh install
while the damaged one stays broken.
The assignment goes before `sh` rather than before `curl`, matching the
form install.sh documents at line 14, and the root is quoted so paths
with spaces survive. The default root keeps the plain command.
* Stop pinning one CPython's Distribution.files behaviour
The test asserted that files() drops entries whose file is gone. That
filter is not in every interpreter this project supports (>= 3.9), and on
the ones without it the entry is listed instead, so the assertion fails
there. The unsloth_cli suite happens to run only on 3.12 in CI, so this
was a trap for anyone running it locally on an older interpreter rather
than a live failure.
Asserts the property that actually matters on every version: files()
either drops the entry or lists one whose locate() does not resolve, so
it cannot report the deletion either way, which is why the detector
parses RECORD. Checked the new form is falsifiable, not vacuous.
* Say what to do when the reinstall will not clear the damage
install_python_stack installs the current requirement sets and prunes
nothing, and the installer never recreates the venv, so a package left
over from an older release, or added by hand, survives the reinstall the
message recommends and reports the same damage forever. The only way out
was --no-verify, which the message offered without explaining why the
first suggestion had not worked.
Scoping the scan to Studio's dependency closure was the other option and
is the more dangerous one: the closure would have to be derived from the
requirement files, anything installed outside them would fall out of
scope, and a miss there means real damage passes silently, which is the
exact failure this check exists to catch. Over-blocking with an accurate
message is the safe direction.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Quote the interpreter and drop deps in the direct repair command
Two problems with the fallback command added in the previous commit.
sys.executable was printed unquoted, so a custom root containing spaces
split into several shell tokens and the command would not run. Custom
roots with spaces are supported, and the reinstall line right above it
already quotes STUDIO_HOME. Windows gets the call operator, since a
quoted string in command position is otherwise parsed as a string.
It also lacked --no-deps. pip would resolve the damaged package's
dependency graph, and --force-reinstall can then replace pinned runtime
packages, swapping the installed CUDA or ROCm torch build while repairing
an unrelated orphan. install_python_stack.py pairs the two flags in its
own targeted repairs for the same reason.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Scan only this interpreter's site-packages, and exempt shared paths both ways
Two review findings on the detector itself.
distributions() searches every sys.path entry, not just sys.prefix, so a
damaged distribution reachable only through an inherited PYTHONPATH
failed every update while sitting outside the installation. Neither
printed repair command can reach it there, so unlike an orphan inside the
venv it was a permanent failure loop with only --no-verify as a way out.
Reproduced, then confirmed fixed. The scan now takes purelib and platlib,
and falls back to the old unrestricted behaviour if those cannot be
resolved, since over-scanning beats scanning nothing.
The collision exemption was also one-directional. Two distributions
claiming one path means whichever copy landed is the one on disk, so its
size says nothing about either RECORD -- but only the larger-than-recorded
case was excluded, and a collision that overwrote with a shorter file was
reported as corruption. Sizes are now compared in a second pass, once
multiply-owned paths are known. A singly owned short file is still
damage, with a test pinning that so the exemption cannot widen.
Real venv with torch: 0 findings, 0.92s.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Close four holes in the damage scan
Reproduced all four before fixing; the first is a regression from the
previous commit.
Skipping multiply-owned paths outright also skipped their existence
check. Multiple ownership makes the recorded sizes ambiguous, but it
cannot explain a missing file, so only the size comparison is skipped now
and a deleted shared file is reported once per owner.
Rows with a blank size field were discarded before the file was located.
The field is optional and real wheels leave it blank, so deleting such a
file went unreported. The row is kept with an unknown size and only the
shrinkage comparison is skipped.
A directory standing in for a recorded module passed as healthy, because
an empty directory is commonly 4096 bytes on POSIX and so cleared the
shrinkage test. Entries must now resolve to a regular file. RECORD
directory entries (a trailing slash) are skipped outright.
The printed repair command used a bare package name, and --force-reinstall
reinstalls even when a package is current, so it would have upgraded the
orphan rather than repairing it. --no-deps does not prevent that. It now
shows <package>==<installed version>.
A real venv with torch still reports 0 findings in 0.95s, so the two new
rules add no false positives.
* Describe --verify as the file scan it is
The help said it checks that the backend still imports. It imports
nothing: it compares RECORD entries against the filesystem, so same-size
corruption, a file edited longer, or an intact but incompatible package
all pass. That is a weaker guarantee than the wording promised, and the
wording is what a user decides --no-verify against.
* Keep a no-torch install in no-torch mode when repairing it
install.sh and install.ps1 derive SKIP_TORCH from their own flag or
UNSLOTH_NO_TORCH and then pass that value into setup, and the env tier
beats the recorded mode by design (install_python_stack.py:2257-2276), so
a plain reinstall over a GGUF-only install resolves as UNSLOTH_NO_TORCH
=false and pulls the whole PyTorch stack. Ordinary `studio update` stays
in no-torch mode precisely because it injects no env var. The printed
command now carries UNSLOTH_NO_TORCH, in the form each installer
documents, when the record says so.
Added only on an explicit True: recorded_no_torch() returns None when
nothing recorded the mode, and its contract is that None is not False.
Guessing either way is harmful, so an unknown mode keeps the plain
command.
No root argument, which a first attempt got wrong. The manifest and
marker live in the venv, not the install root, and recorded_no_torch
defaults to Path(sys.prefix); passing STUDIO_HOME looked one directory
too high, read None, and would never have fired. Verified against the
real install: the install-root path returns None, the venv returns the
recorded False. The test now asserts the root so it cannot regress.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
|
||
|
|
7869f3f491
|
studio update --local: refuse site-packages as the repo root (#7693)
* studio update --local: refuse site-packages as the repo root
The second consecutive `unsloth studio update --local` failed on Windows:
ERROR: file:///C:/Users/.../unsloth_studio/Lib/site-packages does not appear
to be a Python project: neither 'setup.py' nor 'pyproject.toml' found.
[FAILED] Python dependency installation failed (exit code 1)
STUDIO_LOCAL_REPO was derived as Path(__file__).parents[2], which is the repo
root only while the CLI runs from a checkout. The first update replaces the
editable install with a normal one, so on the second run parents[2] is
site-packages and uv is asked to install site-packages as a project. The
existing comment already noted the path 'may be inside site-packages'; nothing
checked it.
An explicitly set STUDIO_LOCAL_REPO is now honoured, which makes repeat local
updates work, and a root with no pyproject.toml is refused with the two ways
forward instead of uv's error. Found by a Windows update matrix on staging;
the first update passes there and only the second failed.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Absolutise an explicit STUDIO_LOCAL_REPO before checking it
A relative override passed the guard and then failed anyway. The check runs
against update()'s cwd, but the value is handed downstream verbatim, and
studio/setup.sh does `cd "$SCRIPT_DIR"` (line 1031) before it calls
install_python_stack (line 1105). So `STUDIO_LOCAL_REPO=.` re-resolved against
studio/, which has neither pyproject.toml nor setup.py, and uv produced exactly
the error this guard exists to replace.
Also strip and expanduser the value: a tilde path is the natural way to name a
checkout and was never expanded, and a whitespace-padded value rejected a valid
one. The sibling reader at studio.py:2744 already strips.
Three tests added; all three fail without the change.
* Run the override checkout's setup script, and print a PowerShell-valid hint
Two review findings on the STUDIO_LOCAL_REPO escape hatch this PR adds.
The override chose which repo gets installed but not which setup script
ran: _run_setup_script resolved setup.sh/setup.ps1 from _PACKAGE_ROOT.
Those scripts build the frontend under their own SCRIPT_DIR, and the
editable install of the override removes the installed tree they built
into. studio/frontend/dist is gitignored, so pointing at a fresh checkout
left Studio with no frontend and `unsloth studio run` exiting 1. The
checkout's own script now takes precedence; with no override the
resolution order is unchanged.
The guard also printed only `STUDIO_LOCAL_REPO=/path/to/unsloth ...`,
which is POSIX prefix-assignment syntax. PowerShell parses that as a
command name, so the sole recovery instruction did not work on the
platform the guard exists for. Windows now gets the $env: form.
All three regression tests fail against the unfixed source.
* Refuse a checkout with no setup script instead of falling back
Falling back to the installed copy's setup script is the behaviour the
override exists to stop: that script builds its own frontend, the
editable install then removes the tree it built into, and the selected
checkout is left without one. A sparse checkout is an unusable local
source, so say so rather than quietly running somebody else's script.
The message names the missing path; the no-override resolution order is
unchanged.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
|
||
|
|
92106ea1fb
|
Fix multiline prompts through Windows npm shims (#7567)
* Fix multiline prompts through Windows npm shims * Fix npm shim review edge cases * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Support all standard Windows npm shims * Resolve Node without PATHEXT script shadowing --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com> |
||
|
|
b5b52cfa48
|
Run OpenClaw in the launched project workspace (#7568)
* Run OpenClaw in the launched project workspace * Fix OpenClaw recipe workspace resolution * [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: Wasim Yousef Said <wasimysdev@gmail.com> |
||
|
|
d1127c09af
|
Studio: keep a slow model load alive through a proxy timeout (#7635)
* Studio: keep a slow model load alive through a proxy timeout
Loading a large GGUF behind --secure failed in the browser with "Request
failed" while the server returned 200. Cloudflare quick tunnels drop a
request whose origin has sent no body bytes for ~100s; over one session
12 requests passed that mark and every one of them succeeded server-side:
6 loads (102.6-327.1s), 1 unload (160.9s) and 5 chat completions.
The chat ones stream, so bytes flow from the first token and they survive
(one ran 2254.9s). Load and unload send nothing until they finish, so
only those get killed.
Measured against a real quick tunnel before picking this design:
no body for 150s -> 524
status + headers at t=0, no body -> 524, so headers alone are NOT enough
one byte at t=90s, then silence -> killed ~125s later; the client sees
a 200 with an EMPTY body
one space every 20s -> survives, body intact
So the timer wants body bytes and resets on each one, and the padding has
to be continuous. /load and /unload now hand off to _tunnel_safe_json: a
call finishing inside 15s keeps today's contract exactly, HTTPException
and status code included, and only a slower one commits a 200 and emits a
space every 20s until its payload is ready. Leading whitespace is legal
JSON, so all six loadModel callers parse the body unchanged.
The cost is that a failure discovered after the status is committed can
only travel in-band, as _deferred_error, which parseJsonOrThrow now
raises on. Everything that fails before the 15s mark still gets a real
status code, which covers argument validation, an unknown identifier and
the download-manager and sidecar 409s.
A client disconnect does not cancel the work, so an abandoned load still
leaves the model resident, as it does today.
Also rename the load log's `fit` field. It is use_fit, i.e. whether
--fit on is passed, so `fit: False` meant "fits, GPUs pinned, --fit off"
-- the opposite of how it reads.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Tighten the comments added by this PR
Comment, docstring and whitespace only. Keeps every measured number (the ~100s
quick-tunnel idle cutoff, the 100-330s load window, the 160s teardown, the 20s
pad interval, and the probe results including the empty-body outcome at t=90s),
the headers-are-not-enough finding that motivates streaming rather than an early
flush, and the list of failures that still get a real status code.
Also corrects one claim rather than shortening it: the fixture comment said it
preserved the ordering the real values encode, but the defaults are
AFTER=15.0 < EVERY=20.0 while the fixture sets 0.05 > 0.02, so it inverts that
ordering. It now points at the test that does guard the real defaults.
* Keep non-browser load clients blocking and able to see a late failure
Padding a slow /load commits its 200 before the work finishes. That was handled
for the browser and nowhere else, so two classes of client were left broken.
In-process callers got a StreamingResponse whose body nobody drains, so they
resumed while the load was still running. routes/preview.py awaited the route
and then immediately started a preview chat against the previous model, or none.
Everything the route did apart from the padding now lives in load_model_gated,
the route is the only caller of _tunnel_safe_json, and preview awaits the gated
coroutine, so it blocks until the model is really resident and sees the real
exception. Both sidecar checks, the lifecycle gate and the active-generation
callback move across unchanged. /unload needs no equivalent: nothing calls it
in-process, and _unload_model_impl already is the named awaitable, so its
docstring just says so.
Python clients treated any 200 as success, so a late OOM or llama-server startup
failure read as a successful load: unsloth_cli/commands/studio.py returned the
envelope as the load result and start.py recorded it as a success. _inference.py
was worse, closing the response at the headers and generating mid-load.
raise_for_deferred_error and read_json_checking_deferred_error go in
unsloth_cli/_inference.py, the CLI's shared HTTP module, and raise
urllib.error.HTTPError carrying the deferred status and detail. That is the class
all three sites already handle for a plain HTTP failure, and .read() yields the
same {"detail": ...} shape, so their existing except blocks, messages and exit
codes keep working. Keys match the backend and the TypeScript client:
status_code and detail, defaulting to 500.
The fast path is untouched: under the keepalive delay the route still returns the
plain payload and still raises HTTPException with its real status code.
18 tests, each confirmed to fail before the fix. The preview ones are the clearest:
pre-fix the chat starts with zero loads finished.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Take the GGUF teardown off the event loop, and reject a truncated padded body
Three review findings, all correct, and the first two meant the padding was dead
code on the two slowest paths it exists for.
LlamaCppBackend.unload_model is synchronous, and two call sites ran it bare on the
event loop: replacing a loaded GGUF with a non-GGUF model, and the already-loaded
manual unload. A 600 GB teardown measures 160s, and while it holds the loop
_tunnel_safe_json's delay timer cannot fire and its keepalive generator cannot
yield, so zero bytes go out and the proxy still kills the request. Both now go
through asyncio.to_thread, matching the in-flight-cancel branch that already did.
An AST test fences both functions against a new bare call, since the fix makes the
call an attribute rather than a call node and would otherwise be easy to undo.
Moving them off-loop lets other handlers run during the teardown. Everything that
could interleave destructively is already excluded: concurrent load and unload by
the process-wide lifecycle gate held across the new await, new inference by the
keep-warm middleware taking the same gate, idle auto-unload likewise, and every
other holder of the backend lock only ever runs inside a thread already. What is
newly observable is GET /status, which is not gated, reporting the model as loaded
for up to the 5s terminate window while the server dies, then flipping. The
reverse is unreachable: _kill_process clears _process before the identifier, so
is_loaded can never be true with a cleared identifier.
A truncated padded response was read as success. On the web side .json() throws,
the catch makes the body null, the 200 is already committed and no deferred error
is present, so it returned null as a completed load or unload. This PR's own
tunnel probe documents exactly that case. On the Python side _http_json's
`or "{}"` turned a blank body into {}, which read as a finished load. Both clients
now require a non-empty object from those two routes and say the operation did not
report completion otherwise.
That strictness is scoped to the two padded routes rather than the shared JSON
helper. They are the only routes that commit a 200 before the work is done, so
they are the only ones where an empty body means "may or may not have happened"
instead of "no payload by design"; the codebase has six 204 routes and five
callers that discard the value, which a blanket null error would break.
17 tests, each confirmed to fail first. The teardown ones use a synchronous fake
that blocks until it has seen pad bytes, so pre-fix the log shows the pad message
only after the teardown returns, and the unload path never becomes a stream at all.
* Tighten the comments the review rounds added
Comment, docstring and whitespace only: 46 comment lines out, 46 file lines out.
The first pass ran when this was only the tunnel-safe change, so the prose added
by the two rounds of review fixes had never been through one. The tunnel test
file carries most of the reduction at 18 lines.
Every non-derivable fact survives, grep-checked: the ~100s idle cutoff, the
100-330s load window, the 160s 600 GB teardown, the 20s pad interval, the probe
result that a byte at t=90s then silence yields a 200 with an empty body,
headers-alone still 524ing, leading whitespace being legal JSON, the failures
that keep a real status code, the client-disconnect guarantee, why in-process
callers await load_model_gated, why the deferred error is an HTTPError
specifically, why the teardowns must be off-loop, and why the truncated-body
strictness is scoped to the two padded routes.
One wording fix rather than a shortening: an empty dict was described as
"counts", which was ambiguous about which way. The check is
`isinstance(body, dict) and body`, so {} is rejected, and the comment now says so.
Left alone: everything from the first pass, and test_preview_routes.py, whose
diff is pure load_model -> load_model_gated renames with no new prose.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
|
||
|
|
a31a8ff352
|
Don't disarm Ctrl+C in agents launched by unsloth start (#7630) | ||
|
|
1564ce4585
|
Fix two pre-existing Backend CI failures blocking every open PR (#7639)
* Read the backend source as utf-8 in the model-picker contract test _read_backend landed in #7385 without an encoding, so it falls back to the platform default and breaks on Windows the moment a backend file it reads gains a non-ASCII byte. tests/test_source_read_encoding.py guards exactly this and is currently failing on main, which turns Repo tests (CPU) red on every open PR whose CI runs the merge commit. The sibling _read helper directly above already passes encoding = "utf-8". * Strip colour from the unsloth_cli CLI test output Three test_start.py cases assert on plain substrings of result.output, e.g. "Invalid value for '--gpu-memory-mode'". Typer renders parameter errors through Rich, which emits ANSI escapes as soon as FORCE_COLOR is set, so the substring is split across escape sequences and the assertion fails even though the message is present. They pass locally and fail in CI purely because the runner exports FORCE_COLOR; click, typer and rich are the same versions in both. Setting NO_COLOR alone does not help, FORCE_COLOR still wins, so the autouse fixture removes FORCE_COLOR and CLICOLOR_FORCE as well. #7598 only recently started running unsloth_cli/tests in Backend CI, which is why this surfaced now. Verified: the whole suite is 748 passed with and without FORCE_COLOR=1. |
||
|
|
cf96288820
|
unsloth start: harden coding-agent installation (#7523)
* unsloth start: harden coding-agent installation * Never let the managed-Node probe break an agent launch * unsloth start: narrow managed Node probe fallback * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep npm lookup compatible with single-arg which stubs and skip POSIX-only tests on Windows Cross-platform runs surfaced two problems in the npm work. `_npm_executable` called `shutil.which("npm", path = ...)` on the common path, which broke callers and tests that stub `shutil.which` with a single positional argument. It now keeps the plain `shutil.which("npm")` lookup and only walks PATH entry by entry after that first hit is rejected as a WSL Windows shim, which is the only case that needs the wider search. Seven tests added in this branch assume POSIX: the managed node layout uses a bare `npm` rather than `npm.cmd`, WSL shim rejection is a no-op when os.name is "nt", and the npm hint is PowerShell quoted there. Marked them skipif os.name == "nt", matching the convention already used in this file. unsloth_cli/tests/test_start.py: 381 passed. * Resolve the npm directory with dirname so a patched os.name cannot pick the wrong Path flavour _install_command used Path(npm).parent. pathlib chooses PosixPath or WindowsPath from os.name at call time, so a caller that overrides os.name (the install tests set it to posix) builds the flavour the host cannot instantiate, and the call raised NotImplementedError on Windows. os.path.dirname gives the same answer without consulting os.name. An empty result now leaves PATH alone rather than prepending the current directory. * Reclaim ephemeral agent homes left behind by abnormal exits Moving ephemeral homes out of the system temp directory and under Studio's auth tree removed the only thing that ever cleaned them up: the OS. When the wrapper is killed by SIGKILL, the console closes or the machine crashes, the context manager's finally never runs, and nothing prunes <STUDIO_HOME>/auth/agents/.tmp, so interrupted sessions accumulate there indefinitely. Only the Windows codex path had reclamation. `_temporary_agent_config` now goes through the same locked session helper that path already used, so every agent gets the scavenge on launch, the advisory lock that keeps a live session from being swept, and the heartbeat that anchors the stale window to wrapper death rather than session start. `_reclaim_stale_ephemeral_sessions` and `_short_ephemeral_session` take the prefix to glob and create, which is the only part that was codex specific. An age-only sweep was not enough on its own: without the live marker a session still running after the stale window would be deleted underneath itself. unsloth_cli/tests/test_start.py: 382 passed. * Route Codex subagent homes through the short Windows parent `_ephemeral_session_parent` matched the agent name exactly, so only `codex` reached the short `~/.unsloth/.tmp/u-codex-*` root. The subagent path is created as `codex-subagent`, and it also nests CODEX_HOME one level deeper under `<home>/parent`, so it needed the short root more than a plain launch, not less. On Windows with an 8 character mkdtemp suffix the resulting CODEX_HOME was 86 characters against 248 for the git limit, where current main was 69 and a plain codex launch is 45. Codex checks out its curated plugins below CODEX_HOME, which is what exceeded the limit in the first place. Both names now take the short root and the same `u-codex-` prefix, so one scavenger pass covers both, and the subagent home lands at 52. `_session_config` now derives its prefix from `_ephemeral_session_prefix` for both branches rather than rebuilding it inline. unsloth_cli/tests/test_start.py: 385 passed. * Fall back to the system temp dir when the Studio auth tree is unwritable Routing every ephemeral home under <STUDIO_HOME>/auth/agents/.tmp made a writable local auth tree a hard requirement for any non-persistent launch. Attaching to a remote or already-running Studio with an explicit key does not need one, and the key cache already tolerates that: _remember_key wraps its write in except OSError. Such launches used tempfile.mkdtemp() before this branch and now died with PermissionError before the agent started. _temporary_agent_config now falls back to the system temp dir when the root cannot be created, which is where these homes lived previously. Reclamation is lost on that path, but the OS prunes it, so nothing accumulates. unsloth_cli/tests/test_start.py: 386 passed. * Give agent probes the augmented PATH and cover an existing unwritable temp root Two gaps in the previous two commits. _which_with_install_dirs restores PATH before returning, so a shim it resolved through Studio's managed Node could not find that node when the probe actually ran it. That broke `opencode debug config` on the --as-subagent path and made the claude and codex version probes report an unsupported build, which changes the flags the agent is launched with. Probes now build their environment with _probe_env, which augments PATH for the child without leaving it set in this process. The unwritable-root fallback only covered mkdir. When the temp root already exists but cannot be written, mkdir(exist_ok = True) succeeds and the failure moves to .cleanup.lock inside _short_ephemeral_session. The whole setup is now inside the guard, so either failure falls back to the system temp dir. unsloth_cli/tests/test_start.py: 388 passed. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep the managed Node on PATH when the home directory is unavailable _augment_path_with_install_dirs returned as soon as Path.home() raised, and the managed Node lookup sits after that return, so a container running under a bare UID never got the managed Node on PATH. An npm agent shim installed there still resolved, then failed because `env node` could not find a node. The same environment is the one the install hint was already taught to handle. Only the user install dirs need a home, so a missing one now leaves them out rather than skipping the rest. When there is nothing to add, PATH is still left untouched, as before. unsloth_cli/tests/test_start.py: 390 passed. * Tighten the comments added on this branch Compress the explanations to the fewest lines that still carry the reason: which failure each guard is for, and which environment reproduces it. No code changes. --------- 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: danielhanchen <unslothai@gmail.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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). |
||
|
|
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> |
||
|
|
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> |
||
|
|
31699f9c04
|
Default coding-agent servers to reasoning off (#7521)
* Default coding agent servers to reasoning off * Fix reasoning startup compatibility and attach warning |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
3d379cdb81
|
Fix local CLI streamed generation error handling (#7135)
Some checks are pending
Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Unsloth GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Unsloth API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Unsloth Updating Tests (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Windows Unsloth GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Windows Unsloth GGUF CI / JSON, images (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
|
||
|
|
c7b17c455b
|
Fix unsloth start on Windows: agent install, PATH resolution, and local model selection (#7257)
Some checks are pending
Unsloth GGUF CI / JSON, images (push) Waiting to run
Unsloth load-orchestrator CI / test (push) Waiting to run
Mac Studio API CI / Unsloth API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Windows Unsloth GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Windows Unsloth GGUF CI / JSON, images (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Unsloth Updating Tests (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
* 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> |
||
|
|
39497e6516
|
Translate PWD for WSL-launched Windows agents (#7111)
Some checks are pending
Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Unsloth GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Unsloth API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Unsloth Updating Tests (push) Waiting to run
Unsloth UI CI / Chat UI Tests (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Windows Unsloth GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Windows Unsloth GGUF CI / JSON, images (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |