BowerBot

June 19, 2026 Β· View on GitHub

BowerBot

BowerBot

AI agent for OpenUSD.

From empty scene to a production-ready OpenUSD stage.

License Python OpenUSD YouTube Built by Binary Core LLC PRs Welcome


🐦 Meet BowerBot

In the rainforests of Australia and New Guinea lives one of nature's most remarkable architects: the bowerbird.

Instead of relying on appearance, the bowerbird collects, curates, and arranges objects from its environment into a carefully constructed 3D composition. Every object is chosen. Every placement is intentional.

BowerBot brings that same idea to OpenUSD.

BowerBot is an AI agent for OpenUSD, a conversational interface that helps any team using OpenUSD go from an empty scene to a production-ready stage by:

  • finding assets from any connected source (Sketchfab, local disk, company DAM, or any custom provider)
  • placing them with spatial awareness
  • authoring materials inline in ASWF-compliant asset folders
  • setting up native USD lighting (sun, dome, point, area, disk, tube)
  • validating technical correctness (units, hierarchy, references, bindings)
  • packaging the result for any USD-compatible runtime

🎯 What BowerBot Is (and Is Not)

BowerBot is:

  • A USD authoring agent for any OpenUSD pipeline: VFX, AEC, simulation, spatial computing, digital twins, robotics, e-commerce 3D
  • A conversational interface for the full authoring surface: assets, lighting, materials, validation, packaging
  • A fast way to go from 0 β†’ production-ready scene
  • A pipeline assistant that handles technical correctness (units, hierarchy, references, bindings)
  • A pipeline guardian that catches asset issues before they reach production
  • Extensible by design: new asset sources, DCCs, and domains plug in as skills

BowerBot is NOT:

  • A final scene generator
  • A replacement for DCC tools like Maya or Omniverse
  • A system that produces perfect composition or artistic layouts

Scenes generated by BowerBot are meant to be opened, reviewed, and refined in your DCC.

Think of it as:

"Block out the scene instantly, then refine like a pro."

Pipeline Quality Built In

BowerBot enforces ASWF USD standards at every step, not just placing assets. Fixable mismatches (non-canonical folder names, external dependencies) are auto-normalized on intake so the project copy is always self-contained. Production-required invariants are validated at intake too: assets with non-identity root transforms (Maya pivot dance, unfrozen DCC exports) are refused with a clear message and the option to bake transforms into vertex data on the project copy without touching the source. Unfixable violations (wrong root prim type, missing defaultPrim, incorrect metersPerUnit, circular references, missing dependencies) are caught at assembly time with a clear message about what's wrong and how to fix it.

"The cheapest bug to fix is the one you catch before it enters the pipeline."


✨ What It Does

Watch BowerBot build real scenes end-to-end on the demo playlist on YouTube. Each video walks through a project across asset discovery, placement, materials, lighting, validation, and packaging.

Open the resulting .usda in Maya, usdview, Omniverse, Isaac Sim, or any USD-compatible tool to refine composition, lighting, materials, or downstream pipeline steps.

Projects are persistent. Close the session, come back later, and continue where you left off.


✨ Features

  • πŸ“¦ OpenUSD native: references, defaultPrim, metersPerUnit, upAxis, all correct out of the box. BowerBot authors a single scene.usda as the live working layer; save_scene_snapshot(name) writes a flattened, DCC-stripped <name>.usda alongside whenever you want to publish a frozen version
  • 🎭 USD variant sets: asset-level (material, geometry/LOD, configuration, attribute) live in the asset's variants.usda; scene-level (lighting moods, light-type swap, model selection at a placement) live inline in scene.usda. Architectural invariants protect every mutation: auto-promote existing references into a model-selection variant on first add, auto-demote back to a direct ref when the set is removed, cascading orphan-opinion cleanup on prim delete/rename, automatic texture-asset staging for Asset-typed attribute values, and suspect-set detection that flags variants that collapse to a single choice
  • πŸ—οΈ ASWF-compliant asset folders: geometry, materials, and lighting split into a root + layer files, per the USD Working Group guidelines. Heavy geo.usda composes via a payload arc for lazy-load (city-scale digital twins, robot fleets, large layouts open instantly); mtl.usda / lgt.usda / contents.usda use references
  • 🧳 Self-contained intake: non-canonical source folders are detected via USD composition, canonicalized (root.usd β†’ <folder>.usda), and external dependencies (textures, sublayers) are localized into the asset folder so the project copy is always portable
  • 🎨 Material binding: apply MaterialX or existing .usda materials to specific mesh parts; procedural materials author hybrid MaterialX + UsdPreviewSurface outputs so they render across studio renderers (Renderman, Arnold), Hydra Storm, Apple RealityKit / AR Quick Look, and Isaac Sim
  • πŸ’‘ Native USD lighting: sun, dome, point, area, disk, and tube lights at scene or asset level, with optional UsdLux light:link collections so a rim light, kicker, or product-shot key only illuminates the prims you target
  • 🧩 Automatic unit handling: assets in cm, mm, or inches are scaled correctly at reference time
  • πŸ“ Geometry-aware placement: bounding-box resolved positions for surface, above, below, or nested placements
  • πŸ”Œ Pluggable skills: connect any asset source (Sketchfab, PolyHaven, company DAM, or build your own)
  • 🧠 Multi-LLM support: OpenAI, Anthropic, and any provider via litellm
  • πŸ“ Project-based workflow: one folder per scene, resumable across sessions
  • βœ… Scene validation: defaultPrim, units, up-axis, reference resolution, and material binding checks plus USD's modern UsdValidation framework (the same validators behind usdchecker) run automatically on intake and on validate_scene
  • πŸ“¦ USDZ packaging: standard USDZ for Omniverse, Isaac Sim, Unreal, Unity, web viewers, and any USD consumer; opt-in Apple AR Quick Look strict-subset pre-validation when shipping to iOS Files / Safari / iMessage / macOS Quick Look / Vision Pro
  • πŸ—οΈ Onboarding wizard: zero-config setup in 60 seconds

