Skip to content

Commit b148d70

Browse files
SecAI-Hubclaude
andcommitted
M54: On-demand diffusion runtime acquisition — one-click install from UI
Replace the manual SSH-based diffusion activation with a self-service UI flow. The base OS ships without the ~5 GB diffusion runtime; when the user first visits the Generate page, they see a consent dialog and click "Enable Generation" — everything else is automatic. Privilege model: UI writes a request marker file (O_CREAT|O_EXCL, 0600) into its RuntimeDirectory; a systemd path unit detects it and triggers a privileged oneshot installer. The UI never runs the installer directly. Installer pipeline (secai-enable-diffusion.sh): - Validates Python version + architecture against manifest - Detects GPU backend (CUDA/ROCm/CPU) or accepts --backend override - Downloads wheels with full quarantine: source allowlist, redirect verification, HTTPS-only, wheel-only format gate, SHA256 hash, .dist-info metadata inspection, wheel tag/ABI/platform check - Installs offline (pip --no-index --require-hashes) into temp venv - Smoke tests imports + device detection + tensor op (no model needed) - Atomic venv swap + systemd override + enable+start - Fail-closed rollback on any error; flock prevents concurrent runs - --from-local secondary path for air-gapped/admin use - Progress file for UI consumption; cache with full context invalidation New files: - diffusion-runtime-manifest.yaml — per-backend wheel trust anchor - diffusion-{cpu,cuda,rocm}.lock — backend-specific pip lockfiles - secure-ai-enable-diffusion.{path,service} — privileged activation units UI endpoints: GET /api/diffusion/runtime/status, POST .../enable, GET .../progress Also fixes: - Windows test compatibility: import paths, Flask 3.x cookie_jar removal, platform skipif for symlink/permission tests, flexible error assertions - Path separator portability in storage.py and sandbox.py (os.sep) - API docs updated with diffusion runtime contract 901 tests pass, 22 skipped (Windows platform guards — all run on Linux CI). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e6fbf02 commit b148d70

24 files changed

Lines changed: 3187 additions & 26 deletions

