blender
Blender 5.2.2 LTS on fv-ml1 GPU 3, on demand. Agents drive it; there is also a browser desktop to watch it or take over. Prime, 2026-09-27: "go ahead with gpu 3, both". He does not use Blender himself, so the agent side (MCP) is the primary interface.
| Desktop | https://10.251.50.54:3001 (self-signed cert). Basic auth: user blender, password secret get fv-ml1/blender-web-password. |
| Image | lscr.io/linuxserver/blender:5.2.2-ls241@sha256:9216c77a… (Selkies 2.0 Wayland desktop, NVENC stream). |
| GPU | GPU 3 only (NVIDIA_VISIBLE_DEVICES=3). About 270 MiB is held while the desktop runs. |
| Files | /work → /tank/blender (projects, assets, renders; not backed up). /config → /opt/docker/data/blender (prefs, add-ons; restic). Files are owned by infra-ops (uid 1002), so agents can scp in and out. |
⚠ On demand: GPU 3 is borrowed
GPU 3 is the fleet's reserve card for a full-size vLLM seat (servers/fv-ml1/README.md).
Blender uses it only while in use:
ssh infra-ops@10.251.50.54 'cd /opt/docker/compose/blender && docker compose up -d' # start
ssh infra-ops@10.251.50.54 'cd /opt/docker/compose/blender && docker compose down' # stop, card back to 0
restart: "no", so a reboot never brings it back. When a big seat moves onto GPU 3, Blender
stays down.
Headless rendering: scripts/blender-run (for scripts and CLI callers)
A one-shot docker run --rm of this image with Blender as the entrypoint. It needs no desktop and
does not collide with the GUI container's up/down. --job DIR stages a local dir to
/tank/blender/jobs/<name>/ and copies results back. Engines tested headless on 2026-09-28:
Cycles GPU and CPU, EEVEE (EGL), Workbench. STL import is built in. --extensions also
enables the pinned add-on set (STEP import and export among them; see "Extensions" below).
Foot-guns and budget are in docs/fleettools/blender.md. First consumer: draupnir.
Headless rendering inside the running GUI container
ssh infra-ops@10.251.50.54 'docker exec -u abc blender blender -b /work/<file>.blend -E CYCLES -o /work/out/frame_#### -a -- --cycles-device OPTIX'
Run as -u abc, the image's user, mapped to uid 1002, so outputs land owned by infra-ops.
Acceptance (2026-09-27, 1356)
- Cycles sees the card on both OptiX and CUDA: "NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation
Edition". The build ships
kernel_sm_120.cubinplus OptiX PTX. - Self-test (
/tank/blender/render_test.py): a subdivided glass monkey, 1920×1080, 1024 samples, 32 bounces, no denoise. OptiX 3.54 s against CPU 22.71 s (96 threads). That is n=1 per device: a functional check that the GPU is really used, not a benchmark. A trivial default-cube scene could not separate them (0.59 s against 0.65 s), which is why the self-test scene is heavy. - Web auth: no credentials gives 401, a wrong password 401, the right one 200.
- Selkies: "Render node 1 encodes H264, AV1, H265 on nvenc"; the Wayland renderer runs GL on GPU 3.
Agent control (MCP): scripts/blender-mcp
The chosen server is mcp-for-blender (MIT, one
maintainer, ~29k stars; researched by dvalin-smithy-dev 2026-09-27, thread
01M3JA61FTFW2ZPSD20MHF7RPJ, full note in dvalin-smithy
research/blender-agent-drive-2026-09-27.md). Two halves:
- The add-on is inside the running GUI Blender and serves a socket that executes arbitrary
Python with no authentication. It is vendored at upstream commit
41a18432(conf/scripts/addons/blender_mcp.py, MIT licence alongside) and started byconf/scripts/startup/fleet_mcp.py. The add-on only serves from a GUI Blender, never fromblender -b, which is one reason the desktop exists. - The MCP server:
mcp-for-blender==2.1.1, frozen inconf/mcp-requirements.txtand installed in/work/.mcp-venvinside the container (scripts/blender-mcp setup). Agents reach it as stdio overssh infra-ops@fv-ml1 docker exec -i. It runs in the container for two reasons:- No port is published. The socket stays on the container's localhost, so access means ssh + docker on fv-ml1.
- Viewport screenshots need a shared filesystem. Blender writes the image and the server reads it back. With the server on nh3-dev it failed ("Screenshot file was not created").
Settings: DISABLE_TELEMETRY=true and BLENDER_MCP_SAFE_MODE=1. Safe mode puts upstream's AST
allowlist in front of execute_blender_code: bpy, bmesh, mathutils and pure stdlib only, and no
os, open, eval or network. It guards against prompt injection from third-party asset text; it is
not a sandbox.
For an agent
scripts/blender-mcp up # GPU 3 is borrowed: start only when needed
scripts/blender-mcp status # wait for "mcp add-on: answering" (~10-40 s)
claude mcp add blender -- /home/lkraven/development/eshpfi-management/scripts/blender-mcp # PER TASK (Prime 2026-09-27): never user/project-wide
scripts/blender-mcp down # when finished: the card goes back to 0
- Always pass
user_prompt. The tools require it; it is a short statement of the user's request. - Render on the GPU: set
scene.cycles.device = 'GPU'on any scene you create. Safe mode forbids touchingbpy.context.preferences, so the startup hook has already pointed Cycles at OptiX on GPU 3. The startup scene is already set to GPU. - Save everything under
/work/…(=fv-ml1:/tank/blender), thenscpit out. - Look before you report:
get_viewport_screenshotreturns an image, and a still render to/workis the real check. - The asset tools (Poly Haven etc.) reach the internet from Blender. The paid ones (Hyper3D, Hunyuan, Tripo, Sketchfab) need keys we do not have; leave them off.
Acceptance (2026-09-27, 1433)
An MCP client on nh3-dev → scripts/blender-mcp → the in-container server → the add-on:
initializeOK; 36 tools listed.execute_blender_codebuilt a gold metallic torus and rendered it with Cycles on the GPU to/work/_selftest/mcp-torus.png. The file landed, and I checked it by eye.get_viewport_screenshotreturned an image of the scene.- Negative control:
import oswas rejected by safe mode. statuspings the add-on itself. A TCP probe was useless because the port accepted while Blender was still loading.
⚠ Two traps found on the way, both fixed and commented where they live:
- Enabling the add-on from a startup script gets undone when user prefs load. The hook enables it in a timer instead.
- A pre-flight
sshwithout-nswallowed the MCP client'sinitialize, and the session hung at init.
Extensions: the pinned add-on set (2026-09-28)
Why: Prime's ruling of 2026-09-28 makes Blender a mandatory stage in draupnir's pipeline
(requirements → functional shape in build123d → industrial design in Blender → print). draupnir
asked for these add-ons (thread 01M3MQEGGR0N981645WNZ9DMF3). All come from extensions.blender.org,
all are GPL, and all are pinned by version and archive sha256 in extensions.lock.
| Add-on | Version | For | Wheels |
|---|---|---|---|
| SurfacePsycho | 0.10.4 | NURBS/Bezier patch surfacing; STEP/IGES export (the route back to build123d). Alpha. | cadquery-ocp-novtk 7.9.3.1 (cp313) |
| CAD Sketcher | 0.32.1 | Constraint-based precise profiles | slvs 3.2 (cp313) |
| 3D-Print Toolbox | 1.4.1 | Mesh cleanup (clean non-manifold) and checks before a mesh leaves Blender | none |
| STEP Importer (Clonephaze) | 1.2.1 | STEP in, from vendor parts and functional shapes | cascadio 0.0.18rc8 (abi3) |
| Bool Tool | 2.1.0 | Hard-surface booleans | none |
| LoopTools | 4.7.7 | Mesh helpers | none |
| MeasureIt | 1.8.4 | Dimensions drawn in the viewport (they show in screenshots) | none |
| 3MF Import/Export (Clonephaze) | 2.7.7 | 3MF, which carries units (STL does not) | none |
Deliberately skipped (draupnir): ND, HardOps, BoxCutter (modal only, an agent cannot drive them), Quad Remesher (paid; the built-in QuadriFlow covers it), QRemeshify (not in the 5.2 catalogue).
How it is wired
stacks/blender/extensions.lock ──scripts/blender-extensions sync──▶ fv-ml1:/tank/blender-extensions/5.2/system
│ mounted READ-ONLY as Blender's
│ System repo (/blender/5.2/extensions/system)
┌───────────────────────────────────────────┴──────────────────────────┐
GUI container (compose.yaml) blender-run --extensions
conf/scripts/startup/fleet_extensions.py enables all the same file runs as --python ahead of the
of it in a timer after the prefs load caller's args; any failure exits 1 first
- Nobody installs from inside Blender. MCP safe mode blocks
bpy.ops.extensions.*,addon_enableandregister_class, and the repository is mounted read-only, so an agent can neither add an add-on nor alter one. Everything in the System repo is enabled; that directory holds exactly the lock. - Wheels (OCP, slvs, cascadio) are unpacked by Blender into the USER extensions dir, about 60 MB.
In the GUI that is
/config(=/opt/docker/data/blender, restic). In blender-run it is the container's/tmp, rebuilt every run, so concurrent runs share nothing. scripts/blender-extensions syncdownloads each archive (cached in/tank/blender-extensions/zips/), checks its sha256, installs it with Blender's own--command extension install-fileinto a staging dir, pre-warms and byte-compiles it, checks it enables read-only, and only then swaps it in. It refuses while the GUI container or a blender-run job is running, because they hold the old directory through the bind mount.scripts/blender-extensions statuscompares what is installed with the lock.- Bumping a version: edit the lock row, re-run the audit below on the new archive, stop the
GUI (
scripts/blender-mcp down),scripts/blender-extensions sync, run the acceptance probe.
Fleet-local fixes (in fleet_extensions.py, both paths)
- SurfacePsycho
view3d.sp_overwrite_segment_selectionrunseval()on its string property. That walks straight past MCP safe mode: an agent'sbpy.opscall passes the AST check, and the string inside it is never parsed. Nothing in the add-on calls that operator, so the hook swapsevalforast.literal_eval, which keeps its documented use (a literal set of segment ids) and refuses code. Control (2026-09-28): unpatched, the payload[__import__('os').getpid()]ran and returned[1]; patched, it raisedValueError: malformed node; the literal{3, 5}worked both ways. The hook logs a warning if the upstream code changes. - 3MF's "please rate us" popup (after five exports) is switched off.
Phone-home audit (2026-09-28)
- Manifests: none of the eight declares the
networkpermission (five declarefilesonly). - Source: no add-on imports
socket,urllib.request,requests,http,subprocessorwebbrowser(3MF usesurllib.parsefor path joining only).wm.url_openappears only behind buttons a person clicks (CAD Sketcher's help links, 3MF's rating dialog), and safe mode blockswm.url_openin agent code anyway. The GUI hook logsbpy.app.online_access. - Runtime: the full acceptance probe ran with
docker run --network noneunder a Python audit hook watchingsocket.*,urllib.Request,http.client.*,subprocess.Popen,os.system/exec/posix_spawnandwebbrowser.open. Zero events, and every operator passed offline. Positive control: one deliberatesocket.getaddrinfoin the same setup showed up as an event. Sensitivity floor: the hook sees Python-level calls only. The native wheels (OCCT, SolveSpace) could open a socket without Python seeing it, but nothing needed the network to work, and none of them is a networking library.
Foot-guns (all measured 2026-09-28)
blender --addons x,yis not the same as enabling. It leaves the add-on out ofpreferences.addons, and Bool Tool and LoopTools read their own prefs inregister(), so they failed with a KeyError. The hook enables them the way the Preferences button does.- 3D-Print Toolbox writes a cache into its own package dir on first import. On the read-only
mount that fails, which is why
syncpre-warms the staging copy while it is still writable. - CAD Sketcher calls
getpass.getuser(), which fails for a uid with no passwd entry. blender-run setsUSER; the GUI'sabcuser exists. - CAD Sketcher's sketch operators need the GUI. Creating a sketch activates a workspace tool,
and in
blender -bthere is none:'NoneType' object has no attribute 'widget'. The add-on loads headless and its solver (slvs) imports; sketch authoring is a GUI/MCP job. Object.dimensionsis in local axes and ignores rotation, so it cannot show which way an import faces. The probe uses world-space bounds.- STEP Importer orientation: a SurfacePsycho STEP (Z-up, mm) comes back the right way up with
the importer's default
up_axis="Y".up_axis="Z"stands it on its edge (this is the probe's control for its own orientation check). - SurfacePsycho's default patch is 2 x 2 m (Blender units), and STEP export scales ×1000 into millimetres by default. Scale the object to part size first.
- MeasureIt and LoopTools want a 3D View area. In the GUI, override with the window's
VIEW_3D area; headless, a screen datablock's VIEW_3D area works (
view3d_override()in the probe).
Acceptance
The probe is scripts/blender-probes/extensions_acceptance.py:
one real operator run per add-on, written to pass MCP safe mode so the same code runs on both
paths. Files land in fv-ml1:/tank/blender/acceptance/extensions/.
Headless, 2026-09-28: 8 of 9 checks PASS, both online and with --network none: print3d
clean non-manifold (4 → 0 non-manifold edges), Bool Tool auto difference, LoopTools circle
(radius spread 4.14 mm → 0), MeasureIt segment, SurfacePsycho patch → STEP
(sp-patch-headless.step, 20 × 20 mm), STEP Importer read-back (20 × 20 × 0 mm world, plus the
wrong-axis control), 3MF out and back (20 mm cube kept its size). The one FAIL is CAD Sketcher,
for the GUI-only reason above. These runs used the same docker invocation blender-run makes.
Round trip into build123d (draupnir, 2026-09-28): sp-patch-headless.step imported on
irv-ml1 with build123d 0.12.0 as one Shell holding one BSPLINE face, bbox ±10.000000 mm in X/Y at
z = 0, area 399.999982 mm² against 400. Extruded 2 mm, it gave a valid 799.999964 mm³ solid. So a
real NURBS surface reaches the kernel, not triangles, and millimetres hold across the hop. That is
one run of a flat bilinear patch; curved and trimmed patches are untested.
MCP (GUI) path, 2026-09-28 1506 (after Prime ran the deploy): the hook log read
enabled 8/8, hardened: surfacepsycho ... literal_eval, online access: False. Through
scripts/blender-mcp → execute_blender_code under safe mode, the probe passed 9 of 9,
including CAD Sketcher (a sketch on the XY origin plane, then a full solve), because the GUI has
the workspace tool that -b lacks. The eval patch held under MCP: the code payload was refused
(ValueError: malformed node) and the literal {3, 5} was accepted.
- CAD Sketcher sketch, re-solved headless: the MCP-made sketch was saved
(
cad-sketch-mcp.blend), thenblender-run --cpu --extensionsopened it andview3d.slvs_solve(all=True)returned FINISHED. Authoring is in the GUI; re-solving works in batch. - Not solved yet: drawing CAD Sketcher geometry from code.
slvs_add_rectanglewithp1_fallback/p2_fallbackandwait_for_input=Falseraised'NoneType' object has no attribute 'co'. Its drawing ops are stateful and expect point picks, and its Python entity API is out of reach under safe mode. Sketches work; agent-drawn profiles need more work. - Not confirmed: MeasureIt overlays in
get_viewport_screenshot. The segment was created andmeasureit.runopenglreturned FINISHED, but two screenshots (a top view with a 20 mm edge dimension) showed no dimension. blender-run --extensions(the real wrapper,--cpu) ran the same probe at 8 of 9, the one miss being CAD Sketcher for the headless reason above.
Safe-mode note for probe authors: safe mode refuses calling a function held in a variable and
any dunder attribute (type(ex).__name__). The probe calls each test by name and uses repr(ex).