Built on OpenUSD, the ASWF USD Working Group standards, and the Alliance for OpenUSD (AOUSD) core spec driven by Pixar, Apple, NVIDIA, and others.


πŸš€ Quick Start

Install

There are two paths for end users (pick whichever fits your environment), plus a separate path for contributors who want to modify BowerBot itself.

uv manages Python and isolated tool environments for you, so you do not need to install or pin Python yourself.

uv tool install bowerbot

End users, Option B: pip

If you already maintain a Python 3.12+ environment, plain pip works:

pip install bowerbot

Contributors: developer install

To modify BowerBot itself, clone the repo and let uv manage the dev environment:

git clone https://github.com/binary-core-llc/bowerbot.git
cd bowerbot
uv sync
uv run bowerbot onboard

First-time setup

bowerbot onboard

The wizard first asks how you'll run BowerBot (agent or MCP mode, and in MCP mode which transport, see below), then your asset library directory and projects directory, and (in agent mode) your LLM API key. It writes ~/.bowerbot/config.json. One file, one place, no .env.

Create a project and start building

bowerbot new "Coffee Shop"
bowerbot open coffee_shop

To plug in asset providers like Sketchfab, see Skills below.


πŸ”€ Two ways to run: agent mode and MCP mode

BowerBot is one program with a mode switch. You run it one way or the other, never both. The switch is a single line in ~/.bowerbot/config.json (the onboard wizard sets it):

{ "mode": "agent" }   // default. BowerBot uses its own AI. Needs an LLM API key.
{ "mode": "mcp" }     // BowerBot has no AI; an MCP client drives it. No API key.
  • Agent mode: BowerBot thinks for itself using the LLM key. Good for running standalone or as a product: bowerbot open coffee_shop.
  • MCP mode: BowerBot has no brain of its own. An MCP client (Claude Desktop, Cursor, VS Code, Claude Code, ...) is the brain; BowerBot is purely a tool provider. No API key needed.

Agent mode

BowerBot drives itself with its own LLM. Set the model and API key in ~/.bowerbot/config.json:

{
  "mode": "agent",
  "llm": { "model": "anthropic/claude-sonnet-4-6", "api_key": "sk-..." }
}

Then work in natural language:

bowerbot new "Coffee Shop"     # create a project
bowerbot open coffee_shop      # open it and start an interactive session
bowerbot build "a reading nook with a chair, lamp, and bookshelf"

MCP mode

In MCP mode BowerBot has no LLM of its own. An MCP client is the brain and BowerBot is the tool provider, exposing its full tool surface (projects, scene building, lighting, materials, physics, variants, validation, packaging) plus every installed skill (Sketchfab, Kit, ...). The client opens or creates projects through the project tools (create_project, open_project, list_projects). No LLM API key is read; skills still use their own config (the Sketchfab token, the Kit base_url) from the same config.json.

MCP mode speaks one of two transports, chosen by mcp.transport:

{ "mcp": { "transport": "stdio" } }  // the client launches BowerBot as a subprocess
{ "mcp": { "transport": "http" } }   // BowerBot runs as a local server; clients connect by URL

Pick stdio for clients that launch local servers themselves (Claude Desktop). Pick http for clients that connect to a server by URL (Cursor, VS Code, Claude Code, Windsurf, ...). The tool surface is identical either way.

stdio transport

The client spawns BowerBot and talks over stdin/stdout. No port, no server to keep running, no bridge. Set the transport:

{
  "mode": "mcp",
  "mcp": { "transport": "stdio" }
}

Then point your client's MCP config at the bowerbot command:

{
  "mcpServers": {
    "bowerbot": { "command": "bowerbot" }
  }
}

Use the full path to the executable if bowerbot is not on the client's PATH (e.g. C:/Users/you/Desktop/bowerbot/.venv/Scripts/bowerbot.exe). The client starts and stops the server for you.

http transport

BowerBot runs as a local server your client connects to by URL. Set the transport and, optionally, the host/port/path:

{
  "mode": "mcp",
  "mcp": { "transport": "http", "host": "127.0.0.1", "port": 8181, "path": "/mcp" }
}

The host/port/path default to 127.0.0.1:8181/mcp. Start the server and leave it running:

bowerbot

It serves at http://<host>:<port><path> (e.g. http://127.0.0.1:8181/mcp). For safety it only accepts requests whose Host/Origin match that local address (DNS-rebinding protection). Connect your client to the URL, most support a local HTTP MCP server natively:

ClientHow to add it
Claude Code (CLI)claude mcp add --transport http bowerbot http://127.0.0.1:8181/mcp
Codex (CLI)codex mcp add bowerbot --url http://127.0.0.1:8181/mcp (or a [mcp_servers.bowerbot] url = "..." entry in ~/.codex/config.toml)
Cursormcp.json: { "mcpServers": { "bowerbot": { "url": "http://127.0.0.1:8181/mcp" } } }
VS Code (Copilot agent)mcp.json: { "servers": { "bowerbot": { "type": "http", "url": "http://127.0.0.1:8181/mcp" } } }
Windsurf{ "mcpServers": { "bowerbot": { "serverUrl": "http://127.0.0.1:8181/mcp" } } }
Cline{ "mcpServers": { "bowerbot": { "type": "streamableHttp", "url": "http://127.0.0.1:8181/mcp" } } }
Zedsettings.json: { "context_servers": { "bowerbot": { "url": "http://127.0.0.1:8181/mcp" } } }

Claude Desktop is the exception: its config only spawns stdio servers, so use the stdio transport above, or bridge the HTTP server with npx mcp-remote http://127.0.0.1:8181/mcp.


πŸ“Ί Tutorials

New to BowerBot? Watch the tutorial playlist on YouTube for setup walkthroughs, scene building demos, and tips for working with USD pipelines.


🩹 Troubleshooting

Stuck on something? See docs/TROUBLESHOOTING.md for common issues: working alongside a DCC, skill installation, CLI rendering on Windows, and LLM tool-calling pitfalls.


πŸ› οΈ CLI Commands

CommandDescription
bowerbot new "name"Create a new project
bowerbot open nameOpen a project and start chatting
bowerbot listShow all projects
bowerbot chatAuto-detect project in current directory
bowerbot build "prompt"Single-shot build (auto-creates project)
bowerbot skillsList scene builder tools and enabled skills
bowerbot infoShow current configuration
bowerbot onboardFirst-time setup wizard

πŸ“ Projects

Each project is a self-contained folder with metadata, scene, assets, and packaged output in one place:

scenes/coffee_shop/
  project.json    # Metadata: name, created_at, updated_at, scene_file
  scene.usda      # The USD stage (references only, clean and readable)
  scene.usdz      # Packaged output (Apple Vision Pro, Omniverse, etc.)
  assets/         # ASWF folders + self-contained USDZs used by this scene
  textures/       # Scene-level textures (HDRI maps for DomeLights, etc.)

Projects are resumable. Close the session, come back later, and continue where you left off:

$ bowerbot open coffee_shop
# Project: Coffee Shop
# Scene: scene.usda (5 object(s))

You: Show me the scene structure
BowerBot: Scene has 5 objects...

You: Remove Table_03
BowerBot: Removed /Scene/Furniture/Table_03

πŸ”„ How It Works

BowerBot is conversational: you tell it what you want and it uses the right tools to build your scene. Behind the scenes, it manages asset discovery, USD composition, materials, lighting, and more.

Asset Discovery

BowerBot searches for assets across all connected sources, prioritizing what's already available:

  1. Local assets first: BowerBot checks your local asset directory (assets_dir in config.json) for USD files (.usd, .usda, .usdc, .usdz). This includes anything you've exported from Maya, Houdini, Blender, or any DCC tool, as well as assets previously downloaded from cloud providers.

  2. Cloud providers if needed: If the asset isn't found locally, BowerBot searches connected providers (any installed skill, e.g. Sketchfab) and downloads the asset to your local directory.

  3. All downloads are cached locally: Once an asset is downloaded from any source, it lives in your assets_dir and is available for all future projects without re-downloading.

Scene Assembly

When you ask BowerBot to place an asset, it routes by what the source looks like and always produces a self-contained ASWF folder in the project:

  • Folder with a detectable root (canonical wall/wall.usda, or non-canonical wall/root.usd + wall/geo.usd + wall/mtl.usd): the root is identified via USD composition (the file no sibling depends on), the folder is copied into the project, the root is canonicalized to <folder>.usda, sibling references are rewritten, and any externally-referenced textures or layers are localized into the folder so the output is portable.
  • Loose USD geometry (.usd, .usda, .usdc from your DCC exports): wrapped in a fresh ASWF folder named after the file stem, producing <stem>/<stem>.usda + geo.usda.
  • USDZ files (from Sketchfab, DAMs, etc.): placed as-is since they're already self-contained.

When an asset can't be safely intaken (missing external dependencies, or a folder with multiple independent USDs and no clear root), BowerBot refuses with a message naming the conflict instead of guessing.

Material Workflow

When you apply materials to an asset, BowerBot writes them into the asset folder's mtl.usda, not the scene file. The scene stays clean with only references:

You: Apply wood material to the table top
BowerBot: [searches local assets for "wood" materials]
         [discovers mesh parts: table top, legs, frame]
         [writes material definition + binding into assets/table/mtl.usda]
         Bound /table/mtl/wood_varnished to table top

The result is a production-ready asset folder:

assets/single_table/
  single_table.usda   <- root (references geo + mtl)
  geo.usda            <- geometry (untouched from source)
  mtl.usda            <- materials inline + bindings

Scene Output

The scene file (scene.usda) contains only references and lights: no material data, no geometry copies, no sublayers. Clean and readable:

def Xform "Scene" (kind = "assembly") {
    def Xform "Furniture" {
        def Xform "Table_01" {
            xformOp:translate = (5, 0, 4)
            xformOp:scale = (0.01, 0.01, 0.01)
            def Xform "asset" (
                references = @assets/single_table/single_table.usda@
            ) { }
        }
    }
    def Xform "Lighting" {
        def DistantLight "Sun_01" { ... }
        def DomeLight "Environment_01" { ... }
    }
}

Open it in Maya, usdview, Omniverse, Isaac Sim, or any USD-compatible tool to refine.

Batch Placement (layout files)

place_layout places many assets in one call. Small or parametric layouts are passed inline; bulk layouts (a scene extracted by a script, or a DCC export) are passed as a layout file, a JSON contract any exporter can target:

{
  "version": 1,
  "placements": [
    { "asset": "SM_floor02/SM_floor02.usda", "group": "Building/Floor",
      "pattern": { "type": "grid", "origin": [0, 0, 0],
                   "count": [6, 5], "spacing": [6, 6] } },
    { "asset": "forklift/forklift.usda", "group": "Props",
      "transforms": [ { "translate": [4.2, 0, 1.5], "rotate": [0, 90, 0] } ] }
  ]
}
  • version is required; this BowerBot reads version 1.
  • Each entry names one asset (the asset's root file) and one group, plus either an enumerated transforms list or a parametric pattern (grid: origin, count [nx, ny]/[nx, ny, nz], spacing; linear: origin, integer count, spacing direction step). Optional per-entry name, rotate, and scale (a uniform number or [sx, sy, sz]) act as defaults for placements that do not set their own.
  • Relative asset paths resolve in order: layout-file dir β†’ project dir β†’ library dir. There is no working-directory fallback.
  • Translates are in scene units, and pattern axes map to world [x, y, z] (not up-axis aware β€” the example above is for a Z-up scene). Each asset is conformed (units + up-axis) on reference, same as place_asset.
  • A layout expands to at most 100,000 placements per call.
  • The whole file is validated before anything is placed; every invalid entry and unresolvable asset is reported at once with entry indices. To lint a file without placing anything, call place_layout with validate_only=true β€” it checks shape and asset resolution against the live rules. The normative contract is the pydantic models in src/bowerbot/schemas/layout.py.

πŸ”Œ Skills

Skills extend BowerBot with external asset providers, DCC connectors, and simulation runtimes. Each skill is a separate Python package, discovered at runtime through Python entry points (bowerbot.skills). The skill SDK lives in bowerbot.skills; skills themselves ship and version on their own. See Installing a skill below for the walkthrough.

Scene Builder Tools

BowerBot's core tools, grouped by domain. Every tool maps 1:1 to a service function and is described in the LLM prompts under src/bowerbot/prompts/.

Projects

ToolDescription
create_projectCreate a new project and focus it
open_projectFocus an existing project (resume or switch)
list_projectsList every project, flagging the focused one
get_current_projectReport the focused project, path, and object count

Stage & scene

ToolDescription
create_stageInitialize a new USD scene with standard hierarchy
list_sceneShow current scene with positions and bounding boxes
list_prim_childrenDiscover mesh parts inside a referenced asset
list_prim_attributesEnumerate every attribute on a prim with type, value, authored flag
set_prim_attributeAuthor or clear an attribute opinion (per-instance overrides; value=null clears)
move_assetReposition an existing object without creating duplicates
rename_primMove/rename objects in the hierarchy (cascades into variant bodies)
remove_primDelete objects from the scene (cascades orphan-opinion cleanup)
compute_grid_layoutCalculate evenly spaced positions
save_scene_snapshot / list_scene_snapshots / delete_scene_snapshotFlatten the live scene.usda into a frozen <name>.usda snapshot you can publish

Assets

ToolDescription
place_assetAdd an asset (auto-creates ASWF folder for loose geometry)
place_layoutBatch placement: many assets/transforms in one call, inline or from a layout JSON file
place_asset_insideNest an asset inside an ASWF container's contents.usda
list_project_assetsShow asset folders with scene usage status
delete_project_assetRemove an asset folder (scans variant bodies in every layer first)
delete_project_textureRemove a texture file (checks references first)
cleanup_unused_contentsPrune nested asset wrappers whose target folder no longer exists
freeze_assetBake non-identity root transforms (Maya/Houdini unfrozen exports) into vertex data

Library

ToolDescription
search_assetsFind USD assets in the user's library by keyword (geo, mtl, package)
list_assetsList every USD asset in the user's library, classified by category
search_texturesFind HDRIs and material maps in the asset library by keyword
list_texturesList every HDRI and material map in the asset library

Lighting

ToolDescription
list_light_type_propertiesLive UsdLux schema view for a light type (call before create_light to discover supported inputs:*)
create_lightAdd a native USD light (sun, dome, point, area, disk, tube) at scene or asset level
update_lightModify an existing light's xform / HDRI texture
remove_lightDelete a light from the scene or asset

Cameras

ToolDescription
list_camera_propertiesLive UsdGeom schema view of the Camera attributes (call before create_camera)
create_cameraAdd a scene camera, aimed via a look_at point or explicit rotation
update_cameraReposition or re-aim an existing camera
remove_cameraDelete a camera from the scene

Materials

ToolDescription
create_materialAuthor a procedural MaterialX material and bind it to a prim
bind_materialApply a material to a specific mesh part (writes into asset mtl.usda)
remove_materialClear material binding from a prim
list_materialsShow all materials and their bindings
cleanup_unused_materialsPrune material definitions no prim binds to

Physics

ToolDescription
list_physics_api_propertiesLive UsdPhysics schema view for an applied API (call before apply_physics_api)
apply_physics_apiApply a UsdPhysics API (RigidBody, Mass, Collision, MeshCollision, ArticulationRoot) to a prim
remove_physics_apiRemove a UsdPhysics API and any dependent APIs
setup_physics_sceneCreate /Scene/Physics and a UsdPhysics.Scene with gravity
get_physics_summaryReturn asset-side + scene-side physics opinions for a prim
list_joint_propertiesSchema view for a UsdPhysics typed joint
create_joint / remove_joint / list_jointsAuthor / remove / list typed joints (Revolute, Prismatic, Spherical, Fixed, Distance) at scene or asset scope
create_or_update_collision_group / remove_collision_group / list_collision_groupsManage UsdPhysicsCollisionGroup membership and filter relationships

Variants

Asset-level variants live in <asset>/variants.usda; scene-level variants live inline in scene.usda on a carrier prim. Tool names carry an asset_ or scene_ prefix so the LLM never has to guess.

ToolDescription
add_asset_material_variantBind a different material set per variant
add_asset_geometry_variant / setup_asset_geometry_variants / list_asset_geo_filesSwap geometry / LOD payloads per variant
add_asset_attribute_variantOverride any prim attribute per variant (stages texture assets automatically)
add_asset_configuration_variantActivate / deactivate prims per variant
add_scene_lighting_attribute_variant / add_scene_lighting_selection_variantLighting mood swaps + light-type swaps on /Scene/Lighting
add_scene_model_selection_variantSwap which asset reference loads at a placement (auto-promotes the existing ref into a variant body on first call)
select_asset_variant / select_asset_variant_for_instance / select_scene_variantChoose the active variant
remove_asset_variant / remove_asset_variant_set / remove_scene_variant / remove_scene_variant_setDelete a variant or whole set (cascades orphan cleanup, surfaces suspect sets)
list_variantsShow every variant set with carrier path, selections, and authoring layer

Validation & packaging

ToolDescription
validate_sceneCheck for USD errors (USD's UsdValidation framework + BowerBot's invariants)
package_sceneBundle as .usdz (with optional Apple AR Quick Look strict-subset pre-validation)

Extension Skills

First-party skills

Maintained by Binary Core LLC alongside the BowerBot core.

SkillInstallWhat it does
bowerbot-skill-sketchfabpip install bowerbot-skill-sketchfabSearches and downloads models from your own Sketchfab account in USDZ format.

Community skills

Built by external contributors, published to PyPI under each author's namespace, and listed here for discoverability. To add yours, open a PR on this README adding a row to the table below. The skill must be open source, installable via pip from public PyPI, and follow the contract in CONTRIBUTING.md.

SkillAuthorInstallWhat it does
be the first

When this list grows large enough to warrant tooling, it becomes the BowerHub skill registry.

Installing a skill

Three steps. Sketchfab as the worked example.

1. Install the skill alongside BowerBot. With uv, add it to the same tool environment:

uv tool install bowerbot --with bowerbot-skill-sketchfab

To add more skills later, rerun with every --with you want and --reinstall:

uv tool install bowerbot --with bowerbot-skill-sketchfab --with bowerbot-skill-polyhaven --reinstall

If you used plain pip to install BowerBot, install the skill in the same Python environment:

pip install bowerbot-skill-sketchfab

2. Get any credentials the skill needs. Sketchfab requires an API token from https://sketchfab.com/settings/password. Each skill's README documents what credentials (if any) it needs.

3. Add the skill's config block to ~/.bowerbot/config.json:

"skills": {
  "sketchfab": {
    "enabled": true,
    "config": { "token": "your-sketchfab-token" }
  }
}

That's it. BowerBot auto-discovers the skill via Python entry points the next time you run it. The exact shape of config is per-skill; consult the skill's README.

Verifying a skill is installed

Three commands, in increasing depth. All work on Windows, macOS, and Linux.

1. Ask BowerBot what it sees:

bowerbot skills

Lists the core scene-builder tools plus every extension skill the registry has loaded successfully. If your skill shows under "Extension skills" with its tools, you are done.

2. If it does not appear, check the package is installed:

pip show bowerbot-skill-sketchfab

If the package is installed, this prints its name, version, and location. If not, it prints Package(s) not found and exits non-zero. Install it (see Installing a skill above). Replace bowerbot-skill-sketchfab with whichever skill you are checking.

3. If the package is installed but BowerBot still does not see it, inspect the entry-point registration directly:

python -c "from importlib.metadata import entry_points; print('\n'.join(f'{ep.name} -> {ep.value}' for ep in entry_points(group='bowerbot.skills')))"

If your skill does not appear in this output despite being pip-installed, the skill's pyproject.toml is missing or broken. File an issue on the skill's repo. If the skill does appear here but bowerbot skills still does not show it, the gap is in your ~/.bowerbot/config.json: the skill's block is missing, enabled: false, or the credentials fail validate_config().

Private and in-house skills

Skills do not have to be public. Install from a private PyPI index, a git URL, or a local path:

# Private PyPI
pip install bowerbot-skill-acme --index-url https://pypi.acme.internal/

# Direct git URL (any host)
pip install git+ssh://git@github.com/acme/bowerbot-skill-acme.git

# Local path during development
pip install -e /path/to/skill

Entry-point discovery works the same in all three cases.

Trust

A skill's SKILL.md is injected into the LLM's system prompt, and its tools run with the same access as core tools. Only install skills you trust. Open-source skills are auditable; closed-source skills should come from a vendor you have a relationship with. The first-party table above is the only set Binary Core has audited end-to-end.


βš™οΈ Configuration

All settings live in one file: ~/.bowerbot/config.json. BowerBot runs in one mode at a time:

  • agent mode uses the llm block
  • MCP mode uses the mcp block

The skills, assets_dir, and projects_dir keys apply to both modes. The skills block configures any skill packages you've installed (the example shows bowerbot-skill-sketchfab, see Skills); a fresh install starts with "skills": {}. A scene's up-axis and units are not set here, you choose them per project at creation (see MCP mode and bowerbot new).

Agent mode (BowerBot uses its own LLM):

{
  "mode": "agent",
  "llm": {
    "model": "anthropic/claude-sonnet-4-6",
    "api_key": "sk-...",
    "temperature": 0.1,
    "max_tokens": 4096,
    "context_window": null,
    "summarization_threshold": 0.75,
    "num_retries": 3,
    "request_timeout": 120.0,
    "max_tool_rounds": 25
  },
  "skills": {
    "sketchfab": {
      "enabled": true,
      "config": { "token": "your-sketchfab-token" }
    }
  },
  "assets_dir": "./assets",
  "projects_dir": "./scenes"
}

MCP mode (an MCP client drives BowerBot, no LLM key):

{
  "mode": "mcp",
  "mcp": {
    "transport": "stdio"
  },
  "skills": {
    "sketchfab": {
      "enabled": true,
      "config": { "token": "your-sketchfab-token" }
    }
  },
  "assets_dir": "./assets",
  "projects_dir": "./scenes"
}

For the http transport, use "mcp": { "transport": "http", "host": "127.0.0.1", "port": 8181, "path": "/mcp" } (see MCP mode).

Switch models by changing one line:

{ "model": "anthropic/claude-opus-4-7" }
{ "model": "anthropic/claude-sonnet-4-6" }
{ "model": "gpt-5" }
{ "model": "deepseek/deepseek-chat" }

Tested Models

BowerBot leans hard on tool calling, multi-step orchestration, and the SKILL.md instructions injected into the system prompt. The model table below ranks models by how well they hold up under that load.

ModelTool CallingInstruction FollowingRecommended
anthropic/claude-opus-4-7ExcellentExcellentYes (best overall)
anthropic/claude-sonnet-4-6ExcellentExcellentYes (default β€” best value)
anthropic/claude-haiku-4-5-20251001GoodGoodYes (budget / fast)
gpt-5ExcellentExcellentYes
gpt-5-miniGoodGoodYes (budget)
gpt-4.1GoodGoodWorks (legacy β€” newer models give a better experience)
gpt-4.1-miniFairFairWorks (legacy budget)
gpt-4oPoorPoorNo (skips tool calls, ignores SKILL.md)

Claude Opus 4.7 and Sonnet 4.6 give the most reliable experience today, especially on long sessions (physics + variants + materials in the same project) where consistent multi-round tool calling matters most. GPT-4.1 still works for simple flows but is no longer the recommended default β€” pick a current-generation model when you can.

Token Management

BowerBot automatically manages conversation context to stay within model limits. Two settings control this:

SettingDefaultDescription
context_windownullContext window size in tokens. null = auto-detect from the model.
summarization_threshold0.75Fraction of context budget that triggers history summarization.

Additional tuning options (usually don't need changing):

SettingDefaultDescription
tool_result_age_threshold2User turns before old tool results are compressed.
min_keep_recent6Minimum recent messages always kept verbatim.
summary_max_tokens512Max tokens for the summarization LLM call.

Tool-Calling Loop

BowerBot runs a loop where the LLM requests tool calls, BowerBot executes them, and the results are fed back. Complex requests (e.g. binding materials to many mesh parts at once) can require many rounds.

SettingDefaultDescription
max_tool_rounds25Maximum LLM ↔ tool exchange rounds per request. Increase if BowerBot stops with "Reached maximum tool-calling rounds" on legitimate workflows.

Error Recovery

BowerBot automatically handles transient API errors:

SettingDefaultDescription
num_retries3Retries for rate limits and transient errors (429, 500, 503).
request_timeout120.0Seconds before a request times out.
  • Rate limits and transient errors are retried automatically with exponential backoff.
  • Validation errors are fed back to the LLM so it can auto-fix issues and re-validate.
  • Permanent errors (bad API key, unknown model) show a clear message without crashing.

πŸ—οΈ Architecture

BowerBot is organized FastAPI-style:

  • schemas/ describe data (pydantic models + enums)
  • utils/ are pure-function primitives (no SceneState, no orchestration)
  • services/ are state-aware orchestrators, one function per tool, signature (state, params), calls utils and other services freely, raises on errors
  • tools/ are the LLM-facing surface, thin adapters that guard preconditions, call ONE service, wrap the result in ToolResult

Adding a feature is the same three-file change every time: schema, service, tool.

src/bowerbot/
  agent.py            # Agent mode: the LLM tool-calling loop and prompt assembly
  mcp_server.py       # MCP mode: serves the tool surface to an MCP client over stdio or HTTP
  tool_router.py      # Shared router over core tools + skills (used by both modes)
  cli.py              # Click CLI; dispatches to agent runtime or MCP server by mode
  config.py           # Settings (incl. mode: agent|mcp) from ~/.bowerbot/config.json
  project.py          # Project lifecycle (create / load / resume)
  state.py            # SceneState: the context threaded through every tool handler
  dispatcher.py       # Aggregates core tool defs + routes core tool calls to handlers
  token_manager.py    # Conversation compression and summarization (agent mode)

  prompts/            # LLM instructions as markdown (editable without code changes)
    core.md
    projects.md
    scene_building.md
    assets.md
    library.md
    lights.md
    cameras.md
    materials.md
    physics.md
    textures.md
    variants.md

  schemas/            # Pydantic models and enums, grouped by domain
    assets.py         #   Asset formats, categories, ASWF layer names, metadata
    cameras.py        #   CameraParams, CameraPropertySpec, CameraSchemaInfo
    intake.py         #   DetectionOutcome, FolderDetection, IntakeReport
    layout.py         #   LayoutEntry, GridPattern/LinearPattern, LayoutTransform
    lights.py         #   LightType, LightParams, LightPropertySpec, LightTypeSchemaInfo
    materials.py      #   MaterialXShaders, ProceduralMaterialParams
    physics.py        #   PhysicsApiName, PhysicsJointType, PhysicsPropertySpec,
                      #   PhysicsApiSchemaInfo, joint/collision-group summaries
    scene.py          #   SceneNamespace (canonical /Scene/* layout)
    textures.py       #   HDRI / image / texture-category enums
    transforms.py     #   TransformParams, PositionMode, SceneObject
    validation.py     #   Severity, ValidationIssue, ValidationResult
    variants.py       #   VariantCategory, AddVariant params, VariantsSummary

  services/           # State-aware orchestrators. One same-named function per tool.
    project_service.py     #   create_project, open_project, list_projects,
                           #   get_current_project (focus the bound project)
    stage_service.py       #   create_stage, list_scene, rename/remove_prim, move_asset,
                           #   set/list_prim_attribute(s), snapshot lifecycle, ...
    asset_service.py       #   place_asset, place_asset_inside, list/delete_project_*,
                           #   cleanup_unused_contents, freeze_asset
    library_service.py     #   list_assets, search_assets
    light_service.py       #   list_light_type_properties, create/update/remove_light
    camera_service.py      #   list_camera_properties, create/update/remove_camera
    material_service.py    #   create/bind/remove_material, list_materials,
                           #   cleanup_unused_materials
    physics_service.py     #   list_physics_api_properties, apply/remove_physics_api,
                           #   setup_physics_scene, get_physics_summary, joints (3),
                           #   collision groups (3)
    texture_service.py     #   list_textures, search_textures
    validation_service.py  #   validate_scene, package_scene
    variant_service.py     #   add_asset_(material|geometry|attribute|configuration)_variant,
                           #   add_scene_(lighting_attribute|lighting_selection|model_selection)_variant,
                           #   list_variants, select/remove_asset_variant(_set|_for_instance),
                           #   select/remove_scene_variant(_set)

  tools/              # LLM-facing API layer (tool defs + thin handlers).
                      # Every public function mirrors a service function 1:1.
    _helpers.py            #   Precondition guards (require_stage / project / library /
                           #   projects_dir)
    project_tools.py       #   create_project, open_project, list_projects,
                           #   get_current_project
    stage_tools.py         #   create_stage, list_scene, rename/remove_prim, move_asset,
                           #   set/list_prim_attribute(s), snapshot lifecycle, ...
    asset_tools.py         #   place_asset(_inside), list/delete_project_*,
                           #   cleanup_unused_contents, freeze_asset
    library_tools.py       #   search_assets, list_assets
    light_tools.py         #   list_light_type_properties, create/update/remove_light
    camera_tools.py        #   list_camera_properties, create/update/remove_camera
    material_tools.py      #   create/bind/remove_material, list_materials,
                           #   cleanup_unused_materials
    physics_tools.py       #   physics APIs (3), physics scene + summary (2),
                           #   joints (4), collision groups (3)
    texture_tools.py       #   search_textures, list_textures
    validation_tools.py    #   validate_scene, package_scene
    variant_tools.py       #   variant authoring + selection (asset + scene-instance)

  skills/             # Skill SDK. The contract every skill implements.
                      # Skills themselves ship as separate pip packages.
    base.py                #   Skill, SkillContext, SkillConfigError,
                           #   SkillCategory, Tool, ToolResult
    registry.py            #   Entry-point discovery and tool routing

  utils/              # Pure-function primitives. One domain per file.
    stage_utils.py             #   USD-stage primitives: open/save, references,
                               #   xform-op edits, namespace edits, set/list_prim_attribute
    inspection_utils.py        #   Cross-domain list_prims dispatcher (lights, cameras,
                               #   physics, placements, geometry)
    asset_intake_utils.py      #   intake_folder, intake_usdz, create_asset_folder, ASWF
    asset_folder_utils.py      #   ASWF folder primitives (detect root, layer scopes,
                               #   resolve_asset_dir_for_prim)
    library_utils.py           #   scan_library, find_package_for
    light_utils.py             #   All light authoring: create/update/remove,
                               #   list_light_type_properties, lgt.usda lifecycle,
                               #   HDRI staging
    camera_utils.py            #   Camera authoring: create/update/remove, look_at
                               #   aiming, list_camera_properties
    material_utils.py          #   material_in_folder primitives, find_first_material
    texture_utils.py           #   find_textures, copy_texture_to_project,
                               #   find_texture_references
    physics_utils.py           #   All physics authoring: APIs, joints, collision groups,
                               #   phy.usda lifecycle, masking-policy enforcement
    physics_typing_utils.py    #   is_joint / is_physics_scene / is_collision_group / ...
    scene_integrity_utils.py   #   Generic dangling-rel/target scrubbers
    validation_utils.py        #   validate_stage, package_to_usdz, validate_asset_variants
    variant_utils.py           #   variants.usda lifecycle, author_in_variant keystone,
                               #   apply_variant, set/clear_default, removal + cleanup
    geometry_utils.py          #   Bounds, unit conversion, layout math
    layout_utils.py            #   place_layout expansion: grid/linear patterns,
                               #   asset resolution
    dependency_utils.py        #   USD dependency tree walker
    naming_utils.py            #   Name sanitization for files, prims, projects
    usd_schema_utils.py        #   Shared UsdSchemaRegistry introspection helpers
                               #   (used by both physics_utils and light_utils)

Design principles

  • Tool ↔ service ↔ prompt 1:1:1: every public tool function has a same-named public service function and is described in some prompts/*.md file. A test in tests/test_tool_service_prompt_invariant.py fails the build if this ever drifts.
  • Functions only in tools / services / utils: classes live in schemas/ (pydantic models, enums) and a small set of state objects (SceneState, Project).
  • Tools are thin: guard preconditions, call ONE service, wrap in ToolResult. No business logic, no util calls, no cross-service routing.
  • Services own orchestration: take (state, params), do the cross-service and multi-util work, mutate state, raise on errors.
  • Utils are pure primitives: no SceneState, no other services. Composable building blocks.
  • State lives in one place: SceneState holds the open stage, the project binding, the asset library path, and the object counter; tool handlers thread it into service calls.
  • All pxr is in services/ and utils/: the rest of the codebase never imports pxr directly.
  • Prompts are content: editable .md files, not Python constants.
  • Skills are external integrations: new asset providers ship as Python packages discovered via entry points.
  • One config file: ~/.bowerbot/config.json, no .env.

πŸ“ USD Compliance

Every scene follows OpenUSD best practices and the ASWF asset structure guidelines:

Scene level

  • metersPerUnit = 1.0, upAxis = "Y", defaultPrim always set
  • Standard hierarchy: /Scene/Architecture, /Scene/Furniture, /Scene/Products, /Scene/Lighting, /Scene/Cameras, /Scene/Props, /Scene/Physics
  • References only: no inline geometry, no scattered material sublayers
  • Wrapper-prim pattern isolates scene-level transforms from asset-internal ones, so DCC export transforms (Maya pivots, rotations) stay untouched
  • Pre-packaging validator checks defaultPrim, units, up-axis, reference resolution, and material bindings

Asset level

  • References (not sublayers) per ASWF guidelines, for predictable opinion strength
  • Materials inline in mtl.usda, lights inline in lgt.usda, nested references in contents.usda
  • Automatic metersPerUnit conversion across composition boundaries
  • Identity root transforms enforced on intake: pivot dances, baked rotations, and other unfrozen DCC export ops are rejected (or baked into vertex data with explicit user consent), so nested placements compose predictably
  • Nested placements mirror the scene-level wrapper convention (a wrapper Xform holds the per-instance transform, an inner /asset child holds the reference arc), and move_asset / remove_prim on a nested path route writes to contents.usda instead of authoring per-instance overrides at scene level
  • Asset roots carry the canonical ASWF identity: kind = "component" for terminal assets and an assetInfo dictionary (identifier, name, version) so DCC outliners, asset browsers, and pipeline asset-management systems recognise BowerBot output as production-grade

Variant sets

Two layers of authority. The naming convention makes routing explicit.

  • Asset-level variants live in <asset>/variants.usda, referenced (not sublayered) into the asset root. Four orchestrators: material bindings, geometry/LOD payloads, configuration activations, and attribute overrides. The asset's "ship default" lives on the root prim in <asset>.usda, never inside variants.usda.
  • Scene-level variants live inline in scene.usda on a carrier prim. Three orchestrators: lighting attribute swaps and lighting selection on /Scene/Lighting, plus model selection on the placement wrapper. Lighting selection swaps which UsdLux is active across pre-placed siblings (DiskLight vs RectLight). Model selection swaps which asset reference loads at a placement (chair vs stool).
  • Tool names carry an explicit asset_ or scene_ prefix so the LLM never has to guess which layer of authority a call writes to.
  • Foundation: utils/variant_utils.author_in_variant(stage, prim_path, set, name, author_fn) runs any caller function inside the variant's edit context. Asset and scene orchestrators are thin wrappers. Adding a new variant category is a pure addition, never a util change.
  • Per-instance overrides: any placement can author variants = { "set" = "value" } inline in scene.usda to pick a different variant from the asset's default.
  • Validation runs on validate_scene before packaging (referenced not sublayered, default selection present, no orphan reference, naming).

Architectural invariants (apply at every prim mutation):

  1. Orphan opinion cleanup cascade. When a prim is removed, every variant body spec authored at the same path is dropped. Empty intermediate over specs are pruned. Empty variant bodies remove via Sdf.VariantSetSpec.RemoveVariant. Empty variant sets drop along with their variantSetNames and variantSelections metadata. When variants.usda becomes empty, the file is auto-deleted and the root reference scrubbed.
  2. Rename invariant. Renaming a prim follows the rename through every variant body opinion, preserving authored values.
  3. Asset-staging for Sdf.ValueTypeNames.Asset attributes. Variant bodies that author texture or HDRI paths automatically stage the source file into <project>/textures/ and write the project-relative path. Refuses if the source cannot be resolved (no silent broken paths).
  4. Suspect-set detection. After a removal, variant sets that have collapsed to a single model-selection variant (or 2+ variants converging on one prim with active-only opinions) are flagged via suspect_variant_sets on the result. BowerBot surfaces the suspect to the user and asks before deleting the set.
  5. Model-selection symmetry. add_scene_model_selection_variant's first call auto-promotes the placement's existing direct reference into a variant body (named after the source asset folder). Removing the entire set auto-demotes the active variant's reference back to a direct reference on /asset. No data loss, no dead-slot placements.
  6. Layer-level reference scanning. delete_project_asset's safety check scans variant bodies in any layer, not just the composed stage view. An asset referenced only by a non-active variant body still blocks deletion.

Removal scope

  • Removal operations are scoped to one carrier. Removing a variant set from one asset never affects other assets, even when they reference each other.
  • When multiple assets are in scope, BowerBot asks which asset before calling the removal tool. It never guesses.
  • Variants composed in via referenced assets stay visible after removal because they are authored elsewhere. Navigate to that asset and remove them there.

πŸ—ΊοΈ Roadmap

What's next for BowerBot. Contributions welcome:

  • More scene-level variant categories: layout variants (atomic furniture arrangement swap on a group prim) and camera variants (active camera + render settings) on the /Scene/Cameras group. Infrastructure is in place via apply_scene_variant; the orchestrators are pure additions when use cases land
  • Animation variants (asset-level): each variant body references a different animation clip (idle, walk, etc.), production-canonical for articulated state cycling
  • More asset providers: Fab, PolyHaven, Objaverse, CGTrader skills
  • Web UI: chat panel + live 3D viewport
  • BowerHub: community skill registry

🀝 Contributing

BowerBot is open source and welcomes contributions. The best way to start is writing a new skill for an asset provider, DCC, or simulation runtime you use. Skills ship as separate pip packages discovered through the bowerbot.skills entry-point group.

Read CONTRIBUTING.md for the skill contract, the required FastAPI internal layout, and a worked pyproject.toml example for a stand-alone skill package.

For a complete reference, see bowerbot-skill-sketchfab: a real first-party skill on PyPI, with the production layout, entry-point registration, validation, and release pipeline you can mirror for your own.


πŸ’– Sponsors

BowerBot is open source and built by a small team at Binary Core LLC. Sponsorship funds new asset providers (PolyHaven, Fab, CGTrader), USD compliance work, scene templates, documentation, and community support.

Become a sponsor on GitHub. Three monthly tiers (Egg, Nest, Bower) plus one-time options.

Backers

Be the first.


πŸ“„ License

Copyright 2026 Binary Core LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Built with 🐦 by Binary Core LLC

"The bowerbird doesn't have the flashiest feathers. It just builds the most compelling world."