feat: remix tier gating — t1/t2 behind explicit opt-in (default unchanged)#20
feat: remix tier gating — t1/t2 behind explicit opt-in (default unchanged)#20mtmtian wants to merge 13 commits into
Conversation
Add an independent worker-engine RemixJob path alongside the existing
Seedance2-direct remix: control plane creates a pending RemixJob + brief via
POST /api/creatives/remix-jobs, an external HMAC-authenticated worker claims
it, generates beats, assembles/QCs the clip, and reports progress/results
back via /api/worker/remix-jobs/{claim,report,upload}. Purely additive —
reuses buildRemixBrief/competitorCreativeToAnalysis from remix-brief.ts
without touching any existing files.
…n upload Close the code-review gaps on the worker-facing RemixJob surface: report/ now enforces an atomic ALLOWED_PREV transition table (succeeded/failed are terminal), validates outputUrl against the canonical GCS upload path, and surfaces brand-QC failures via Creative.reviewNotes instead of silently promoting. upload/ pre-checks HMAC headers and content-length before buffering the body. claim/ recycles stale in-flight jobs after a configurable lease window and collapses the duplicated jobId/queue branches into one atomic updateMany. De-dupes asJson + canvas-dims math into src/lib/growth/remix-job.ts, and shares the e2e HMAC-signing helper between remix-jobs.spec.ts and competitor-ingest.spec.ts.
Review — remix tier gatingEverything in the #19 review applies here too, since this branch contains those commits — in particular the First, the good news, because it's the part the PR description leads with and it checks out. The problem is with the argument built on top of that, which is roughly "this is safe because it's opt-in." Two findings undercut it. 🔴
|
…-path index Review fix for PR oratis#19: claim/upload/report need a way to detect a preempted worker so it can never overwrite a new holder's output. Adds claimToken (reissued per claim) + attempt (bumps per claim, versions the output object key) plus @@index([status, updatedAt]) for the claim hot path. audit.ts gets the four remix.job_* AuditAction variants used by the routes in the next commits.
Review fixes for PR oratis#19: - claim: mint+return claimToken/attempt, rate-limit (worker-claim, 600/min/IP), audit-log remix.job_claim. - upload: require x-adex-claim-token, bind the HMAC signature to jobId:claimToken:sha256 so a preempted worker's old signature can't be replayed; verify claimToken + in-flight status before writing; version the object key as v${attempt}.mp4; rate-limit (worker-upload, 120/min/IP); audit-log remix.job_upload. - report: require claimToken in the body, include it in both updateMany wheres (atomic fencing), disambiguate count!==1 into 404/409-stale-claim/ 409-illegal-transition; wrap updateMany + re-read + linked-Creative update in a single $transaction so the job can never end up terminal with an orphaned Creative; exact-match the canonical (versioned) outputUrl instead of substring; best-effort delete the current attempt's blob on failed; rate-limit (worker-report, 300/min/IP); audit-log remix.job_report. - creatives/remix-jobs: wrap Creative+RemixJob create in one transaction, move the rate-limit key from 'remix-jobs' to 'remix' (shared bucket with the sibling /api/creatives/remix so the two endpoints can't double the effective burst), audit-log remix.job_create.
Review fix for PR oratis#19: e2e.yml had no DB, so competitor-ingest.spec.ts and remix-jobs.spec.ts self-skipped in CI, silently. Adds a postgres:16-alpine service container + DATABASE_URL, INGEST_WEBHOOK_SECRET/WORKER_WEBHOOK_SECRET (testing-only values, not real secrets) and INVITE_CODES_DISABLED=true so register works without minting codes, plus a `prisma migrate deploy` step. playwright.config.ts's webServer.env now reads WORKER_WEBHOOK_SECRET and INGEST_WEBHOOK_SECRET from process.env (falling back to the same literals the specs themselves fall back to) instead of hardcoding WORKER_WEBHOOK_SECRET — previously a runner that set the secrets in its own env to un-skip the specs but didn't also override this hardcoded value would sign requests the dev server couldn't verify.
…e cases Review fix for PR oratis#19: existing claim/report/upload cases updated for the new contract (claimToken/attempt from claim, claimToken required on report, versioned canonical outputUrl). New negative cases: report with a wrong claimToken → 409 stale claim; upload missing x-adex-claim-token → 401; upload signed against the wrong claimToken → 409. All reject before touching GCS, so no credentials needed; the upload happy path stays untestable locally (same as before). .env.example documents REMIX_JOB_LEASE_MINUTES (already read by claim/route.ts, was undocumented).
Adds t1/t2 tier codes to the remix worker engine behind an explicit REMIX_ENABLED_TIERS opt-in (defaults to t0_5 only, matching current IP policy). t1/t2 require a segmentPlan (t2) and a Tier-2 hand-picked competitor video already stored via the Save video flow; claim now returns refs[] pointing the worker at that stored Asset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WHY: post-PR adversarial review of 51c1ce8 WHAT: segmentPlan requires non-negative ordered non-overlapping segments (max 64) within the source video duration; REMIX_ENABLED_TIERS always keeps t0_5 enabled; t2 Creative.duration derives from kept segments; claim refs lookups scoped to the job's org
…TIERS Review fixes for PR oratis#20: - claim: REMIX_ENABLED_TIERS was only enforced at job creation. Re-check it at claim time too (oldestPending/oldestStale lookups + the claiming updateMany all filter tier IN enabledTiers) — pulling a tier from the env now takes effect immediately for any t1/t2 job still pending/stale, not just new creates. Known boundary: a job already claimed and within its lease when the tier is disabled can't be recalled (the worker already holds its refs) — noted in the route's header comment, not fixed here. - claim: audit-log competitor.video_reference (IP-sensitive, same as competitor.video_store) whenever a t1/t2 claim actually hands the worker a ref. - remix-job.ts: parseEnabledTiers now trims + lower-cases each token and validates against KNOWN_TIERS before adding it to the enabled set — an unrecognized/mis-cased token is dropped with a console.warn instead of silently joining the allowlist (a typo there is a misconfiguration, not a new tier to trust).
…known Review fix for PR oratis#20: the segmentPlan/maxEnd bounds check silently skipped whenever cc.duration was unset, so a segmentPlan reaching well past the actual (unknown) video length sailed through validation to fail — or silently truncate — only once it hit the worker. Now a caller-supplied segmentPlan on a CompetitorCreative with no known duration is rejected up front with a 400 telling them to re-ingest with duration first. The existing maxEnd-vs-duration check is unchanged for rows where duration IS known.
Review fix for PR oratis#20 (owner-requested, minimal change to review/{page,client}.tsx): page.tsx safe-parses Creative.tags (JSON, defensive try/catch since tags is free-form across sources) and passes tier/containsCompetitorFootage/ competitorReferenced through to the client. client.tsx renders a hard badge first in the badge row — rose "⚠ competitor footage · t2" when the creative was assembled from competitor-video segments, amber "competitor-referenced · t1" when it was only generated with a competitor video as a style reference — so a reviewer sees the IP-provenance flag before anything else on the card.
…egression) Review fix for PR oratis#20: no e2e coverage existed for claim's t1/t2 refs resolution (CompetitorCreative.assetId → Asset.fileUrl) or the org-isolation fix in bbf02d4. There's no hermetic HTTP path to get a stored Asset — the real Tier-2 "Save video" flow (/api/competitors/media) fetches the source URL over the network — so these insert the Asset row directly via SQL (raw `pg`, not the generated Prisma client: importing `src/generated/prisma/client` from an e2e spec hit a hard CJS/ESM incompatibility, since that file assumes ESM (`import.meta.url`) but Playwright's test transform loads specs as CommonJS — documented in the file header). Two new cases: - t2 claim returns refs pointing at the org-scoped stored Asset. - org isolation (regression, bbf02d4): a CompetitorCreative whose assetId is mutated to point at a *different* org's Asset must never leak that org's fileUrl through claim's refs. Also fixes the pre-existing sampleBatch fixture's `duration: 24` field, which (per src/lib/growth/competitor-import.ts) is actually parsed as a firstSeen~lastSeen date range, not the video length — the real field is `videoDuration`. No prior test depended on CompetitorCreative.duration being set, so this was latent until the new segmentPlan-duration test exercised it.
bbf02d4 to
608ec23
Compare
Fixes pushed — rebased onto the updated
|
This PR does NOT change the shipped default:
REMIX_ENABLED_TIERSdefaults tot0_5only — the borrow-structure-not-pixels policy inremix-brief.ts/competitor-media.tsremains the effective behavior out of the box.What this adds
Tier codes for t1/t2 exist in the remix worker engine's control plane, but stay behind an explicit env opt-in. Nothing about the default request path changes.
/api/creatives/remix)@referencefor the workersegmentPlan)containsCompetitorFootage: trueGating mechanics
REMIX_ENABLED_TIERS(comma-separated, default"t0_5") gates which tiersPOST /api/creatives/remix-jobswill accept. Tier not in the set →403. Tier not a known code →400.segmentPlanwith at least oneaction: "reuse"segment, or400.CompetitorCreative.assetId→Asset.fileUrl) — this reuses your hand-picked, legal-cleared winners gate rather than adding a new one. Missing it →400with a "use the Tier-2 Save video flow first" message.POST /api/worker/remix-jobs/claimnow returnsrefs: [{url, kind:'video'}]for t1/t2 jobs, pointing the worker at that stored Asset. t0_5 is untouched.reviewStatus: 'pending'— the human review gate is identical across tiers, this PR only changes what's allowed to be generated, never what's allowed to ship without review.Why gate via env instead of code-only
This moves the t1/t2 IP boundary from "not possible in code" to "off by default, explicit config choice + auditable via
REMIX_ENABLED_TIERS, gated behind your own Tier-2 hand-picked-video approval step." That gives you (or legal/product) a deliberate lever to pull per deployment, rather than a hardcoded wall — while leaving the out-of-the-box behavior identical to today.Stacked on #19
This branch is based on
feat/remix-worker(#19, still pending review) and includes its commits. Once #19 merges, this diff will automatically shrink to just the tier-gating commit (51c1ce8).Test plan
npm run lint— 0 errors (pre-existing warnings only)npx tsc --noEmit— cleannpx playwright test e2e/remix-jobs.spec.ts— 14/14 passing, including new coverage for: unknown-tier 400, t2-without-segmentPlan 400, t2/t1-without-stored-video 400. (The e2e webServer runs withREMIX_ENABLED_TIERS="t0_5,t1,t2"so these 400 branches — which sit downstream of the 403 gate — are exercised; the refs[] happy path requires a stored competitor Asset and is left to cross-repo integration with the worker.)🤖 Generated with Claude Code