# AD-721i-1 — License-audited starter asset pack (manifest + audit, no asset bytes in v1)

**Status:** Draft v1 (research + manifest + optional fetcher; **no asset bytes shipped in this AD**).
**Closes:** #542.
**Dependencies:** AD-721i (Wave 134, shipped — Blender render pipeline with `_base_meshes/<body_type>.blend` lookup and E10 capsule fallback). AD-721d DSL schema (`body.type`, `hair.style`, `outfit.style`).
**Estimated tests:** +6 pytest. **0 new pip/npm deps.**

---

## Problem

`src/probos/avatars/_blender/render_avatar.py:68` reads `<avatars_dir>/_base_meshes/<body_type>.blend` and falls through to the E10 procedural capsule if missing. The capsule is intentionally crude — fine for v1 smoke tests, not fine for production avatars. The realistic-humanoid path requires per-asset license clearance.

License hygiene is strict (see `.github/copilot-instructions.md` + `/memories/probos-architect-learnings.md` "License posture"): every asset must be CC0 / MIT / Apache-2.0 / BSD / CC-BY (with attribution preserved). No GPL / AGPL / CC-BY-SA / CC-BY-NC. No mixed-license repos absorbed without per-component review.

## Solution

**This AD ships zero asset bytes.** It ships:

1. A `data/avatar-assets/MANIFEST.md` with a per-asset license-disposition table.
2. An optional `scripts/avatar-assets-fetch.ps1` (gitignored target, mirrors `piper-voice-fetch.ps1` shape) that operators run locally to download cleared assets.
3. `.gitignore` rule for `data/avatar-assets/_base_meshes/` and `data/avatar-assets/_hair/` and `data/avatar-assets/_outfits/`.
4. A pytest regression test that asserts the manifest format and parses every license claim.
5. Documentation in `docs/development/avatar-assets.md` for operators.

The actual asset bundling — uploading `slim.blend`, `average.blend`, `stocky.blend`, hair styles, outfits — is deferred to AD-721i-1a once the manifest is reviewed and specific sources are approved by Captain. The reason: assets need provenance evidence (commit hash, download timestamp, license file SHA), and that evidence is generated by running the fetcher script against the upstream sources at build time. Architect-time research can identify candidates; operator-time execution provides the audit trail.

### Section 1 — `data/avatar-assets/MANIFEST.md`

New file documenting:

- **Schema** (markdown table per category):
  - Asset name, source URL, license, version/commit, SHA-256 of the file, attribution string, disposition.
- **Disposition column** is one of: `APPROVED`, `RESEARCH`, `REJECTED`. Only `APPROVED` rows are downloaded by the fetcher.
- **Sections**: `## Base meshes (body_type)`, `## Hair styles`, `## Outfits`, `## Materials/Textures`.

v1 content: candidate sources documented as `RESEARCH` status. **NONE marked APPROVED until Captain ruling.** Candidates listed:

| Source | License | Status | Notes |
|--------|---------|--------|-------|
| Quaternius Free 3D Models | CC0 | RESEARCH | Comprehensive humanoid set; CC0 is the cleanest |
| KayKit Character Pack | CC0 | RESEARCH | Low-poly but VRM-friendly rigging |
| KhronosGroup glTF-Sample-Assets | CC0 / Apache 2.0 | RESEARCH | Reference humanoids; mixed licenses per-asset |
| MakeHuman Community assets | AGPL-3.0 | REJECTED | License conflict — pattern-only absorption |
| Mixamo character library | Proprietary (Adobe) | REJECTED | TOS-bound, not OSS-compatible |
| ReadyPlayerMe API | Proprietary | REJECTED | Commercial overlay only |
| VRoid Studio outputs | Per-file metadata | REJECTED | VRM file-level licensing (see memory note 2026-05-09) |

### Section 2 — Manifest parser + regression test

New module `src/probos/avatars/asset_manifest.py`:

```python
class AssetManifestEntry(NamedTuple):
    category: str       # base_mesh|hair|outfit|material
    name: str
    source_url: str
    license: str        # CC0|MIT|Apache-2.0|BSD|CC-BY-4.0|AGPL|...
    version: str
    sha256: str         # may be "TBD" for RESEARCH rows
    attribution: str
    disposition: str    # APPROVED|RESEARCH|REJECTED

class AssetManifest:
    @classmethod
    def load(cls, path: Path) -> "AssetManifest": ...
    def approved(self, category: str | None = None) -> list[AssetManifestEntry]: ...
    def by_disposition(self, disposition: str) -> list[AssetManifestEntry]: ...
```

Parser: lenient markdown table reader. Skips header rows, accepts pipe-delimited cells, strips whitespace.

License validator function `validate_license(value: str) -> bool` — the **whitelist** is:

```python
_ALLOWED_LICENSES = frozenset({
    "CC0", "CC0-1.0",
    "MIT",
    "Apache-2.0", "Apache 2.0",
    "BSD", "BSD-2-Clause", "BSD-3-Clause",
    "CC-BY-4.0", "CC-BY",  # attribution required — operator MUST preserve it
})
```

Anything outside the whitelist is REJECTED at the validation boundary.

### Section 3 — Fetcher script (PowerShell)

New file `scripts/avatar-assets-fetch.ps1` mirrors `piper-voice-fetch.ps1`:

- Parses `data/avatar-assets/MANIFEST.md`.
- Filters to `disposition == "APPROVED"` rows.
- For each row, downloads via `Invoke-WebRequest` to `data/avatar-assets/_<category>/<name>.<ext>`.
- Verifies SHA-256 against the manifest. On mismatch: delete file, report error, continue.
- Writes attribution to `data/avatar-assets/ATTRIBUTION.txt` (one line per asset for CC-BY compliance).
- Logs per-asset status (ok / skip / fail). Exits non-zero on any failure.

v1 implementation: the script is shipped but the manifest has zero APPROVED rows, so running it is a no-op + zero-byte ATTRIBUTION.txt. Operators see "0 assets approved" and a pointer to the manifest for context.

### Section 4 — Gitignore

Append to `.gitignore`:

```
# AD-721i-1: operator-fetched avatar assets (license-audited per data/avatar-assets/MANIFEST.md)
data/avatar-assets/_base_meshes/
data/avatar-assets/_hair/
data/avatar-assets/_outfits/
data/avatar-assets/_materials/
data/avatar-assets/ATTRIBUTION.txt
```

