unsloth/studio/backend/state/tool_policy.py
Nilay e5cf956601
Studio: shareable per-checkpoint preview links (#6486)
* checkpoint preview endpoint

* harden new preview endpoints

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

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

* address review

* Studio preview: pin adapter, guard streaming submit, robust copy-link

Harden the public per-checkpoint preview surface:

- Pin use_adapter=True in the preview payload sanitizer. Otherwise an
  unauthenticated /p caller can POST use_adapter=false, which calls
  disable_adapter_layers() on the shared in-memory model without restoring
  it; since load_model skips reloads for the same checkpoint, every later
  visitor (the page never sends the field) keeps getting base-model output
  instead of the fine-tuned checkpoint. Forcing it on also re-enables a
  previously disabled adapter and no-ops on merged checkpoints.
- Ignore preview-page submits while a response is streaming. The send
  button was disabled but the Enter handler still called requestSubmit(),
  so a second request could start before the first reply landed in msgs and
  reorder the chat history. Both the keydown and submit handlers now honor
  the disabled button.
- Keep the cloudflare-URL polling loop alive across transient startup fetch
  errors instead of letting one rejection halt it.
- Build the copy-link from a backend preview_ref (output dir relative to
  outputs_root, gated on previewability and the two-segment /p route limit)
  so a nested output dir no longer copies a basename-only link that 404s.
  Expose preview_ref on training run summaries.

Add route-level security tests (path traversal, payload sanitization,
asset containment, CSP header, HTML title escaping, streaming lock held
until drained) and preview_ref unit tests.

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

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

* Studio preview: Safari-safe submit and adapter pin only for LoRA

Follow-ups from cross-browser and route simulations:

- Preview page: send the message from a shared send() helper called by both
  the form submit and the Enter key, instead of form.requestSubmit(). The
  latter throws on Safari < 16 and older iOS, which broke Enter-to-send there.
  Verified across Chromium, Firefox and WebKit with Playwright.
- Only pin use_adapter=True when the resolved checkpoint is a LoRA adapter
  (adapter_config.json present); for a merged checkpoint strip it to None.
  A merged model has no adapter to toggle, so forcing it on only produced a
  per-request "not a PeftModel" warning. The cross-request base-model
  contamination fix still holds for LoRA previews.

Add a merged-checkpoint test asserting use_adapter is stripped to None.

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

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

* Studio preview: trim verbose comments

Tighten comments across the preview routes, page, checkpoint helpers, and tests
to short single-line notes; drop ones that just restate the code. No behavior
change (verified comment/docstring-only with comment_tools.py check).

* Harden preview routes for PR #6486

- Return a generic 400 detail on a rejected preview path so the public /p
  route never echoes the absolute install path (the real reason is logged
  server-side instead).
- Strip confirm_tool_calls, session_id and rag_scope in the preview payload
  sanitizer so the public surface stays inert regardless of the tool gate.
- Use Path.is_relative_to for the asset containment check, matching the rest
  of the codebase.
- Add img-src 'self' and font-src 'self' to the preview page CSP.
- Preview page: on a mid-stream error keep the streamed text, flag the break,
  and restore the prompt so the user can retry; drop the unused --font-sans var.

---------

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: Daniel Han <danielhanchen@gmail.com>
2026-06-24 06:31:53 -07:00

50 lines
1.4 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
"""Process-level server-side tool policy.
Set by `unsloth run` at startup; consulted by the inference route gates.
None -> no CLI override (default). Per-request `enable_tools` is honored.
True -> CLI forced tools on for every request.
False -> CLI forced tools off for every request.
"""
import contextvars
from contextlib import contextmanager
from typing import Iterator, Optional
_tool_policy: Optional[bool] = None
# Per-request hard-off so public surfaces refuse tools even under a CLI `--enable-tools`.
_force_disabled: contextvars.ContextVar[bool] = contextvars.ContextVar(
"tool_policy_force_disabled", default = False
)
def get_tool_policy() -> Optional[bool]:
if _force_disabled.get():
return False
return _tool_policy
@contextmanager
def tools_force_disabled() -> Iterator[None]:
"""Hard-disable server-side tools for the current async context."""
token = _force_disabled.set(True)
try:
yield
finally:
_force_disabled.reset(token)
def set_tool_policy(value: Optional[bool]) -> None:
if value is not None and not isinstance(value, bool):
raise TypeError(f"tool_policy must be Optional[bool], got {type(value).__name__}")
global _tool_policy
_tool_policy = value
def reset_tool_policy() -> None:
global _tool_policy
_tool_policy = None