# OMU Engine: library search for app builds (patch pack, NOT deployed)

Source of truth read over ssh: ax42 `/home/claude-agent/forge-monorepo/apps/forge-engine/` (not a git repo there). The local mirror
`~/forge-monorepo` is stale (commit 90ab39a, 5 Sep 2026; its build.py differs): never patch it. Patches below apply with
`patch -p1` to the ax42 files with these md5s: corp/build.py `f97aa421...`, fast/fast_build.py `8b2edfc1...` (checked 18 Oct 2026, dry run clean).

## How a build agent works today
The agent (Claude CLI inside the Engine container, `/app/forge_engine` present, python3 present, outbound HTTPS) is steered by one big
prompt string per lane. 3D today = `forge:3d` GPU job (2-4 min) declared in `.forge/mesh-manifest.json`; after the agent finishes, the
Engine bakes manifests into `public/` (asset_vendor, image_gen, mesh). No library access at all.

## The smallest path (same pattern as asset-manifest)
1. SEARCH: `python3 /app/forge_engine/tools/library_search_cli.py "wardrobe" "street lantern" --kind prop.` (stdlib, BM25, floor, reuse.omu only).
2. DECLARE: `.forge/library-manifest.json` = `[{"id": "...", "path": "generated/library/x.glb"}]`.
3. VENDOR: `pipeline/library_vendor.py::vendor_library_assets(sandbox)` runs next to `vendor_manifest_assets`, downloads
   `blobs/<sha>.glb`, checks the hash, size budget (3 MB each, 12 MB total, 16 entries), reuse.omu, writes `public/generated/library/*.glb`
   and `.forge/library-attributions.json`. Fail-open. The app loads the LOCAL path (house rule: never an https URL).

## Files
| file | where on ax42 | what |
| --- | --- | --- |
| `forge_engine/tools/library_search_cli.py` | NEW `forge_engine/tools/` | search + CLI (first part is a verbatim copy of forge-assets `omg/factory/library_search.py`, a test keeps them equal) |
| `forge_engine/pipeline/library_vendor.py` | NEW `forge_engine/pipeline/` | the vendoring step |
| `patches/build.py.patch` | `forge_engine/pipeline/corp/build.py` | hunk 1 after L2602 (new bullet inside `_AI_CATALOG_FULL`, right after "3D mesh generation"); hunk 2 before L6429 (`from ...image_gen import generate_manifest_images`) = the vendor call |
| `patches/fast_build.py.patch` | `forge_engine/pipeline/fast/fast_build.py` | hunk 1 after L1708 (same bullet, constant 1666-1722); hunk 2 before L7225 (`_reconcile_images`, the vendor call) |
| `tests/test_library_vendor.py` | `tests/` | 6 tests |

## The ONE infra change (gex44, not in this pack): public-to-Engine read-only route
The GCS bucket `forge-assets-library-eu` is private, so blobs are served from the host tree. Add to the `infer.ohmyunicorn.com` vhost of
`/etc/caddy/Caddyfile` (next to `@hy3d`; same internal-hosts-only allowlist, ax42 `5.9.32.176` is in it):
```
@library {
    path /library/*
    remote_ip 46.224.168.31 5.9.32.176 136.243.170.223 127.0.0.1
}
handle @library {
    uri strip_prefix /library
    @libfile path /repo/index.json /blobs/*.glb
    handle @libfile {
        root * /srv/forge-assets
        file_server
    }
    handle {
        respond 404
    }
}
```
Override the base with env `FORGE_LIBRARY_BASE` (default `https://infer.ohmyunicorn.com/library`). Until the route exists the CLI prints
"library unreachable" and the vendor step logs a warning: builds go on as today.

## Deploy + verify (lead)
1. gex44: add the Caddy block, `caddy validate`, reload. From ax42: `curl -s -o /dev/null -w '%{http_code}' https://infer.ohmyunicorn.com/library/repo/index.json` = 200; a blob URL = 200; `/library/repo/cards/...` = 404; from any other IP = 403/404.
2. ax42: copy the 2 new files, `patch -p1 < patches/*.patch` (reconcile host <- live container first, per the Engine rules), `engine-deploy`.
3. `docker exec $(engine-live) python3 /app/forge_engine/tools/library_search_cli.py "wardrobe"` prints the wardrobe lines; `"dragon"` prints GAP.
4. Run `pytest tests/test_library_vendor.py` in the container (the equality test skips when the forge-assets tree is absent).
5. One throwaway tenant build (never a live app): brief "a 3D room viewer with a wardrobe and a floor lamp"; expect `[corp-build] library-manifest vendored ... vendored>=1`, files in `public/generated/library/`, the page loads `/generated/library/*.glb` (200).
Kill switch: delete the prompt bullet (agents then never write a manifest; the vendor step is a no-op without one).
