diff --git a/.github/workflows/mlx-ci.yml b/.github/workflows/mlx-ci.yml index 864630f9f0..424a706d7c 100644 --- a/.github/workflows/mlx-ci.yml +++ b/.github/workflows/mlx-ci.yml @@ -231,99 +231,6 @@ jobs: tests/studio/test_is_mlx_dispatch_gate.py \ tests/studio/test_mlx_training_worker_behaviors.py - # Studio prebuilt llama.cpp install + GGUF inference. Mirrors the - # path Studio's setup.sh takes on macOS since #5963: plan against - # the unslothai/llama.cpp fork's latest release, which ships the - # bin-macos-arm64 bundle plus the llama-prebuilt-manifest.json the - # default policy reads. After install, downloads a small published - # GGUF (unsloth/gemma-3-270m-it-GGUF, Q4_K_M) and validates - # llama-server /completion end to end. An install failure or a - # non-zero binary exit is an Unsloth/Studio bug. - - name: Studio prebuilt llama.cpp install + GGUF inference (Mac M1) - env: - # Withheld on PR: this step runs checked-out PR code; public GGUF still downloads. - HF_TOKEN: ${{ github.event_name != 'pull_request' && secrets.HF_TOKEN || '' }} - # install_llama_prebuilt.py hits the GitHub releases API to - # resolve the asset URL. Anonymous calls share the runner-IP - # rate-limit bucket and 403 quickly -- pass the workflow's - # automatic GITHUB_TOKEN to bump us to the 5000/hr authenticated - # bucket. - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - INSTALL_DIR="$HOME/.unsloth-studio-prebuilt-test/llama.cpp" - rm -rf "$INSTALL_DIR" - # Mirror studio/setup.sh on macOS (the install.sh user path): - # it plans against the unslothai/llama.cpp fork's latest - # release with no policy or tag flags. - python studio/install_llama_prebuilt.py \ - --install-dir "$INSTALL_DIR" \ - --published-repo unslothai/llama.cpp - - # Studio bundles only llama-server + llama-quantize from the - # prebuilt (not llama-cli) -- inference goes through - # llama-server's HTTP /completion endpoint. Validate both: - # llama-quantize --help proves the dynamic libs link, then - # spin up llama-server and POST a /completion request on a - # tiny published GGUF. - LLAMA_SERVER="$INSTALL_DIR/build/bin/llama-server" - LLAMA_QUANT="$INSTALL_DIR/build/bin/llama-quantize" - [ -x "$LLAMA_SERVER" ] || { echo "::error::llama-server missing at $LLAMA_SERVER"; find "$INSTALL_DIR/build" -type f | head -40; exit 1; } - [ -x "$LLAMA_QUANT" ] || { echo "::error::llama-quantize missing at $LLAMA_QUANT"; exit 1; } - echo "llama-server : $LLAMA_SERVER" - echo "llama-quantize: $LLAMA_QUANT" - "$LLAMA_QUANT" --help >/dev/null && echo " llama-quantize loads OK" - - mkdir -p /tmp/ggufs - bash .github/scripts/hf-download-with-retry.sh \ - 'unsloth/gemma-3-270m-it-GGUF' \ - 'gemma-3-270m-it-Q4_K_M.gguf' \ - /tmp/ggufs - - PORT=18080 - echo "=== starting llama-server on 127.0.0.1:$PORT ===" - "$LLAMA_SERVER" \ - -m /tmp/ggufs/gemma-3-270m-it-Q4_K_M.gguf \ - --host 127.0.0.1 \ - --port "$PORT" \ - -c 256 \ - -n 16 \ - --no-warmup \ - > /tmp/llama-server.log 2>&1 & - SERVER_PID=$! - trap 'kill "$SERVER_PID" 2>/dev/null || true' EXIT - - # Wait for /health to come up - for i in $(seq 1 30); do - if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then - echo " server up after ${i}s" - break - fi - sleep 1 - done - if ! curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then - echo "::error::llama-server never became healthy" - tail -40 /tmp/llama-server.log - exit 1 - fi - - PROMPT="Hello, my name is" - echo "=== POST /completion ===" - RESP=$(curl -sf -X POST "http://127.0.0.1:$PORT/completion" \ - -H 'Content-Type: application/json' \ - -d "{\"prompt\":\"$PROMPT\",\"n_predict\":16,\"temperature\":0,\"seed\":3407}") - echo "raw response (head): $(echo "$RESP" | head -c 600)" - CONTENT=$(echo "$RESP" | python -c "import json,sys; print(json.loads(sys.stdin.read()).get('content',''))") - echo "completion content: $CONTENT" - - if [ -z "$CONTENT" ]; then - echo "::error::llama-server /completion returned empty content" - tail -40 /tmp/llama-server.log - exit 1 - fi - echo "OK: Studio prebuilt llama.cpp on Mac M1 + GGUF /completion works" - # Real MLX training + inference smoke test. Trains # unsloth/gemma-3-270m-it for 7 deterministic LoRA steps # (batch_size=2, gradient_accumulation_steps=3) on a single @@ -338,6 +245,9 @@ jobs: UNSLOTH_COMPILE_DISABLE: '1' run: | mkdir -p mlx_workdir + # Authenticate llama.cpp's release-API lookup (anonymous 403s on rate-limit); + # read-only GITHUB_TOKEN scoped here only, never to steps that run binaries. + GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" \ python tests/studio/run_real_mlx_smoke.py train \ --workdir "$PWD/mlx_workdir" @@ -406,3 +316,88 @@ jobs: cat "$f" 2>/dev/null || echo "(missing)" echo done + + # Validates the macOS prebuilt path Studio's setup.sh uses (#5963): install the + # unslothai/llama.cpp fork's latest release, download a small public GGUF, and + # check llama-server /completion end to end. Split and placed last so the + # untrusted binary runs only in the final smoke step, after every HF_TOKEN step, + # leaving no token-bearing step or shared workspace for a tampered prebuilt to + # corrupt. GH_TOKEN: releases API; HF_TOKEN (withheld on PR): probe + GGUF fetch. + - name: Studio prebuilt llama.cpp install + GGUF download (Mac M1) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HF_TOKEN: ${{ github.event_name != 'pull_request' && secrets.HF_TOKEN || '' }} + run: | + set -euo pipefail + INSTALL_DIR="$HOME/.unsloth-studio-prebuilt-test/llama.cpp" + rm -rf "$INSTALL_DIR" + # Download only -- no llama-quantize / llama-server launch in this step. + python studio/install_llama_prebuilt.py \ + --install-dir "$INSTALL_DIR" \ + --published-repo unslothai/llama.cpp + mkdir -p /tmp/ggufs + bash .github/scripts/hf-download-with-retry.sh \ + 'unsloth/gemma-3-270m-it-GGUF' \ + 'gemma-3-270m-it-Q4_K_M.gguf' \ + /tmp/ggufs + + # Final step: runs the downloaded binaries with no secrets present, and clears + # the GitHub Actions command files so a tampered prebuilt cannot influence the job. + - name: Studio prebuilt llama.cpp GGUF inference smoke (Mac M1) + run: | + set -euo pipefail + unset GITHUB_ENV GITHUB_PATH GITHUB_OUTPUT GITHUB_STEP_SUMMARY + INSTALL_DIR="$HOME/.unsloth-studio-prebuilt-test/llama.cpp" + # Studio bundles only llama-server + llama-quantize (not llama-cli); + # inference goes through llama-server's HTTP /completion endpoint. + LLAMA_SERVER="$INSTALL_DIR/build/bin/llama-server" + LLAMA_QUANT="$INSTALL_DIR/build/bin/llama-quantize" + [ -x "$LLAMA_SERVER" ] || { echo "::error::llama-server missing at $LLAMA_SERVER"; find "$INSTALL_DIR/build" -type f | head -40; exit 1; } + [ -x "$LLAMA_QUANT" ] || { echo "::error::llama-quantize missing at $LLAMA_QUANT"; exit 1; } + echo "llama-server : $LLAMA_SERVER" + echo "llama-quantize: $LLAMA_QUANT" + "$LLAMA_QUANT" --help >/dev/null && echo " llama-quantize loads OK" + + PORT=18080 + echo "=== starting llama-server on 127.0.0.1:$PORT ===" + "$LLAMA_SERVER" \ + -m /tmp/ggufs/gemma-3-270m-it-Q4_K_M.gguf \ + --host 127.0.0.1 \ + --port "$PORT" \ + -c 256 \ + -n 16 \ + --no-warmup \ + > /tmp/llama-server.log 2>&1 & + SERVER_PID=$! + trap 'kill "$SERVER_PID" 2>/dev/null || true' EXIT + + # Wait for /health to come up + for i in $(seq 1 30); do + if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then + echo " server up after ${i}s" + break + fi + sleep 1 + done + if ! curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then + echo "::error::llama-server never became healthy" + tail -40 /tmp/llama-server.log + exit 1 + fi + + PROMPT="Hello, my name is" + echo "=== POST /completion ===" + RESP=$(curl -sf -X POST "http://127.0.0.1:$PORT/completion" \ + -H 'Content-Type: application/json' \ + -d "{\"prompt\":\"$PROMPT\",\"n_predict\":16,\"temperature\":0,\"seed\":3407}") + echo "raw response (head): $(echo "$RESP" | head -c 600)" + CONTENT=$(echo "$RESP" | python -c "import json,sys; print(json.loads(sys.stdin.read()).get('content',''))") + echo "completion content: $CONTENT" + + if [ -z "$CONTENT" ]; then + echo "::error::llama-server /completion returned empty content" + tail -40 /tmp/llama-server.log + exit 1 + fi + echo "OK: Studio prebuilt llama.cpp on Mac M1 + GGUF /completion works" diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index c7f34e39f2..e40cb3083e 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -7,6 +7,7 @@ from __future__ import annotations import argparse +import atexit import errno import fnmatch import hashlib @@ -5203,7 +5204,8 @@ def ldconfig_runtime_dirs(required_libraries: Iterable[str]) -> list[str]: def linux_runtime_dirs(binary_path: Path) -> list[str]: - missing = linux_missing_libraries(binary_path) + # ldd may execute the binary, so probe it with a secret-free env. + missing = linux_missing_libraries(binary_path, env = scrubbed_environ()) if not missing: return [] return linux_runtime_dirs_for_required_libraries(missing) @@ -5499,6 +5501,140 @@ def _wsl_system_rocm_lib_dirs() -> list[str]: return out +# Secrets a downloaded llama.cpp binary never needs; keep them out of binary_env(). +# The installer's own API calls read os.environ directly, so auth is unaffected. +_SECRET_ENV_EXACT_NAMES = frozenset( + { + "HF_TOKEN", + "HUGGING_FACE_HUB_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", + "WANDB_API_KEY", + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + "GOOGLE_APPLICATION_CREDENTIALS", + "AZURE_CLIENT_SECRET", + "ACTIONS_ID_TOKEN_REQUEST_TOKEN", + "ACTIONS_ID_TOKEN_REQUEST_URL", + "ACTIONS_RUNTIME_TOKEN", + # Credential pointers (cluster / remote-host access). + "KUBECONFIG", + "SSH_AUTH_SOCK", + } +) +# Case-insensitive substring markers for names we do not enumerate (no bare "KEY", +# which would hit benign runtime vars). +_SECRET_ENV_MARKERS = ( + "TOKEN", + "SECRET", + "PASSWORD", + "PASSWD", + "PASSPHRASE", + "CREDENTIAL", + "PRIVATE_KEY", + "API_KEY", +) +# Proxy / index URLs embed creds in their value; the offline binaries never need them. +_SECRET_ENV_URL_NAMES = frozenset( + { + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "FTP_PROXY", + "RSYNC_PROXY", + "PIP_INDEX_URL", + "PIP_EXTRA_INDEX_URL", + "UV_INDEX_URL", + "UV_DEFAULT_INDEX", + "UV_EXTRA_INDEX_URL", + } +) +# Also drop values with URL userinfo creds (scheme://user:secret@host or token@host). +_URL_USERINFO_CREDENTIAL_RE = re.compile(r"://[^/@\s]+@") + + +def is_secret_env_name(name: str) -> bool: + upper = name.upper() + return ( + upper in _SECRET_ENV_EXACT_NAMES + or upper in _SECRET_ENV_URL_NAMES + or any(marker in upper for marker in _SECRET_ENV_MARKERS) + ) + + +def scrub_env(env: dict[str, str]) -> dict[str, str]: + """Drop secret-bearing variables before handing an env to a downloaded binary.""" + return { + key: value + for key, value in env.items() + if not is_secret_env_name(key) and not _URL_USERINFO_CREDENTIAL_RE.search(value or "") + } + + +# Home / cache pointers to on-disk token stores (~/.cache/huggingface/token, +# ~/.aws/credentials, ...). Stripping env tokens is not enough; point these at an +# empty home so the binary cannot read those files via $HOME. +_RUNTIME_HOME_POINTER_VARS = ( + "HOME", + "USERPROFILE", + "APPDATA", + "LOCALAPPDATA", + "XDG_CACHE_HOME", + "XDG_CONFIG_HOME", + "XDG_DATA_HOME", + "HF_HOME", + "HUGGINGFACE_HUB_CACHE", + "HF_HUB_CACHE", +) +# Credential / config file pointers outside HOME; drop so lookups fall back to the +# empty home. +_CREDENTIAL_FILE_POINTER_VARS = ( + "NETRC", + "PIP_CONFIG_FILE", + "DOCKER_CONFIG", + "GIT_CONFIG_GLOBAL", +) +# GitHub Actions command files: appending to these injects PATH/env into later steps. +_CI_COMMAND_FILE_VARS = ( + "GITHUB_ENV", + "GITHUB_PATH", + "GITHUB_OUTPUT", + "GITHUB_STEP_SUMMARY", + "BASH_ENV", +) + +_isolated_runtime_home_dir: str | None = None + + +def isolated_runtime_home() -> str: + # Empty dir, created lazily and removed at exit. (A binary resolving the real + # home via getpwuid is out of scope; that needs OS sandboxing.) + global _isolated_runtime_home_dir + if _isolated_runtime_home_dir is None: + path = tempfile.mkdtemp(prefix = "unsloth-prebuilt-home-") + atexit.register(shutil.rmtree, path, ignore_errors = True) + _isolated_runtime_home_dir = path + return _isolated_runtime_home_dir + + +def scrubbed_environ() -> dict[str, str]: + # os.environ minus secrets, with home / credential pointers neutralised. Used for + # the binary env and any probe (e.g. ldd) that runs the untrusted binary. + env = scrub_env(os.environ.copy()) + runtime_home = isolated_runtime_home() + for pointer in _RUNTIME_HOME_POINTER_VARS: + env[pointer] = runtime_home + # Windows rebuilds the profile from %HOMEDRIVE%%HOMEPATH% (no-op pair on POSIX). + drive, tail = os.path.splitdrive(runtime_home) + env["HOMEDRIVE"], env["HOMEPATH"] = drive, tail or runtime_home + for pointer in (*_CREDENTIAL_FILE_POINTER_VARS, *_CI_COMMAND_FILE_VARS): + env.pop(pointer, None) + return env + + def binary_env( binary_path: Path, install_dir: Path, @@ -5506,7 +5642,7 @@ def binary_env( *, runtime_line: str | None = None, ) -> dict[str, str]: - env = os.environ.copy() + env = scrubbed_environ() if host.is_windows: path_dirs = [ str(binary_path.parent), diff --git a/tests/studio/install/test_install_llama_prebuilt_logic.py b/tests/studio/install/test_install_llama_prebuilt_logic.py index 9ee8759bb4..c852d7c495 100644 --- a/tests/studio/install/test_install_llama_prebuilt_logic.py +++ b/tests/studio/install/test_install_llama_prebuilt_logic.py @@ -22,6 +22,9 @@ SPEC.loader.exec_module(INSTALL_LLAMA_PREBUILT) PrebuiltFallback = INSTALL_LLAMA_PREBUILT.PrebuiltFallback extract_archive = INSTALL_LLAMA_PREBUILT.extract_archive binary_env = INSTALL_LLAMA_PREBUILT.binary_env +is_secret_env_name = INSTALL_LLAMA_PREBUILT.is_secret_env_name +scrub_env = INSTALL_LLAMA_PREBUILT.scrub_env +isolated_runtime_home = INSTALL_LLAMA_PREBUILT.isolated_runtime_home HostInfo = INSTALL_LLAMA_PREBUILT.HostInfo AssetChoice = INSTALL_LLAMA_PREBUILT.AssetChoice ApprovedArtifactHash = INSTALL_LLAMA_PREBUILT.ApprovedArtifactHash @@ -779,6 +782,259 @@ def test_binary_env_linux_includes_binary_parent_in_ld_library_path( assert str(install_dir) in ld_dirs +def test_scrub_env_drops_secrets_and_keeps_runtime_vars(): + raw = { + # secrets + "HF_TOKEN": "hf_x", + "HUGGING_FACE_HUB_TOKEN": "hf_y", + "GH_TOKEN": "gh_x", + "GITHUB_TOKEN": "gh_y", + "WANDB_API_KEY": "wandb_x", + "AWS_SECRET_ACCESS_KEY": "aws_x", + "ACTIONS_ID_TOKEN_REQUEST_TOKEN": "oidc_x", + "ACTIONS_ID_TOKEN_REQUEST_URL": "https://oidc", + "SOME_VENDOR_API_KEY": "vendor_x", + "DB_PASSWORD": "pw", + "MY_PRIVATE_KEY": "pk", + "KUBECONFIG": "/home/runner/.kube/config", + "SSH_AUTH_SOCK": "/tmp/ssh-agent.sock", + "SSH_PASSPHRASE": "ssh_pass", + # runtime vars to keep + "PATH": "/usr/bin", + "LD_LIBRARY_PATH": "/opt/lib", + "DYLD_LIBRARY_PATH": "/opt/dyld", + "HOME": "/home/runner", + "TMPDIR": "/tmp", + "CUDA_VISIBLE_DEVICES": "0", + "HSA_OVERRIDE_GFX_VERSION": "11.0.0", + } + + cleaned = scrub_env(raw) + + for secret in ( + "HF_TOKEN", + "HUGGING_FACE_HUB_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", + "WANDB_API_KEY", + "AWS_SECRET_ACCESS_KEY", + "ACTIONS_ID_TOKEN_REQUEST_TOKEN", + "ACTIONS_ID_TOKEN_REQUEST_URL", + "SOME_VENDOR_API_KEY", + "DB_PASSWORD", + "MY_PRIVATE_KEY", + "KUBECONFIG", + "SSH_AUTH_SOCK", + "SSH_PASSPHRASE", + ): + assert secret not in cleaned, f"{secret} must be stripped from binary env" + + for keep in ( + "PATH", + "LD_LIBRARY_PATH", + "DYLD_LIBRARY_PATH", + "HOME", + "TMPDIR", + "CUDA_VISIBLE_DEVICES", + "HSA_OVERRIDE_GFX_VERSION", + ): + assert cleaned[keep] == raw[keep], f"{keep} must be preserved for the binary" + + # no bare "KEY" marker: benign KEY-containing names survive + assert is_secret_env_name("API_KEY") is True + assert is_secret_env_name("SSH_KEYFILE_PATH") is False + assert is_secret_env_name("PATH") is False + + +def test_scrub_env_drops_proxy_index_and_embedded_url_credentials(): + raw = { + # proxy / package-index URLs whose values commonly embed credentials + "HTTPS_PROXY": "https://user:secret@proxy:8080", + "https_proxy": "https://user:secret@proxy:8080", # lower-case variant + "ALL_PROXY": "socks5://user:secret@proxy:1080", + "PIP_INDEX_URL": "https://u:p@pypi.internal/simple", + "UV_INDEX_URL": "https://u:p@index.internal/simple", + # credentials embedded in an otherwise benign-named variable's value + "MY_DB_DSN": "postgres://admin:secret@db:5432/app", + # benign vars the binary needs, including a URL with no userinfo + "PATH": "/usr/bin", + "CUDA_VISIBLE_DEVICES": "0", + "NO_PROXY": "localhost,127.0.0.1", + "SOME_ENDPOINT": "https://example.com:8080/v1", + } + + cleaned = scrub_env(raw) + + for secret in ( + "HTTPS_PROXY", + "https_proxy", + "ALL_PROXY", + "PIP_INDEX_URL", + "UV_INDEX_URL", + "MY_DB_DSN", + ): + assert secret not in cleaned, f"{secret} must be stripped from binary env" + for keep in ("PATH", "CUDA_VISIBLE_DEVICES", "NO_PROXY", "SOME_ENDPOINT"): + assert cleaned[keep] == raw[keep], f"{keep} must be preserved for the binary" + + assert is_secret_env_name("HTTPS_PROXY") is True + assert is_secret_env_name("https_proxy") is True + assert is_secret_env_name("NO_PROXY") is False + + +def test_binary_env_strips_secrets_from_downloaded_binary_environment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + bin_dir = install_dir / "build" / "bin" + bin_dir.mkdir(parents = True) + binary_path = bin_dir / "llama-server" + binary_path.write_bytes(b"fake") + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_runtime_dirs", lambda _bp: []) + + monkeypatch.setenv("HF_TOKEN", "hf_secret_from_ci") + monkeypatch.setenv("GITHUB_TOKEN", "gh_secret_from_ci") + monkeypatch.setenv("GH_TOKEN", "gh_secret_from_ci") + monkeypatch.setenv("WANDB_API_KEY", "wandb_secret_from_ci") + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "1") + + env = binary_env(binary_path, install_dir, host) + + assert "HF_TOKEN" not in env + assert "GITHUB_TOKEN" not in env + assert "GH_TOKEN" not in env + assert "WANDB_API_KEY" not in env + # library/runtime resolution unaffected + assert str(bin_dir) in env["LD_LIBRARY_PATH"].split(os.pathsep) + assert env["CUDA_VISIBLE_DEVICES"] == "1" + + +def test_binary_env_redirects_home_away_from_real_credential_stores( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + bin_dir = install_dir / "build" / "bin" + bin_dir.mkdir(parents = True) + binary_path = bin_dir / "llama-server" + binary_path.write_bytes(b"fake") + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_runtime_dirs", lambda _bp: []) + + real_home = str(tmp_path / "real_home") + monkeypatch.setenv("HOME", real_home) + monkeypatch.setenv("HF_HOME", real_home + "/.cache/huggingface") + + env = binary_env(binary_path, install_dir, host) + + # HOME and the cache pointers are redirected to a single empty, existing dir. + assert env["HOME"] != real_home + assert env["HF_HOME"] == env["HOME"] + assert env["HOME"] == isolated_runtime_home() + assert os.path.isdir(env["HOME"]) + assert os.listdir(env["HOME"]) == [] + # Windows reconstructs the profile from HOMEDRIVE + HOMEPATH. + assert env["HOMEDRIVE"] + env["HOMEPATH"] == env["HOME"] + + +def test_scrub_env_drops_token_only_url_userinfo(): + raw = { + "GENERIC_REPO": "https://ghp_tokenonly@github.com/org/repo", + "GENERIC_OK": "https://example.com:8080/v1", + } + cleaned = scrub_env(raw) + assert "GENERIC_REPO" not in cleaned + assert cleaned["GENERIC_OK"] == raw["GENERIC_OK"] + + +def test_binary_env_drops_explicit_credential_file_pointers( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_runtime_dirs", lambda _bp: []) + dropped = ( + "NETRC", + "PIP_CONFIG_FILE", + "DOCKER_CONFIG", + "GIT_CONFIG_GLOBAL", + "GITHUB_ENV", + "GITHUB_PATH", + "GITHUB_OUTPUT", + "GITHUB_STEP_SUMMARY", + "BASH_ENV", + ) + for var in dropped: + monkeypatch.setenv(var, "/home/realuser/secret") + + env = binary_env(tmp_path / "llama-server", tmp_path, host) + + for var in dropped: + assert var not in env + + +def test_linux_runtime_dirs_probes_with_secret_free_env(monkeypatch: pytest.MonkeyPatch): + captured: dict[str, object] = {} + + def fake_missing(binary_path, *, env = None): + captured["env"] = env + return [] + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_missing_libraries", fake_missing) + monkeypatch.setenv("HF_TOKEN", "hf_secret") + monkeypatch.setenv("GITHUB_TOKEN", "gh_secret") + + INSTALL_LLAMA_PREBUILT.linux_runtime_dirs(Path("/fake/llama-server")) + + probe_env = captured["env"] + assert probe_env is not None + assert "HF_TOKEN" not in probe_env + assert "GITHUB_TOKEN" not in probe_env + + def test_install_prebuilt_falls_back_to_older_release_plan( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ):