Skip to content

feat(icons): deny the network by default, and give the cache a way to be filled - #454

Merged
LeadcodeDev merged 1 commit into
mainfrom
feat/icon-offline-gate
Sep 29, 2026
Merged

LeadcodeDev merged 1 commit into
mainfrom
feat/icon-offline-gate

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #440.

#320 gated Google Fonts: a family missing from the cache is refused by name, with the URL that was not called and the directory to drop a TTF into, and one global flag opts in. Icons had nothing — icon_source_cache reached api.iconify.design on any miss, from validate, still, sheet and every encoder, with no opt-in and no way to say "this render must not touch the network".

The gate

RemoteIconPolicy mirrors RemoteFontPolicy, down to the atomic and the shape of the refusal:

Icons: 'lucide:satellite-dish' is not in the icon cache, and fetching it would reach
https://api.iconify.design/lucide/satellite-dish.svg — a scenario chooses that target,
so it is denied by default.
Run `rustmotion icons prefetch -f <scenario>` once to fill the cache, pass
--allow-remote-icons to opt in for this run, or place the SVG at:
  /Users/…/.cache/rustmotion/icons/lucide_satellite-dish.svg

Its own flag, not a shared one. The issue left this open, and the argument that settles it is consent: widening --allow-remote-fonts to cover icons grants it retroactively. A CI job passing that flag today consented to fonts.googleapis.com, not to a second host that a scenario gets to name. Two flags, each naming its own target, is the version that can be explained honestly. An umbrella --allow-network can be added later on top of both; it cannot be subtracted.

Where it sits matters. After the cache read, and after the pre-#425 legacy name migration. A cache filled under the old {slug}-{colour}-{w}x{h}.svg naming still resolves offline, because refusing the network must not break a render that already has what it needs. There is a test for exactly that, since it is the one placement mistake that would look fine in review.

The way to fill the cache

A deny-by-default with no prefetch turns an offline CI runner into a manual copy of SVGs — which is what #320's cache_hint already asks of it for fonts, and the issue called that out as the piece that makes the first one usable.

rustmotion icons check -f scenario.json      # list what is missing, fetch nothing
rustmotion icons prefetch -f scenario.json   # download them once
rustmotion render -f scenario.json           # renders offline from here on

prefetch takes no flag: running it is the opt-in. Both subcommands reuse the walk preload.rs already performed, extracted as collect_icon_requests rather than written a second time.

--quiet makes check print one missing icon per line and nothing else, so it composes in a shell.

Verified end to end

On a scenario naming two icons absent from the cache, in this order:

  1. icons check → 2 icon(s) named, 0 already cached, both listed
  2. still without the flag → refused, naming the icon, the URL, both ways out and the exact path
  3. icons prefetch → Fetched 2 icon(s)
  4. still, still without the flag → renders, both icons painted in their own colours
  5. icons check → Nothing to fetch — this scenario renders offline.

Four tests, each verified to fail when its own fix is reverted. cargo fmt --all --check, cargo clippy --workspace --all-targets --features rustmotion/studio -- -D warnings, and cargo test --workspace --features rustmotion/studio (2132 tests) pass.

Also

IconsUnresolved advised "connect once so they are downloaded" — stale as soon as the gate exists, since connecting is precisely what is now refused. It names the prefetch command instead.

Documented in CLAUDE.md next to the font gate, and in the README's CLI reference.

… be filled

Closes #440.

#320 gated Google Fonts: a family missing from the cache is refused by name,
with the URL that was not called, and one global flag opts in. Icons had
nothing. `icon_source_cache` reached api.iconify.design on any miss, from
`validate`, `still`, `sheet` and every encoder, with no opt-in and no way to say
that a render must not touch the network.

`RemoteIconPolicy` mirrors `RemoteFontPolicy`, down to the atomic and the
refusal naming the icon, the URL and the file to drop an SVG into.

The flag is its own, `--allow-remote-icons`, rather than a shared
`--allow-remote-fonts` or an umbrella. Widening an existing flag grants consent
retroactively: a CI job passing `--allow-remote-fonts` today consented to
fonts.googleapis.com, not to a second host a scenario names. Each flag names its
own target.

A deny with no way to fill the cache is a deny with no way out, so
`rustmotion icons prefetch -f <scenario>` downloads what a scenario names, and
`icons check` reports what is missing without fetching. `prefetch` takes no
flag — running it is the opt-in. Both reuse the walk `preload.rs` already had,
extracted as `collect_icon_requests`.

The gate sits after the cache read AND after the pre-#425 name migration, so a
cache filled under the old `{slug}-{colour}-{w}x{h}.svg` naming still resolves
offline. Refusing the network must not break a render that already has what it
needs; a test pins that specifically.

`IconsUnresolved` stopped advising "connect once" and names the prefetch
command instead.
@LeadcodeDev LeadcodeDev self-assigned this Sep 29, 2026
@LeadcodeDev
LeadcodeDev merged commit 4626e91 into main Sep 29, 2026
4 checks passed
@LeadcodeDev
LeadcodeDev deleted the feat/icon-offline-gate branch September 29, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An offline render has a gate for fonts and none for icons

1 participant