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 "<provider>"' 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 <michaelhan2050@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
1582d2854c
commit
4e8d0da8f9
6 changed files with 82 additions and 3 deletions
|
|
@ -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 "<provider>"` 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.
|
||||
|
|
|
|||
|
|
@ -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 "<provider>"` 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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "<provider>"` clause, rendered only when a provider was resolved. */
|
||||
function ProviderSuffix({ provider }: { provider: string | null }) {
|
||||
if (!provider) return null;
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
from{" "}
|
||||
<span className="font-medium text-foreground">"{provider}"</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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() {
|
|||
<>
|
||||
<span className="font-medium text-foreground">
|
||||
{displayName}
|
||||
</span>{" "}
|
||||
</span>
|
||||
<ProviderSuffix provider={provider} />{" "}
|
||||
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() {
|
|||
<>
|
||||
<span className="font-medium text-foreground">
|
||||
{displayName}
|
||||
</span>{" "}
|
||||
</span>
|
||||
<ProviderSuffix provider={provider} />{" "}
|
||||
declares custom Python code in its repository.{" "}
|
||||
{blocked
|
||||
? "A security scan flagged CRITICAL issues, so it cannot be enabled."
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export async function confirmRemoteCodeIfNeeded({
|
|||
scanCreatedRepos: [],
|
||||
unsafeFiles: [],
|
||||
securityBlocked: false,
|
||||
provider: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <provider>" tag; null when unattributable
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue