assetforge 0.3.2

License-aware discovery and archival helpers for free 3D assets
Documentation
# AssetForge Agent Guide

This guide describes AssetForge 0.3.2 as implemented. AssetForge is a blocking Rust library, not a command-line tool. It discovers provider metadata and archives provider bundles without scraping websites, bypassing access controls, changing licenses, or extracting downloaded ZIP files.

The same text is available at runtime as `assetforge::AGENT_GUIDE`.

## Minimal flow

Create one `AssetForge` client per provider. Always use an application-specific User-Agent with a contact or project identifier; Poly Haven requires API clients to identify themselves.

```rust
use std::path::PathBuf;

use assetforge::{
    parse_asset_target_for_provider, ArchiveOptions, ArchiveProgressPhase, AssetForge,
    AssetFormat, AssetProvider, AssetResolution,
};

let provider = AssetProvider::PolyHaven;
let forge = AssetForge::with_user_agent(
    provider,
    "my-asset-tool/1.0 (contact@example.com)",
)?
.with_discovery_limit(10);

// Shelf_01 is an identifier used by the live Poly Haven API coverage.
let target = parse_asset_target_for_provider("asset:Shelf_01", provider)?;
let assets = forge.discover(&target)?;
let options = ArchiveOptions {
    output: PathBuf::from("assets"),
    format: AssetFormat::Gltf,
    resolution: AssetResolution::OneK,
    skip_existing: true,
    max_download_bytes: 256 * 1024 * 1024,
};
let summary = forge.archive_assets_with_progress(&assets, &options, |progress| {
    if progress.phase == ArchiveProgressPhase::Downloading {
        eprintln!("{} bytes", progress.bytes_written);
    }
})?;
assert!(summary.is_success(), "failures: {:?}", summary.failed);
# Ok::<(), assetforge::AssetForgeError>(())
```

`discover` returns `AssetSummary` values authenticated for that client provider. Pass summaries only to the same provider client; a mismatch is rejected. `bundle` can resolve one asset without archiving it. `archive_assets` and `archive_assets_with_progress` return an aggregate `ArchiveSummary`; an individual asset failure is recorded in `summary.failed` rather than aborting later selected assets.

## Providers and bundle choices

`AssetFormat::Auto` and `AssetResolution::Auto` resolve to provider-specific values. Read the resolved `AssetBundle.format` and `.resolution`, or the archived manifest, rather than assuming the requested `Auto` values were retained.

| Provider | Discovery and content | Accepted format | Accepted resolution | `Auto` result |
| --- | --- | --- | --- | --- |
| `PolyHaven` | Poly Haven CC0 models and dependency-complete scenes | `Gltf`, `Fbx`, `Blend`, `Usd` | `OneK`, `TwoK`, `FourK`, `EightK`, subject to asset availability | 2K glTF |
| `AmbientCG` | ambientCG CC0 `3d-model` records and ZIP packages | `Archive` | `OneK`, `TwoK`, `FourK` | 2K archive, preferring HQ then SQ then LQ JPG |
| `Sketchfab` | Downloadable, non-age-restricted Sketchfab models whose API license is CC0 | `Gltf` | `Original` | Original glTF ZIP |
| `GoogleScannedObjects` | Public Google Research collection members exposed by Gazebo Fuel | `Archive` | `Original` | Original ZIP archive |

For Poly Haven, `Archive` and `Original` are invalid choices. For ambientCG, `Original` and `EightK` are invalid. For Sketchfab and Google Scanned Objects, fixed texture resolutions are invalid. Google packages contain the provider's documented model files, commonly OBJ plus textures, but AssetForge intentionally treats the ZIP as `Archive`; there is no `Obj` format variant and the library does not extract it.

Known API examples used by the crate include:

- Poly Haven: `Shelf_01` and `https://polyhaven.com/a/wooden_stool_02`
- ambientCG: `3DTreeStump001` and `https://ambientcg.com/a/3DTreeStump001`
- Sketchfab: `45bf9e2128bd4c1b97ef3018e8163999` and `https://sketchfab.com/3d-models/cargo-ship-45bf9e2128bd4c1b97ef3018e8163999`
- Google Scanned Objects: `Mens_Authentic_Original_Boat_Shoe_in_Navy_Leather_NHHQddDLQys` and `https://fuel.gazebosim.org/1.0/GoogleResearch/models/Mens_Authentic_Original_Boat_Shoe_in_Navy_Leather_NHHQddDLQys`

Provider catalogs can change. Handle `NotFound` and `BundleUnavailable` even for previously observed IDs.