`MANIFEST.md` itself is **tracked** (it's the audit ledger). Asset bytes are operator-fetched.

### Section 5 — Documentation

New file `docs/development/avatar-assets.md` covers:

- License policy summary (CC0/MIT/Apache/BSD/CC-BY only; per `.github/copilot-instructions.md`).
- How to propose a new asset (PR adds RESEARCH row → Captain reviews → flips to APPROVED → re-run fetcher).
- How to revoke an asset (flip APPROVED → REJECTED → fetcher won't re-download; operator manually deletes; ATTRIBUTION.txt regenerated on next run).
- Verification commands (SHA-256 check, license file presence).

### Section 6 — Renderer wire-up (verify only)

`src/probos/avatars/_blender/render_avatar.py:68` already reads `_base_meshes/<body_type>.blend`. Verify (no edit needed in v1):

- The renderer's `--avatars-dir` argument resolves to `data/avatar-assets/` by convention.
- The procedural-capsule (E10) fallback works when no `.blend` is present (renderer test exists — confirm via grep).

If `--avatars-dir` does NOT currently default to `data/avatar-assets/`, update the renderer to default to `<data_dir>/avatar-assets` as a discoverable convention. This is the only optional source-code change in v1.

### Tests (`tests/test_ad721i_1_asset_manifest.py`)

1. `test_asset_manifest_loads_from_v1_file` — manifest at `data/avatar-assets/MANIFEST.md` parses without raising.
2. `test_asset_manifest_v1_has_zero_approved_rows` — guard against accidental APPROVED additions before Captain review.
3. `test_asset_manifest_all_dispositions_valid` — every row has `APPROVED|RESEARCH|REJECTED`.
4. `test_asset_manifest_rejects_disallowed_licenses` — synthetic manifest with `GPL-3.0` row → row's `validate_license` returns False.
5. `test_asset_manifest_accepts_whitelisted_licenses` — synthetic CC0/MIT/Apache/BSD/CC-BY rows all validate.
6. `test_asset_manifest_approved_filter` — synthetic manifest with mixed dispositions; `manifest.approved("base_mesh")` returns only approved base meshes.

Tests use real file fixtures via `tmp_path` (BF-287). No MagicMock.

## What This Does NOT Change

- AD-721i renderer behavior unchanged — E10 capsule fallback remains the default path when no `.blend` files exist.
- AD-721d DSL schema unchanged.
- No new pip / npm deps.
- No asset bytes committed to the repo.
- No license-bearing files (VRM, GLB with attribution metadata) added to the tracked tree (see "Files with embedded licensing" memory note).
- The AD-721i-2 VRoid Studio CLI alternative (#543) is independent and not blocked by this AD.

## Tracking

- `PROGRESS.md` — Wave 166 entry.
- `docs/development/roadmap.md` — close #542.
- `DECISIONS.md` — append AD-721i-1 with the license whitelist + rejection rationale for MakeHuman/Mixamo/ReadyPlayerMe/VRoid.

Forward markers (TECHNICAL triggers):
- AD-721i-1a — Asset bundling phase. Trigger: ≥3 manifest rows flipped to APPROVED by Captain ruling AND fetcher script verified end-to-end (operator runs it, ATTRIBUTION.txt produced, renderer loads the .blend).
- AD-721i-1b — Per-asset preview thumbnails in HXI avatar picker. Trigger: AD-721h browser VRM upload UI lands.
- AD-721i-1c — Asset attribution surfaced in agent profile panel ("Avatar uses 'Stocky Body' by [author] under CC-BY-4.0"). Trigger: first CC-BY-4.0 asset flipped to APPROVED (the moment attribution becomes a runtime obligation).
- AD-721i-1d — Procedural asset variants (procedurally adjusted height/weight on a base mesh). Trigger: AD-721i-1a lands AND operators report wanting body-shape sliders.

## Acceptance Criteria

- 6 pytest tests green under serial + parallel gates.
- Full pytest gate: previous +N → ≥+6.
- Verify all changes comply with the Engineering Principles in `.github/copilot-instructions.md`.
- `data/avatar-assets/MANIFEST.md` parses cleanly via the new `AssetManifest.load`.
- Zero APPROVED rows in v1 (Test 2 enforces).
- `.gitignore` excludes `data/avatar-assets/_<category>/` and `ATTRIBUTION.txt`.
- `MANIFEST.md` IS tracked.
- No new pip/npm deps.
- License hygiene confirmed per `.github/copilot-instructions.md`: every accepted license is in the whitelist; every rejected source has a documented reason.

## Verified Against Codebase (2026-05-16)

```
grep -n "_base_meshes" src/probos/avatars/_blender/render_avatar.py
  68:    base = Path(avatars_dir) / "_base_meshes" / f"{body_type}.blend"

grep -n "_base_meshes" src/probos/avatars/blender_renderer.py
  134:            base_mesh = self._avatars_dir / "_base_meshes" / f"{dsl.body.type}.blend"

ls scripts/piper-voice-fetch.ps1
  (exists — fetcher pattern reference)

grep -n "class AvatarDSL" src/probos/avatars/dsl.py
  (verify body.type / hair.style / outfit.style enum surface)
```

Issue #542 body confirms parent AD-721i shipped Wave 134 and the `_base_meshes/<body_type>.blend` lookup is the seam this AD targets. License policy hard rules confirmed in `.github/copilot-instructions.md` and `/memories/probos-architect-learnings.md` ("License posture", "License hygiene", "Files with embedded licensing").