docs/api.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,78 @@ Generate text from a prompt (non-chat completion).
255255
```
256256
- **Response:** `200 OK` -- generated text
257257

258+
### Diffusion Runtime (On-Demand Acquisition)
259+
260+
The diffusion runtime (PyTorch, diffusers, etc.) is not included in the base OS image. These endpoints manage the one-click install flow.
261+
262+
**Contract:**
263+
- `GET /api/diffusion/runtime/status` is the source of truth for whether the runtime is installed, failed, or available for install. Always safe to call.
264+
- `POST /api/diffusion/runtime/enable` requests installation by writing a marker file. A systemd path unit triggers the privileged installer.
265+
- `GET /api/diffusion/runtime/progress` is only meaningful after `enable` has been called. Callers should check `status` first.
266+
267+
#### GET /api/diffusion/runtime/status
268+
269+
Return the current diffusion runtime state.
270+
271+
- **Response:** `200 OK`
272+
```json
273+
{
274+
"installed": false,
275+
"detected_backend": "cuda",
276+
"estimated_size_mb": 4500,
277+
"cache_available": false,
278+
"installing": false,
279+
"error": null
280+
}
281+
```
282+
- **Fields:**
283+
- `installed` -- true if the runtime is installed and the service is enabled
284+
- `detected_backend` -- auto-detected GPU backend: `"cuda"`, `"rocm"`, `"cpu"`, or `null` if detection failed
285+
- `estimated_size_mb` -- estimated download size from manifest for the detected backend; `null` if backend unknown
286+
- `cache_available` -- true if verified wheel cache exists (faster re-install)
287+
- `installing` -- true if an install is in progress (request marker or active progress)
288+
- `error` -- error detail from the last failed install, or `null`
289+
- **Status priority:** installed > failed (suppresses installing) > in-progress > not installed
290+
291+
#### POST /api/diffusion/runtime/enable
292+
293+
Request diffusion runtime installation.
294+
295+
- **Response:** `202 Accepted` -- install requested
296+
```json
297+
{ "status": "installing" }
298+
```
299+
- **Response:** `200 OK` -- already installed
300+
```json
301+
{ "status": "already_installed" }
302+
```
303+
- **Error:** `409 Conflict` -- install already in progress
304+
```json
305+
{ "status": "already_installing" }
306+
```
307+
- **Notes:** Does not directly run the installer. Atomically creates a request marker file (`O_CREAT|O_EXCL`, mode `0600`). A systemd path unit detects the marker and starts the privileged oneshot installer.
308+
309+
#### GET /api/diffusion/runtime/progress
310+
311+
Return current install progress from the installer's progress file.
312+
313+
- **Response:** `200 OK`
314+
```json
315+
{
316+
"phase": "downloading",
317+
"percent": 45,
318+
"backend": "cuda",
319+
"detail": "Downloading torch-2.3.1+cu121...",
320+
"total_packages": 42,
321+
"downloaded": 19,
322+
"verified": 19,
323+
"cached_hits": 5,
324+
"error": null
325+
}
326+
```
327+
- **Valid phases:** `detecting`, `downloading`, `verifying`, `installing`, `smoke_testing`, `enabling`, `complete`, `failed`, or `null`
328+
- **Notes:** Only meaningful after `POST /api/diffusion/runtime/enable` has been called. When no install has ever been requested, returns `phase: null`. When the install completed, returns `complete`; when it failed, returns `failed`. Invalid phases from the progress file are normalized to `failed`. All branches return the same field set.
329+
258330
### Vault Management
259331

260332
#### GET /api/vault/status

files/scripts/diffusion-cpu.lock

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#
2+
# Backend-specific diffusion runtime lockfile: CPU
3+
#
4+
# Generated by pip-compile with Python 3.12 (placeholder — regenerate with
5+
# scripts/refresh-diffusion-locks.sh before first use).
6+
#
7+
# pip-compile --allow-unsafe --generate-hashes \
8+
# --extra-index-url https://download.pytorch.org/whl/cpu \
9+
# --output-file=diffusion-cpu.lock diffusion-in.txt
10+
#
11+
# This lockfile is consumed by the diffusion installer in offline mode:
12+
# pip install --no-index --require-hashes --find-links=<verified-cache> -r diffusion-cpu.lock
13+
#
14+
15+
accelerate==1.13.0 \
16+
--hash=sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0 \
17+
--hash=sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236
18+
# via -r diffusion-in.txt
19+
diffusers==0.28.0 \
20+
--hash=sha256:PLACEHOLDER_HASH_1 \
21+
--hash=sha256:PLACEHOLDER_HASH_2
22+
# via -r diffusion-in.txt
23+
safetensors==0.4.3 \
24+
--hash=sha256:PLACEHOLDER_HASH_1 \
25+
--hash=sha256:PLACEHOLDER_HASH_2
26+
# via
27+
# accelerate
28+
# diffusers
29+
# transformers
30+
torch==2.3.1+cpu \
31+
--hash=sha256:PLACEHOLDER_HASH_1
32+
# via
33+
# -r diffusion-in.txt
34+
# accelerate
35+
# diffusers
36+
transformers==4.41.2 \
37+
--hash=sha256:PLACEHOLDER_HASH_1 \
38+
--hash=sha256:PLACEHOLDER_HASH_2
39+
# via -r diffusion-in.txt
40+
41+
# NOTE: This is a placeholder lockfile with representative entries.
42+
# Run scripts/refresh-diffusion-locks.sh to generate the complete
43+
# lockfile with all transitive dependencies and real SHA256 hashes.

files/scripts/diffusion-cuda.lock

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#
2+
# Backend-specific diffusion runtime lockfile: CUDA 12.1
3+
#
4+
# Generated by pip-compile with Python 3.12 (placeholder — regenerate with
5+
# scripts/refresh-diffusion-locks.sh before first use).
6+
#
7+
# pip-compile --allow-unsafe --generate-hashes \
8+
# --extra-index-url https://download.pytorch.org/whl/cu121 \
9+
# --output-file=diffusion-cuda.lock diffusion-in.txt
10+
#
11+
# This lockfile is consumed by the diffusion installer in offline mode:
12+
# pip install --no-index --require-hashes --find-links=<verified-cache> -r diffusion-cuda.lock
13+
#
14+
15+
accelerate==1.13.0 \
16+
--hash=sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0 \
17+
--hash=sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236
18+
# via -r diffusion-in.txt
19+
diffusers==0.28.0 \
20+
--hash=sha256:PLACEHOLDER_HASH_1 \
21+
--hash=sha256:PLACEHOLDER_HASH_2
22+
# via -r diffusion-in.txt
23+
safetensors==0.4.3 \
24+
--hash=sha256:PLACEHOLDER_HASH_1 \
25+
--hash=sha256:PLACEHOLDER_HASH_2
26+
# via
27+
# accelerate
28+
# diffusers
29+
# transformers
30+
torch==2.3.1+cu121 \
31+
--hash=sha256:PLACEHOLDER_HASH_1
32+
# via
33+
# -r diffusion-in.txt
34+
# accelerate
35+
# diffusers
36+
transformers==4.41.2 \
37+
--hash=sha256:PLACEHOLDER_HASH_1 \
38+
--hash=sha256:PLACEHOLDER_HASH_2
39+
# via -r diffusion-in.txt
40+
41+
# NOTE: This is a placeholder lockfile with representative entries.
42+
# Run scripts/refresh-diffusion-locks.sh to generate the complete
43+
# lockfile with all transitive dependencies and real SHA256 hashes.

files/scripts/diffusion-rocm.lock

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#
2+
# Backend-specific diffusion runtime lockfile: ROCm 6.0
3+
#
4+
# Generated by pip-compile with Python 3.12 (placeholder — regenerate with
5+
# scripts/refresh-diffusion-locks.sh before first use).
6+
#
7+
# pip-compile --allow-unsafe --generate-hashes \
8+
# --extra-index-url https://download.pytorch.org/whl/rocm6.0 \
9+
# --output-file=diffusion-rocm.lock diffusion-in.txt
10+
#
11+
# This lockfile is consumed by the diffusion installer in offline mode:
12+
# pip install --no-index --require-hashes --find-links=<verified-cache> -r diffusion-rocm.lock
13+
#
14+
15+
accelerate==1.13.0 \
16+
--hash=sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0 \
17+
--hash=sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236
18+
# via -r diffusion-in.txt
19+
diffusers==0.28.0 \
20+
--hash=sha256:PLACEHOLDER_HASH_1 \
21+
--hash=sha256:PLACEHOLDER_HASH_2
22+
# via -r diffusion-in.txt
23+
safetensors==0.4.3 \
24+
--hash=sha256:PLACEHOLDER_HASH_1 \
25+
--hash=sha256:PLACEHOLDER_HASH_2
26+
# via
27+
# accelerate
28+
# diffusers
29+
# transformers
30+
torch==2.3.1+rocm6.0 \
31+
--hash=sha256:PLACEHOLDER_HASH_1
32+
# via
33+
# -r diffusion-in.txt
34+
# accelerate
35+
# diffusers
36+
transformers==4.41.2 \
37+
--hash=sha256:PLACEHOLDER_HASH_1 \
38+
--hash=sha256:PLACEHOLDER_HASH_2
39+
# via -r diffusion-in.txt
40+
41+
# NOTE: This is a placeholder lockfile with representative entries.
42+
# Run scripts/refresh-diffusion-locks.sh to generate the complete
43+
# lockfile with all transitive dependencies and real SHA256 hashes.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Diffusion runtime trust anchor.
2+
#
3+
# This manifest defines the per-backend wheel sets needed by the diffusion
4+
# runtime installer (secai-enable-diffusion.sh). It is the sole trust anchor
5+
# for the on-demand acquisition flow.
6+
#
7+
# Two-file trust model per backend:
8+
# 1. Lockfile (diffusion-<backend>.lock) -- pip-compile --generate-hashes
9+
# Used at install time: pip install --no-index --require-hashes
10+
# 2. Wheel manifest (the "wheels" list below)
11+
# Used at download time: exact filename + SHA256 before promotion.
12+
#
13+
# To refresh after a dependency change:
14+
# scripts/refresh-diffusion-locks.sh (regenerates lockfiles + manifest)
15+
16+
schema_version: 1
17+
description: "Diffusion runtime trust anchor — per-backend wheel manifests and lockfiles"
18+
python_version: "3.12"
19+
supported_architectures:
20+
- x86_64
21+
22+
# ---------------------------------------------------------------------------
23+
# Allowed download sources (checked against both initial and final URLs)
24+
# ---------------------------------------------------------------------------
25+
allowed_sources:
26+
- "https://download.pytorch.org/whl/*"
27+
- "https://files.pythonhosted.org/packages/*"
28+
29+
format_policy: wheel_only # reject sdists / tarballs / eggs
30+
31+
# ---------------------------------------------------------------------------
32+
# Backend definitions
33+
# ---------------------------------------------------------------------------
34+
backends:
35+
cpu:
36+
lockfile: diffusion-cpu.lock
37+
torch_index: https://download.pytorch.org/whl/cpu
38+
estimated_size_mb: 2000
39+
wheels:
40+
# -- PyTorch core (CPU) --
41+
- filename: "torch-2.3.1+cpu-cp312-cp312-linux_x86_64.whl"
42+
sha256: "PLACEHOLDER_HASH_torch_cpu"
43+
source: "https://download.pytorch.org/whl/cpu/*"
44+
# -- Diffusers ecosystem --
45+
- filename: "diffusers-0.28.0-py3-none-any.whl"
46+
sha256: "PLACEHOLDER_HASH_diffusers"
47+
source: "https://files.pythonhosted.org/packages/*"
48+
- filename: "transformers-4.41.2-py3-none-any.whl"
49+
sha256: "PLACEHOLDER_HASH_transformers"
50+
source: "https://files.pythonhosted.org/packages/*"
51+
- filename: "safetensors-0.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
52+
sha256: "PLACEHOLDER_HASH_safetensors"
53+
source: "https://files.pythonhosted.org/packages/*"
54+
- filename: "accelerate-1.13.0-py3-none-any.whl"
55+
sha256: "PLACEHOLDER_HASH_accelerate"
56+
source: "https://files.pythonhosted.org/packages/*"
57+
# NOTE: Placeholder entries. Run scripts/refresh-diffusion-locks.sh to
58+
# generate the complete wheel list with real SHA256 hashes.
59+
60+
cuda:
61+
lockfile: diffusion-cuda.lock
62+
torch_index: https://download.pytorch.org/whl/cu121
63+
estimated_size_mb: 4500
64+
wheels:
65+
# -- PyTorch core (CUDA 12.1) --
66+
- filename: "torch-2.3.1+cu121-cp312-cp312-linux_x86_64.whl"
67+
sha256: "PLACEHOLDER_HASH_torch_cuda"
68+
source: "https://download.pytorch.org/whl/cu121/*"
69+
# -- Diffusers ecosystem (same pure-Python wheels as CPU) --
70+
- filename: "diffusers-0.28.0-py3-none-any.whl"
71+
sha256: "PLACEHOLDER_HASH_diffusers"
72+
source: "https://files.pythonhosted.org/packages/*"
73+
- filename: "transformers-4.41.2-py3-none-any.whl"
74+
sha256: "PLACEHOLDER_HASH_transformers"
75+
source: "https://files.pythonhosted.org/packages/*"
76+
- filename: "safetensors-0.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
77+
sha256: "PLACEHOLDER_HASH_safetensors"
78+
source: "https://files.pythonhosted.org/packages/*"
79+
- filename: "accelerate-1.13.0-py3-none-any.whl"
80+
sha256: "PLACEHOLDER_HASH_accelerate"
81+
source: "https://files.pythonhosted.org/packages/*"
82+
# NOTE: Placeholder entries. Run scripts/refresh-diffusion-locks.sh to
83+
# generate the complete wheel list with real SHA256 hashes.
84+
85+
rocm:
86+
lockfile: diffusion-rocm.lock
87+
torch_index: https://download.pytorch.org/whl/rocm6.0
88+
estimated_size_mb: 4000
89+
wheels:
90+
# -- PyTorch core (ROCm 6.0) --
91+
- filename: "torch-2.3.1+rocm6.0-cp312-cp312-linux_x86_64.whl"
92+
sha256: "PLACEHOLDER_HASH_torch_rocm"
93+
source: "https://download.pytorch.org/whl/rocm6.0/*"
94+
# -- Diffusers ecosystem (same pure-Python wheels as CPU) --
95+
- filename: "diffusers-0.28.0-py3-none-any.whl"
96+
sha256: "PLACEHOLDER_HASH_diffusers"
97+
source: "https://files.pythonhosted.org/packages/*"
98+
- filename: "transformers-4.41.2-py3-none-any.whl"
99+
sha256: "PLACEHOLDER_HASH_transformers"
100+
source: "https://files.pythonhosted.org/packages/*"
101+
- filename: "safetensors-0.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
102+
sha256: "PLACEHOLDER_HASH_safetensors"
103+
source: "https://files.pythonhosted.org/packages/*"
104+
- filename: "accelerate-1.13.0-py3-none-any.whl"
105+
sha256: "PLACEHOLDER_HASH_accelerate"
106+
source: "https://files.pythonhosted.org/packages/*"
107+
# NOTE: Placeholder entries. Run scripts/refresh-diffusion-locks.sh to
108+
# generate the complete wheel list with real SHA256 hashes.

0 commit comments

Comments
 (0)