## Sketchfab token

Sketchfab discovery is public, but `bundle` and archive downloads require the end user's own API token. AssetForge does not read the environment automatically. Read `SKETCHFAB_API_TOKEN` in the application and pass it with `with_api_token`; never put the token in a target, URL, log, manifest, or source file.

```rust
use assetforge::{AssetForge, AssetProvider, AssetTarget};

let token = std::env::var("SKETCHFAB_API_TOKEN")
    .expect("set an end-user Sketchfab API token");
let forge = AssetForge::with_user_agent(
    AssetProvider::Sketchfab,
    "my-asset-tool/1.0 (contact@example.com)",
)?
.with_api_token(token)
.with_discovery_limit(5);
let assets = forge.discover(&AssetTarget::Search("cargo ship".into()))?;
# Ok::<(), assetforge::AssetForgeError>(())
```

The token is sent as `Authorization: Token ...` only to the trusted Sketchfab API host. Download URLs are temporary delivery URLs and are separately host-validated.

## Targets and parsing

Construct `AssetTarget` directly when possible:

- `AssetTarget::Asset(id)` selects one canonical provider ID.
- `AssetTarget::Search(text)` runs free-text discovery.
- `AssetTarget::Category(name_or_path)` filters by a provider category.
- `AssetTarget::Top` selects popular assets.
- `AssetTarget::Latest` selects recently published assets.

`parse_asset_target_for_provider(input, provider)` accepts these user-facing forms:

- `asset:<id>`, `search:<text>`, and `category:<name-or-path>`
- `top`, with aliases `popular` and `trending`
- `latest`, with aliases `newest` and `new`
- A plain string containing whitespace as search text
- A plain string without whitespace as an exact asset ID
- A canonical HTTPS asset URL for the selected provider, including the examples above

`parse_asset_target(input)` is only the Poly Haven-style convenience parser. Use the provider-aware function for ambientCG, Sketchfab, and Google URLs. URLs with another host, non-HTTPS URLs, credentials, non-default ports, and unrecognized path shapes are rejected. An exact Google Scanned Objects ID is accepted only if it is a member of the official Google Research collection and its fetched record still has the required owner and license metadata.

The category value is provider-defined. Poly Haven supports its category values and paths, ambientCG and Sketchfab pass the value to their APIs, and Google Scanned Objects compares collection metadata categories case-insensitively.

## Ranked selection

Collection limits default to 20. `with_discovery_limit(n)` clamps the value to 1 through 100.

- Search normalizes punctuation and case, requires every query term to occur in the ID, name, tags, categories, or description, and ranks stronger ID/name/tag/category matches ahead of description-only matches. Download count breaks equal relevance scores; canonical ID is the final stable tie-breaker.
- `Top` and `Category` order by download count, then ID.
- `Latest` orders by publication date, then download count, then ID.
- Exact `Asset` discovery returns one record or `NotFound`; it is not ranked.

Poly Haven ranking uses its model catalog. Google Scanned Objects search ranks the complete fetched official collection, cached per client. ambientCG and Sketchfab ask their APIs for bounded candidate sets and then apply local ranking, so their search is ranked selection from those candidates rather than an exhaustive mirror of each provider.

Do not silently take `assets[0]` for an ambiguous request. Show the ranked metadata and `rights` to a user, or apply explicit application criteria such as polygon count, format availability, and download size before archiving.

## Terms, licenses, and attribution

Licensing and API terms are separate. Every `AssetSummary` and resolved `AssetBundle` contains `AssetRights { license, license_url, attribution, api_terms_url }`; the archive copies authoritative rights into `assetforge-asset.json`. Preserve this metadata with the asset and consult the linked current terms.

- Poly Haven assets are recorded as `CC0-1.0` with `Poly Haven` attribution metadata. Its public API terms separately limit API use to reasonable non-commercial use unless separately licensed.
- ambientCG assets are recorded as `CC0-1.0` with `ambientCG` attribution metadata. Follow the ambientCG API terms even though the content is CC0.
- Sketchfab results are restricted to downloadable CC0 models. Downloads require the end user's token. Preserve the specific `<creator> via Sketchfab` attribution and comply with the Sketchfab API agreement; do not generalize a result to CC0 unless the returned rights say so.
- Google Scanned Objects are recorded as `CC-BY-4.0`, not CC0. Preserve `Google Research via Gazebo Fuel` attribution and satisfy CC BY 4.0 attribution requirements. AssetForge rejects records that are private, not owned by `GoogleResearch`, not public, or missing the expected CC BY 4.0 metadata.

