--- a/forge_engine/pipeline/corp/build.py
+++ b/forge_engine/pipeline/corp/build.py
@@ -2600,6 +2600,7 @@
 - **Photo enhance, looks, logo to SVG, PDF print check** (deterministic, our own CPU service, keeps the user's REAL pixels): `model='forge:craft-v1'` via `openai.images.edit(model='forge:craft-v1', image=<bytes>, prompt='<op>')` (+ optional form field `size` = long edge px). Ops: `enhance` (auto tone + straighten, 1 JPEG); `look:<id>` (1 JPEG, full size); `looks:<id>,<id>,...` (max 12, one small JPEG per id in order: a filter-picker row in ONE call, `size` default 480); `trace:6` (logo/icon/flat art PNG -> 1 SVG; `trace:3`/`trace:16` fewer/more colours); `preflight:150` (upload a PDF <= 25 MB -> 1 JSON report: ppi per image, fonts embedded, boxes). Look ids include lc.warm-glow, lc.golden-hour, lc.bright-airy, lc.soft-pastel, lc.warm-film, lc.faded-matte, lc.cinematic, lc.bw-high-contrast, lc.vivid-pop, lc.teal-orange, lc.moody, lc.neon-night (unknown id -> 400). Reply: `data[i].b64_json` = output bytes; `data[i].revised_prompt` = JSON facts per item (look `id`). The right model for 'enhance/auto-fix my photo', photo filters/presets, 'vectorize my logo', print-ready checks. Not for content changes (`forge:image-edit`) or cutouts (`forge:bg-remove`).
 - **Text-to-speech + the user's OWN cloned voice** (self-hosted, French/English/German/Spanish/Italian/Portuguese): `openai.audio.speech.create(model='forge:tts-v1', voice='house:fr-female'|'house:en-female'|'house:en-male', input=text, response_format='mp3')` server-side (first call after idle can take ~30 s while the GPU model starts: 120 s timeout + a progress state). CLONING is consent-gated and never through the SDK: server-side fetch to base = `LITELLM_BASE_URL` with its trailing `/v1` replaced by `/tts/v1`, header `Authorization: Bearer ${LITELLM_API_KEY}`: (1) `POST {base}/voice/consent/challenge` {user, display_name, language} -> {challenge_id, text}; (2) show `text`, record the user reading it aloud LIVE in the browser (MediaRecorder, 4-30 s, one take; NEVER accept an uploaded file) with two explicit checkboxes (I am 18 or older / this is my own voice and I agree it is copied); (3) `POST {base}/voice/consent/enroll` multipart {challenge_id, user, audio, attest_adult:'true', attest_own_voice:'true', client_ip} -> {voice_id} or 422 {reason, heard} (show the reason, let them record again); store voice_id server-side on that user only; (4) `POST {base}/audio/speech` {input, voice: voice_id, user, response_format:'mp3'}; (5) a 'Delete my voice' button -> `DELETE {base}/voice/voices/{voice_id}?user=`. Label cloned audio 'AI copy of your voice'. Never clone another person's voice or a clip; the service refuses it.
 - **3D mesh generation** (text → GLB): `model='forge:3d'` via `openai.images.generate(model='forge:3d', prompt='...', n=1)`. ~3min background job — NEVER inline; run async with 3-4× retry on 429/5xx. Response: `data[0].url` = hosted .glb; `data[0].preview_url` = ready-made PNG thumbnail (optional, may be absent) — use it for cards/galleries/list views instead of rendering the mesh client-side. Interactive display: embed `<model-viewer src={url} auto-rotate camera-controls></model-viewer>` (Google web component, CDN-loaded) or a minimal Three.js `GLTFLoader` viewer.
+- **Forge 3D library — check it BEFORE generating a mesh for a common object** (furniture, crates and barrels, lamps, benches, trees and rocks, houses and wall/roof modules, cars and bikes, ships): our own approved, phone-sized GLBs, free and instant (a `forge:3d` mesh is a 2-4 minute GPU job). Search: `python3 /app/forge_engine/tools/library_search_cli.py "wardrobe" "street lantern" --kind prop.` (one quoted query per object; `--kind` is an id prefix such as `prop.`, `building.`, `vehicle.`; it prints `id | name | size in metres | triangles | KB | summary`). A line `GAP:` means the library has nothing for that object: generate or build it as usual, never use a lookalike. To use a hit, declare it in `.forge/library-manifest.json` (bare JSON array, 16 entries max): `[{"id": "prop.furniture.wardrobe/store-wardrobe-paxa", "path": "generated/library/wardrobe.glb"}]`; the engine copies the verified file to `public/generated/library/` and writes `.forge/library-attributions.json`; load it with `GLTFLoader` from the LOCAL path `/generated/library/wardrobe.glb` (never an https URL, same lighting rules as any GLB). The file may not exist while you build: load it defensively and keep a primitive stand-in. Scale from the printed metre size. A library hit never replaces the hero mesh rule below, and a game still declares its own concept-sheet props when the library has none.
 - **Displaying a GLB in Three.js — three traps that ship BLACK or washed-out thumbnails.** (1) **No material means METAL.** Meshes from `forge:3d` often carry none, and glTF's default material is metalness 1 — no diffuse response, so under punctual lights alone it renders BLACK; more lights does NOT fix it. ALWAYS add an environment map: `scene.environment = new THREE.PMREMGenerator(gl).fromScene(new RoomEnvironment(), 0.04).texture` from `three/examples/jsm/environments/RoomEnvironment.js` (built-in, no network/HDR asset), and demote every `MeshStandardMaterial` with no `map`/`metalnessMap` to metalness 0.05 / roughness 0.75, mid-grey base colour. TUNE exposure so the mesh lands mid-grey (~170/255), not near-white — blown-out is as invisible on a light card as black is on dark. (2) **Capture from the CANVAS, never a render target.** three applies tone mapping and linear-to-sRGB conversion ONLY on the default framebuffer, so `WebGLRenderTarget`+`readRenderTargetPixels` comes back ~2x too dark. Capture via `gl.render(scene, cam)` then `gl.domElement.toDataURL('image/png')`, with `preserveDrawingBuffer: true`; hide grids/helpers and null the scene background first so the thumbnail carries alpha. (3) **`THREE.Color` cannot parse `oklch()` or `color-mix()`** — warns to console, silently stays WHITE. Every colour handed to three (background, lights, materials, drei `Grid`) MUST be a hex literal, even if the app's palette is oklch.
 - **Image → procedural 3D** (reference image → code-only Three.js): reconstruct an object IN TYPESCRIPT instead of loading a mesh — `three` primitives (Box/Cylinder/Lathe/Extrude/Tube), procedural + canvas-generated materials, instancing. NO `.glb`, no binary asset, no loader, no CDN: diffable source, `three` + `@types/three` auto-provisioned. **Use for**: stylised game props, isometric dioramas, playful/branded hero objects, low-poly scene furniture — anywhere a charming approximation is the goal. **Do NOT use for**: real consumer-product viewers/packshots compared against the actual product (fidelity loss) — use `forge:3d` for a real mesh, or a photograph. Build in passes (silhouette blockout → structure → materials → lighting), never one shot. ⚠️ **SCOPE GUARD — expensive**: a faithful object is a 20-30 min background job costing roughly what a whole app build costs. Do NOT attempt during an autonomous build unless the brief EXPLICITLY asks for a bespoke 3D object; use `forge:image` for routine visuals, `forge:3d` for a solid/printable mesh. Every WebGL mount MUST follow the graceful-degradation rule above.
 - **Parametric CAD** (exact dimensioned parts for 3D printing): `POST https://infer.ohmyunicorn.com/v1/cad/generate` with `{source: '<python defining gen_step(**params) -> build123d Shape>', params: {...}, exports: ['step','stl','3mf']}` (plain httpx/fetch, IP-allowlisted — NOT the LiteLLM gateway, no bearer token). ~1.9s end-to-end — call inline with a spinner, NOT a background job like forge:3d. Response: `{ok, artifacts: {step, stl, "3mf"}, facts: {bbox_mm, volume_mm3, ...}, printability: {ok, manifold, watertight, ...}}` — STEP is AUTHORITATIVE, STL/3MF sidecars; failures return HTTP 200 with `ok:false` and `error{kind,message}`, never assume 200 means success. Author ONE reviewed `gen_step(**params)` generator per part family at build time (never have an LLM write fresh CAD source at request time) — runtime only calls forge:cad with the user's chosen params. ⚠️ **Every dimension the brief states MUST be asserted against `facts` (bbox/volume) before showing the part** — a generator can export a valid watertight STEP and still be dimensionally WRONG, silently. Surface `printability.ok:false` as a failure, never hand over an unprintable file. **Use for**: brackets, enclosures, mounts, jigs, clips, gaskets, hole patterns, parametric configurators — anything mating with real hardware. **Do NOT use for**: organic/decorative/sculptural shapes with no functional dimensions (`forge:3d` instead) or browser-side scenery with no print intent (img2threejs procedural discipline). ⚠️ **MANDATORY SELF-TEST**: POST every generator to `/v1/cad/generate` with representative params, require `ok==true`, `printability.ok==true`, AND `facts.solids==1` — fix and re-run until it passes; ~2s, no excuse to skip. **Do NOT invent build123d API names** — verified surface: `Rectangle(w,h)`, `RectangleRounded(w,h,radius)` (NOT `RoundedRectangle` — doesn't exist), `Circle(r)`, `Ellipse`, `RegularPolygon`, `SlotOverall`, or `Polyline([...], close=True)` + `make_face()`; `extrude(amount=x)` / `extrude(amount=x, both=True)` (NO `symmetric=` kwarg); `Hole(radius)`, `Locations([...])`, `Mode.SUBTRACT`, `fillet`/`chamfer` applied LAST; `part.is_manifold`/`part.is_valid` are PROPERTIES not methods. **One solid, not several** — two `extrude()` calls whose bodies never touch produce disjoint solids that slice as loose pieces; prefer ONE profile extruded once (e.g. an L-shaped `Polyline` for a bracket, not slab+wall), always assert `facts.solids==1`. ⚠️ **EXACT response shape**: `artifacts[fmt]` is an OBJECT `{url, bytes}`, `url` a public CORS-open link — fetch server-side or hand to the browser directly; NOT base64, never `Buffer.from()` it. `facts.bbox_mm` is an ARRAY `[x, y, z]`, NOT `{x, y, z}`. Real response: `{ok: true, artifacts: {stl: {url: 'https://infer.ohmyunicorn.com/v1/cad/artifacts/<uuid>.stl', bytes: 127884}, step: {...}, '3mf': {...}}, facts: {bbox_mm: [80.0, 50.0, 3.0], volume_mm3: 11599.57, center_mm: [0,0,1.5], solids: 1, faces: 12, edges: 24}, printability: {ok: true, manifold: true, watertight: true, winding_consistent: true, euler_number: -6, volume_agreement_pct: 0.0, fits_bed: true, warnings: [], errors: []}, timing_ms: 1966}`. `printability.ok` is the AUTHORITATIVE verdict — gate on it, not a subset of flags. ⭐ **A reviewed feature library ships with the service — `import cad_features as F`** (copied into the sandbox; do NOT re-derive): `F.clearance_hole(size, depth)`, `F.counterbore(size, depth)`, `F.countersink(size, depth)`, `F.hex_nut_blank(size)` (captive-nut POCKET to subtract — robust way to get a thread into a printed part), `F.hex_prism(across_flats, height)`, `F.spur_gear(module_mm, teeth, thickness, bore=)`, `F.knurl(diameter, height, teeth=)`, plus `F.spec('M4')` / `F.clearance_hole_dia('M4','normal')` / `F.tap_drill_dia('M4')` for ISO 273/4762/4032/68-1 data. `size` is a string `'M2'`..`'M12'`. **Every helper returns a STANDALONE solid — subtract with `add(F.counterbore('M4', depth=6), mode=Mode.SUBTRACT)` inside `with Locations((x, y, z)):`; a helper never modifies the part in place.** ⚠️ Use a clearance hole not a nominal one — FDM prints 0.1-0.3mm undersize, so M3 clearance is Ø3.4mm, not Ø3.0mm. ⚠️ **NO thread primitives, do NOT hand-roll one** — a helical sweep cuts turns unreliably (±4% material) in this build123d version; use a clearance hole plus `F.hex_nut_blank` and real hardware. ⭐ **Declare `PARAMS` so the configurator form is GENERATED, not hand-built**: module-level dict beside `gen_step`, e.g. `PARAMS = {'width': {'min': 20, 'max': 200, 'step': 1, 'unit': 'mm', 'group': 'Dimensions', 'label': 'Overall width'}, 'style': {'options': ['square','round']}}`. `POST /v1/cad/manifest {source}` returns `{ok, params: [{name, type, default, widget, label, group, min, max, step, unit, options}], groups, warnings}` — render the form straight from it (`widget`: `slider|number|select|switch|text`); static and free, call on page load. **Bounds are ENFORCED**: `/v1/cad/generate` returns `ok:false, error.kind='bad_params'` for out-of-range/bad-enum/unknown params BEFORE running the generator. Read `warnings` — a `PARAMS` key matching no `gen_step` param, or a default outside its own range, is reported there not silently ignored. ⭐ **Add `'views'` to `exports` and LOOK before calling a generator done**: `exports: ['step','stl','views']` returns `artifacts.views.url`, a 7-view PNG sheet (iso/front/back/left/right/top/bottom), plus `views: {ok, triangles, bbox_mm}`. bbox/volume/manifold/watertight ALL pass on a part missing a briefed feature entirely (measured: an L-bracket missing 2 of 4 required holes passed every numeric check, identical bbox). **A dimensional assertion cannot see a wrong shape; the sheet can.** ⭐ **MULTI-PART ASSEMBLIES use a DIFFERENT endpoint and gate — `POST /v1/cad/assemble`.** A printable part must be ONE watertight solid; an assembly must not be, so ⚠️**`facts.solids==1` does NOT apply to an assembly** — don't force a gearbox into one solid. Define `gen_assembly(**params)` returning a LIST of part dicts instead of `gen_step`: `[{'name': 'base plate', 'part': <build123d Shape, placed in assembly coords>, 'color': '#6f8fbf', 'explode': (0,0,-1), 'envelope': True}, {'name': 'pinion', 'part': ..., 'contacts': ['gear']}]`. `name` required+unique (BOM/clash report/exploded view); `color`/`explode` drive the exploded view; **exactly one part may declare `envelope: True`**, every other part checked to sit inside its footprint; `contacts` declares an EXPECTED touch (meshing gear teeth genuinely share volume) — undeclared overlap is a clash. Request `exports: ['step','views','exploded']`. Response: `{ok, parts: [{name, volume_mm3, bbox_mm, solids, manifold}], assembly: {ok, part_count, interference: {ok, calibration, pairs_tested, clashes: [{a,b,shared_mm3}]}, containment: {ok, violations: [{name, overhangs_mm}]}, unprintable_parts}, artifacts: {step, views}, part_artifacts: [{name, url}]}`. **`assembly.ok` is authoritative** — gate on it. `part_artifacts` gives one STL per part for individual printing. ⚠️**If `assembly.interference.calibration.ok` is false the clash report is INCONCLUSIVE, not clean** — the kernel failed its own overlap self-test, so 'no clashes' means nothing; say so, don't show a green tick. ⭐ **The assembly gate proves parts don't interpenetrate and sit inside the box — NOT that the mechanism works.** Anything the brief implies about how parts MATE is your assertion in the generator, against measured geometry, same as stated dimensions on a single part. Example that passed every automatic check and was still wrong: a meshing gear pair needs centre distance = `module*(z1+z2)/2` AND the two gears PHASED by half an angular pitch (`180/z` degrees) so a tooth sits in a gap — both at phase 0 meet tooth-on-tooth (123 mm³ shared volume) in a render that looks normal. Assert centre distances, running clearances and phase offsets; then LOOK at the exploded view before calling it done.
@@ -6429,6 +6430,15 @@
                     )
             except Exception as _av_exc:  # noqa: BLE001
                 log.warning("[corp-build] asset vendoring skipped (non-fatal): %s", _av_exc)
+            # 2026-10-18: Forge 3D library models the agent chose (library-manifest) are copied in. Fail-open.
+            try:
+                from forge_engine.pipeline.library_vendor import vendor_library_assets as _vla
+                _lv = await _vla(sandbox)
+                if _lv.get("manifest"):
+                    log.info("[corp-build] library-manifest vendored for %s: vendored=%d skipped=%d dropped=%d failed=%d",
+                             project_slug, _lv.get("vendored", 0), _lv.get("skipped", 0), _lv.get("dropped", 0), _lv.get("failed", 0))
+            except Exception as _lv_exc:  # noqa: BLE001
+                log.warning("[corp-build] library vendoring skipped (non-fatal): %s", _lv_exc)
             from forge_engine.pipeline.image_gen import generate_manifest_images
             _img = await generate_manifest_images(sandbox, stage_run_id=stage_run_id)
             if _img.get("manifest"):
