|
| 1 | +# Backend contract: resolve `runflow://` asset refs at dispatch (RUN-384 gaps 2 + 6) |
| 2 | + |
| 3 | +**Target repo:** `runflow-monorepo` (api.runflow.io). **Not** implemented in `runflow-js` — |
| 4 | +this document is the companion spec for the backend ticket. |
| 5 | + |
| 6 | +## Current behavior (verified in code) |
| 7 | + |
| 8 | +- **Writes** (`POST /v1/models/{owner}/{slug}/runs`, `POST /v1/comfyui-workflows/.../runs`, |
| 9 | + batches, admin retries): inline `data:` URIs in `body.input`/`body.metadata` are |
| 10 | + auto-materialized to R2 and stored as `runflow://assets/{uuid}` refs. This is correct |
| 11 | + and should stay. |
| 12 | +- **Reads** (`GET /v1/runs`, `GET /v1/runs/{id}`, batch/canonical listings): asset-backed |
| 13 | + URLs are re-signed and `runflow://assets/{uuid}` refs are resolved to short-TTL signed |
| 14 | + HTTPS URLs. Also correct. |
| 15 | +- **The gap:** at **model dispatch**, the materialized `runflow://` ref is forwarded to the |
| 16 | + model worker **as-is**. Worker media validators accept only HTTP(S)/`data:` URIs — |
| 17 | + `google/nano-banana-pro/edit` (and most non-ComfyUI models) reject with |
| 18 | + `422: media URL must use HTTP(S) or data URI, got 'runflow'`. ComfyUI workflow file |
| 19 | + inputs already accept the refs; singleton model dispatch does not. |
| 20 | + |
| 21 | +So the `data:`-materialization *convenience* currently breaks the exact requests it |
| 22 | +rewrites. (The original ticket framed this as "make materialization opt-in"; the better |
| 23 | +fix below removes the need for any flag.) |
| 24 | + |
| 25 | +## Requested change |
| 26 | + |
| 27 | +At the dispatch layer — after materialization, before the input reaches the model |
| 28 | +worker / provider transport — apply the **same resolution the read path already does**: |
| 29 | + |
| 30 | +1. Walk `body.input` (and `metadata` where it feeds workers) for strings of scheme |
| 31 | + `runflow://assets/{uuid}`. |
| 32 | +2. Resolve each ref org-scoped (existing read-side resolver semantics: load asset, check |
| 33 | + `access_expires_at`, sign `r2_key`) into a signed HTTPS URL whose TTL comfortably |
| 34 | + covers worker pull + retries (suggest ≥ the worker's max queue+run window). |
| 35 | +3. Forward the signed HTTPS URL to the worker. Persist the **ref** (not the signed URL) |
| 36 | + on the run record, as today. |
| 37 | +4. Unknown/foreign-org/expired refs → 422 with a precise message |
| 38 | + (`asset not found or expired: runflow://assets/{uuid}`) — fail at dispatch, not in |
| 39 | + the worker. |
| 40 | + |
| 41 | +### Acceptance criteria |
| 42 | + |
| 43 | +- `POST /v1/models/google/nano-banana-pro/edit/runs` with `input.image_urls: |
| 44 | + ["runflow://assets/{uuid}"]` succeeds end to end (no 422), for both an explicit ref |
| 45 | + and one produced by `data:` auto-materialization. |
| 46 | +- Read-side responses are unchanged (refs still resolve on read). |
| 47 | +- ComfyUI dispatch behavior unchanged. |
| 48 | +- A run whose ref points at a foreign org's asset 404s/422s without leaking existence |
| 49 | + details beyond the standard non-leaky pattern. |
| 50 | + |
| 51 | +### Why dispatch-side resolution (not validator changes, not an opt-in flag) |
| 52 | + |
| 53 | +- One implementation point instead of N model-validator changes across providers. |
| 54 | +- Asset refs become first-class on the write path, matching the read path — the SDK can |
| 55 | + then hand `UploadedAsset.ref` (stable, no TTL) to any model instead of the signed `url`. |
| 56 | +- The `data:` materialization default stays a pure convenience with no footgun, so no |
| 57 | + config flag is needed. |
| 58 | + |
| 59 | +## Interim state (already shipped in runflow-js) |
| 60 | + |
| 61 | +`rf.assets.upload(file)` returns the **signed HTTPS** url from the confirmation response |
| 62 | +(`routers/asset_uploads.py` signs it server-side), so external forks are unblocked today |
| 63 | +without this change. Once dispatch-side resolution lands, the SDK will document `ref` as |
| 64 | +the preferred long-lived input. |
| 65 | + |
| 66 | +## Suggested ticket |
| 67 | + |
| 68 | +> **Title:** Resolve `runflow://assets/{uuid}` refs to signed HTTPS at model dispatch |
| 69 | +> **Parent:** RUN-384 |
| 70 | +> **Why:** Auto-materialized `data:` inputs currently 422 on most singleton models |
| 71 | +> (worker media validators only accept HTTP(S)/data:). Read path already resolves refs; |
| 72 | +> dispatch must do the same so asset refs are first-class across the stack. |
| 73 | +> **Scope:** dispatch layer for `POST /v1/models/.../runs` (+ batches, retries); |
| 74 | +> resolver reuse from the read path; 422 on unknown/expired refs; tests per acceptance |
| 75 | +> criteria above. ComfyUI unchanged. |
0 commit comments