Skip to content

validate a standing unattended render route when it is saved, not on every job it fails - #4823

Merged
atomantic merged 2 commits into
mainfrom
claim/issue-4348
Aug 22, 2026
Merged

validate a standing unattended render route when it is saved, not on every job it fails#4823
atomantic merged 2 commits into
mainfrom
claim/issue-4348

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

The peer picker this run was dispatched for shipped in #4819, so this slice takes the one gap in "Proposed approach" item 6 that is neither ADR-gated nor already decided against: provider/model selection and capacity messaging on the Creative Commission flow — Instances → Unattended render routing, the surface left behind when the interactive ones were consolidated.

A standing route is now validated where it is SAVED, not only where it is used. federatedMediaRoutingSchema proves a route is well-shaped and nothing more, so a route naming a peer that is unknown, switched off, not enabled as a media provider, not allowlisted for that exact engine/model pair, or reachable outside the tailnet saved happily and then failed every future Creative Director / Creative Commission render of that kind. None of those is transient capacity — such a route can never run — and unattended work has no human at the moment it fails, so it broke in silence. PUT /api/settings now refuses it with the same typed reason the enqueue would have given (MEDIA_PROVIDER_PEER_NOT_FOUND / _PEER_DISABLED / _NOT_CONFIGURED / _MODEL_NOT_ALLOWED / MEDIA_ROUTING_PEER_NOT_TAILNET), rather than two vocabularies for one misconfiguration.

Two boundaries are deliberate:

  • Only durable configuration is checked at save time. A provider is routinely asleep, busy, or mid-probe when its route is configured, so gating the save on a fresh snapshot would make the card unusable at exactly the moment someone sits down to set it up. Freshness, queue admission, and per-model readiness stay on the enqueue path, which re-checks all of them — and re-checks the tailnet gate per request, since a peer's host can be edited after the save.
  • Clearing a route is always allowed, whatever became of its peer. Without that, a save-time gate would be the thing that makes a bad configuration permanent.

The card stops being the surface that re-derives all this by hand. It now reads the shared federatedMediaReadiness helpers the Instances peer card, System Health, and the interactive pickers use, so it cannot become a fourth opinion; it gains the routed peer's readiness and shared-queue occupancy as a caption (item 6's "capacity messaging", wired to the select with aria-describedby); and it surfaces the server's refusal message instead of a generic failure, which is the useful half now that a refusal names a fixable cause.

Its inline copy of the tailnet predicate is replaced by client/src/lib/tailnetPeer.js, a faithful port of server/lib/tailnetPeer.js carrying that suite's own test table. The inline copy had drifted: it range-checked only the second octet, so an address like 100.64.999.1 read as tailnet in the browser and as ordinary public space on the server — the picker would have offered a peer the server refuses. The server stays authoritative either way; the port only lets the picker explain an absent option.

Also moved: the route's shape (ROUTABLE_MEDIA_KINDS, sanitizeRoute, normalizeMediaRoutingConfig) out of defaultRouting.js — which owns enqueue-time resolution — into the new routingPolicy.js, re-exported so existing callers keep one import site. modelKey is now exported from federatedMediaConsumer.js so the save gate and the submit gate cannot disagree about what "allowlisted" means.

Test plan

  • cd server && npm test — 33002 passed, 19 skipped, 0 failed.
  • cd client && npm test — 9423 passed, 0 failed.
  • cd client && npm run lint — clean.
  • New server/services/federatedMedia/routingPolicy.test.js (20 cases): the accept path; each refusal with its code and status; engine/model treated as a pair, not a model id; every kind in a patch checked, not just the first; clearing allowed with an empty registry; a currently-offline peer and a peer with an expired snapshot both still savable.
  • New client/src/lib/tailnetPeer.test.js: the server suite's table case for case, plus the malformed-CGNAT cases the old inline copy accepted.
  • server/routes/settings.test.js: route-level accept/refuse/clear, with the peer registry mocked (the policy loads it lazily, so an unmocked run would read the developer's own data/instances.json).
  • client/.../UnattendedRenderRouting.test.jsx: readiness + queue caption for a routed kind, an expired snapshot read as stale rather than repeating its own ready, an unregistered routed peer, no caption for a locally-rendered kind, and the server's reason surfaced on a refused save with a generic fallback when none is carried.
  • Local agy review caught a real CI failure this branch would otherwise have shipped: two malformed synthetic addresses in the new test table sit inside 100.64.0.0/10 and tripped scripts/tailnet-identity-leak.test.js. Fixed in the second commit by allowlisting them (they carry an octet > 255, so they are provably not any real address) — the same treatment the existing top-of-range fixture already gets. That guard scopes to git-tracked files, so it only goes red once the files are committed.

Remaining

Still open on #4348, unchanged by this PR:

  • Input-asset transfer (init/reference images, keyframes, clips to extend, LoRA weights) — still refused with MEDIA_PROVIDER_INPUT_UNSUPPORTED. ADR-gated: docs/decisions/2026-08-20-federated-visual-prompts.md says widening the carve-out "requires a new ADR, not a reading of this one".
  • Privacy-preserving remote lyrical conditioning — same gate, and it is also what keeps unattended audio local.
  • Multi-provider fairness / failover — listed under Out of scope on the issue, and in tension with the fail-closed-never-fail-quiet decision recorded in feat([issue-4348]): route unattended renders to a configured federated peer #4679. Worth an explicit decision rather than silent drift.
  • A fixed-vocabulary visual profile for image/video prompts — explored during feat([issue-4348]): federate image/video job kinds (discovery + submission) #4674's review and rejected by the ADR under Alternatives considered, so it is a decided question rather than a pending one. Listing it here for the record.

Refs #4348

…nstead of on every job (#4348)

Instances -> Unattended render routing saved whatever it was given. A route
naming a peer that is unknown, switched off, not enabled as a media provider,
not allowlisted for that exact engine/model pair, or reachable outside the
tailnet is not a transient capacity problem -- it can never run -- and
unattended work has no human at the moment it fails, so it broke every future
Creative Director / Creative Commission render of that kind in silence.

PUT /api/settings now validates federation.mediaRouting against the durable
configuration and refuses it with the same typed reason the enqueue would have
given later. Live capacity is deliberately NOT a save-time gate: a provider is
routinely asleep when its route is configured. Clearing a route is always
allowed, so a bad configuration can never become permanent.

The card itself stops being the one surface that re-derives all this by hand:
it now reads the shared readiness lib the Instances peer card, System Health
and the interactive pickers use, gains the routed peer's readiness and queue
occupancy as a caption, and surfaces the server's refusal message instead of a
generic failure. Its inline copy of the tailnet predicate -- which range-checked
only the second octet, so 100.64.999.1 read as tailnet in the browser and as
public space on the server -- is replaced by client/src/lib/tailnetPeer.js,
a faithful port carrying the server suite's own test table.
The two addresses carry an octet > 255, so they are provably not any real
peer — but they sit inside 100.64.0.0/10, which is what the identity-leak
guard scans for. Same treatment the existing top-of-range fixture already gets.
@atomantic
atomantic merged commit bdb5c3a into main Aug 22, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-4348 branch August 22, 2026 05:53
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.

1 participant