From 4e8d0da8f963165215b3d04cb4e0a1ce6f6dcf64 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 22 Jun 2026 02:11:18 -0700 Subject: [PATCH] Show model provider/org in the trust_remote_code consent dialog (#6537) * Show model provider/org in the trust_remote_code consent dialog The consent dialog showed only the trailing model name (modelName.split('/').pop()), dropping the owner. The HF org/owner is the 'who do I trust' signal the prompt is asking about, so render it: 'NVIDIA-Nemotron-3-Nano-4B from "unsloth"'. A null provider for local paths and bare names leaves those renders unchanged. Applies to the enable/blocked/malware variants (shared description block). * Only show consent provider tag for a confident single Hub repo Tighten parseModelDisplay so the 'from ""' tag is shown only for a canonical owner/repo Hub id (exactly one slash, both segments non-empty) that is not a local path and not part of a multi-repo scan. This avoids misattributing a relative local directory name (models/llama/7b) or a LoRA base/external repo's finding to the wrong publisher in a trust decision. Extract a ProviderSuffix component so both description branches render the clause identically via &&. * Tighten consent provider-tag comments * Source the consent provider tag from the backend The dialog inferred the provider client-side from the model id, using scanCreatedRepos (a cleanup-only list) to detect multi-repo scope and a regex that missed bare relative paths like a local owner/model dir. Both could attribute the scanned code to the wrong publisher. Move the decision to the backend, where locality and scan scope are known: _consent_provider returns the owner only for a single, non-local, canonical owner/repo Hub id, and the route returns it as payload[provider]. The frontend now renders scan.provider directly. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Suppress consent provider tag when external auto_map code is scanned A single Hub repo can declare an auto_map that loads code from another repo (owner/other--module.Class). The scanner fingerprints that external repo's Python, but security_targets still held only the primary, so the dialog attributed the custom code to the primary publisher. Pass the external refs collected during the scan to _consent_provider and return no provider when any are present, so attribution is shown only for genuinely self-contained repos. * Trim comments to be more succinct --------- Co-authored-by: Daniel Han Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- studio/backend/routes/models.py | 21 ++++++++++++ studio/backend/tests/test_consent_gate.py | 33 +++++++++++++++++++ .../features/security/api/remote-code-api.ts | 2 ++ .../components/remote-code-consent-dialog.tsx | 27 +++++++++++++-- .../security/hooks/use-remote-code-consent.ts | 1 + .../frontend/src/features/security/types.ts | 1 + 6 files changed, 82 insertions(+), 3 deletions(-) diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index c17bb6fb57..1e567774ac 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -1582,6 +1582,23 @@ async def get_model_config( ) +def _consent_provider( + model_name: str, + scanned_targets: List[str], + external_refs: Optional[List[str]] = None, +) -> Optional[str]: + """HF org for the consent dialog's `from ""` tag, or None. + + Returns the owner only for a single, non-local, canonical ``owner/repo`` id; a LoRA's + extra base, a local path, or an external ``auto_map`` ref yields None so the dialog + never misattributes scanned code. + """ + if len(scanned_targets) != 1 or external_refs or is_local_path(model_name): + return None + parts = model_name.split("/") + return parts[0] if len(parts) == 2 and all(parts) else None + + @router.post("/remote-code-scan") async def scan_model_remote_code( model_name: str = Body(..., embed = True), @@ -1645,12 +1662,14 @@ async def scan_model_remote_code( except Exception: pass + external_refs: list = [] 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): + external_refs.append(_ext) _mark_scan_created(_ext) decision = preflight_remote_code_consent_for_targets(security_targets, hf_token = hf_token) payload = decision.response_payload() @@ -1658,6 +1677,8 @@ async def scan_model_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 + # Provider tag decided here, where locality/scan scope/external refs are known. + payload["provider"] = _consent_provider(model_name, security_targets, external_refs) # 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. diff --git a/studio/backend/tests/test_consent_gate.py b/studio/backend/tests/test_consent_gate.py index 3237ffea8d..0fc8d7b695 100644 --- a/studio/backend/tests/test_consent_gate.py +++ b/studio/backend/tests/test_consent_gate.py @@ -815,6 +815,39 @@ class TestRemoteCodeScan: assert not scan_remote_code_files({"modeling_x.py": _SCAN_MALICIOUS}).clean +class TestConsentProvider: + """_consent_provider attributes the dialog's `from ""` tag only when safe.""" + + @staticmethod + def _fn(): + from routes.models import _consent_provider + return _consent_provider + + def test_single_hub_id_returns_owner(self): + assert self._fn()("NVIDIA/Nemotron", ["NVIDIA/Nemotron"]) == "NVIDIA" + assert self._fn()("NVIDIA/Nemotron", ["NVIDIA/Nemotron"], []) == "NVIDIA" + + def test_multi_target_lora_returns_none(self): + # A LoRA scans adapter + base; attributing to one would mislead. + assert self._fn()("user/adapter", ["user/adapter", "NVIDIA/base"]) is None + + def test_external_auto_map_ref_returns_none(self): + # A single repo whose auto_map pulls code from another repo: don't attribute it. + assert self._fn()("owner/repo", ["owner/repo"], ["evilorg/evilrepo"]) is None + + def test_local_path_returns_none(self, tmp_path): + d = tmp_path / "org" / "model" + d.mkdir(parents = True) + assert self._fn()(str(d), [str(d)]) is None + assert self._fn()("/home/me/model", ["/home/me/model"]) is None + + def test_non_canonical_id_returns_none(self): + fn = self._fn() + assert fn("a/b/c", ["a/b/c"]) is None + assert fn("/repo", ["/repo"]) is None + assert fn("plainname", ["plainname"]) is None + + class TestScannerCoversAllExecutableCode: """repo_remote_code_files must collect every .py the loader could execute, so the fingerprint can't certify unscanned code.""" diff --git a/studio/frontend/src/features/security/api/remote-code-api.ts b/studio/frontend/src/features/security/api/remote-code-api.ts index 8c22ec0f11..0e9a75cc0c 100644 --- a/studio/frontend/src/features/security/api/remote-code-api.ts +++ b/studio/frontend/src/features/security/api/remote-code-api.ts @@ -39,6 +39,7 @@ interface RemoteCodeScanResponse { scan_created_repos?: string[]; unsafe_files?: Array<{ path?: string; level?: string }>; security_blocked?: boolean; + provider?: string | null; } /** Scan a model's auto_map code for the consent dialog (backend reads config + repo @@ -93,6 +94,7 @@ export async function getRemoteCodeScan( (data.created_by_scan ? [data.model_name ?? modelName] : []), unsafeFiles, securityBlocked: Boolean(data.security_blocked), + provider: data.provider ?? null, }; } diff --git a/studio/frontend/src/features/security/components/remote-code-consent-dialog.tsx b/studio/frontend/src/features/security/components/remote-code-consent-dialog.tsx index 8f3616553b..6426225e7f 100644 --- a/studio/frontend/src/features/security/components/remote-code-consent-dialog.tsx +++ b/studio/frontend/src/features/security/components/remote-code-consent-dialog.tsx @@ -161,6 +161,24 @@ function FindingCard({ finding }: { finding: RemoteCodeFinding }) { ); } +/** Last path segment of the model id, for display. */ +function modelDisplayName(modelName?: string): string { + if (!modelName) return "This model"; + return modelName.split("/").pop() || modelName; +} + +/** ` from ""` clause, rendered only when a provider was resolved. */ +function ProviderSuffix({ provider }: { provider: string | null }) { + if (!provider) return null; + return ( + <> + {" "} + from{" "} + "{provider}" + + ); +} + /** App-wide consent dialog for trust_remote_code loads: shows scan findings with the * flagged code in context; CRITICAL is a hard block. Mounted once in the root layout. */ export function RemoteCodeConsentDialog() { @@ -168,7 +186,8 @@ export function RemoteCodeConsentDialog() { const scan = useRemoteCodeConsentDialogStore((s) => s.scan); const resolve = useRemoteCodeConsentDialogStore((s) => s.resolve); - const displayName = scan?.modelName?.split("/").pop() || "This model"; + const displayName = modelDisplayName(scan?.modelName); + const provider = scan?.provider ?? null; const blocked = scan ? !scan.approvable : false; const findings = scan?.findings ?? []; const unsafeFiles = scan?.unsafeFiles ?? []; @@ -218,7 +237,8 @@ export function RemoteCodeConsentDialog() { <> {displayName} - {" "} + + {" "} contains files that Hugging Face's security scan flagged as unsafe (for example, a malicious pickle that would run code when the model loads). It cannot be loaded. The flagged @@ -228,7 +248,8 @@ export function RemoteCodeConsentDialog() { <> {displayName} - {" "} + + {" "} declares custom Python code in its repository.{" "} {blocked ? "A security scan flagged CRITICAL issues, so it cannot be enabled." diff --git a/studio/frontend/src/features/security/hooks/use-remote-code-consent.ts b/studio/frontend/src/features/security/hooks/use-remote-code-consent.ts index 363c8249c7..68045559b3 100644 --- a/studio/frontend/src/features/security/hooks/use-remote-code-consent.ts +++ b/studio/frontend/src/features/security/hooks/use-remote-code-consent.ts @@ -42,6 +42,7 @@ export async function confirmRemoteCodeIfNeeded({ scanCreatedRepos: [], unsafeFiles: [], securityBlocked: false, + provider: null, }; } diff --git a/studio/frontend/src/features/security/types.ts b/studio/frontend/src/features/security/types.ts index f11dbdd38b..0c850c3bfe 100644 --- a/studio/frontend/src/features/security/types.ts +++ b/studio/frontend/src/features/security/types.ts @@ -43,4 +43,5 @@ export interface RemoteCodeScan { scanCreatedRepos: string[]; unsafeFiles: UnsafeFile[]; // files HF flagged unsafe; non-empty => hard block securityBlocked: boolean; // blocked specifically by the malware gate + provider: string | null; // HF org for the "from " tag; null when unattributable }