The exported URL constants (`POLY_HAVEN_LICENSE_URL`, `POLY_HAVEN_API_TERMS_URL`, `AMBIENTCG_LICENSE_URL`, `AMBIENTCG_API_TERMS_URL`, `SKETCHFAB_CC0_LICENSE_URL`, `SKETCHFAB_API_TERMS_URL`, `GOOGLE_SCANNED_OBJECTS_LICENSE_URL`, and `GAZEBO_FUEL_API_TERMS_URL`) are useful for interfaces, but the per-result `rights` value is the archive authority.

## Archive, progress, and verification

`ArchiveOptions::default()` writes one directory per asset under `assets`, requests automatic format and resolution, does not skip existing output, and caps each bundle at 1 GiB. Set `max_download_bytes` to a smaller application limit; `0` explicitly disables this configurable bundle cap.

Archiving follows this sequence:

1. Re-fetch authoritative asset metadata and resolve the requested bundle.
2. Reject an advertised bundle over `max_download_bytes`.
3. Stream each file into a unique staging directory and a `.assetforge-part` file, enforcing advertised and configured sizes while downloading.
4. Verify Poly Haven's provider MD5 for every scene and dependency. Other providers do not supply MD5 through this API contract.
5. Compute a local BLAKE3 digest for every downloaded file from every provider.
6. Write and verify `assetforge-asset.json`, including provider/source/rights, the actual format and resolution, provider revision where available, byte totals, file metadata, and BLAKE3 values.
7. Rename the complete staging directory into place. Existing recognized AssetForge output is replaced through a backup rename; partial downloads and failed staging directories are removed.

Progress callbacks receive `Starting`, `Downloading`, and `Completed` snapshots. Active downloads report the asset, relative file, active bytes, optional advertised file size, aggregate bytes, and success/failure counts. Byte updates are emitted at approximately 1 MiB intervals and at file completion; write callbacks so they do not block for long periods.

With `skip_existing: true`, AssetForge does not trust directory existence alone. It resolves the current bundle and provider revision, reads a bounded manifest, validates identity and rights, rejects symlinks and non-regular files, checks paths and trusted URLs, verifies every file size and byte total, verifies Poly Haven MD5 where applicable, and verifies every version-2 BLAKE3 digest. Only a matching complete archive is skipped. A stale variant, revision, missing file, changed byte, malformed manifest, or unsafe path is not treated as a valid skip.

AssetForge refuses to overwrite an existing directory unless it contains a recognizable manifest for the same provider and asset. Inspect `ArchiveSummary.archived`, `.skipped`, `.failed`, `.files_archived`, and `.bytes_written`; `is_success()` means there were no per-asset failures.

## Host, path, and size safety

AssetForge validates every API URL, download URL, and redirect as HTTPS on the default HTTPS port with no URL credentials. Redirect following is manual and limited to five hops. Current allowed hosts are:

- Poly Haven API: `api.polyhaven.com`; downloads: `dl.polyhaven.org`
- ambientCG API: `ambientcg.com`; downloads: `ambientcg.com` and `acg-download.struffelproductions.com`
- Sketchfab API: `api.sketchfab.com`; downloads: `sketchfab.com`, its subdomains, and the explicit Sketchfab production S3 hosts
- Google Scanned Objects API and downloads: `fuel.gazebosim.org`

Provider JSON responses are limited to 16 MiB, error bodies to 8 KiB, and manifests to 4 MiB. Google collection traversal is limited to 20 pages of 100 records. Poly Haven dependency traversal is limited to 128 files and eight include levels. Bundle paths are limited to 512 bytes and 16 components.

Asset IDs used as output directory names may contain only ASCII letters, digits, `-`, `_`, and `.`, and cannot be `.` or `..`. Bundle paths must be non-empty relative normal paths: absolute paths, `..`, dot components, duplicate/conflicting case-folded paths, the manifest name, and internal partial-file names are rejected. Archive verification uses capability-scoped directory access and rejects symlinks in every path component.

The default download cap is a safety boundary, not an estimate. AssetForge checks advertised totals, response `Content-Length` where available, and streamed bytes. Keep a nonzero cap appropriate to the application and available storage.

Downloaded ZIPs remain opaque files. AssetForge validates the ZIP download's host, size, and digest, but does not validate paths inside the ZIP. If another component extracts a provider archive, that component must independently reject absolute paths, traversal, links, special files, decompression bombs, and excessive extracted sizes/counts.