FLV

Founder Launch Video Agent

AdaL Cloud · HyperFrames · Cloudflare Pages

idle backend: …

Generate a launch video

🔒Try a prompt-injection idle

Three demonstrations of the agent refusing hostile input. Each runs the same end-to-end loop and surfaces the guardrail notes.

Live agent output

0 events
Render status: idle (sample loaded) open raw ↗

Storyboard

Agent loop trace


    
⚙️Agent config (AdaL Cloud)
Name
…loading…
Model
…loading…
Positive tool allowlist
    Forbidden tools (by absence)
    Bash · Edit · Read · Search · Web · Image · Video · Consult
    System prompt (excerpt)
    
            
    🛡️Guardrails
    
        
    🏗️Architecture
    1 User enters URL + brief in this Cloudflare Pages UI.
    2 BFF creates an AdaL Cloud session + forwards the chat (SSE).
    3 Agent calls its 6 scoped tools (no Bash, no Web).
    4 Composition HTML is validated and rendered by HyperFrames.
    5 Rendered MP4 streams back via /output/<job>.mp4.
    Cloudflare Pages hosts only the static UI. The BFF + renderer run on a normal Node+FFmpeg container (Render.com or AWS App Runner) because Cloudflare Workers cannot run FFmpeg or headless Chrome.
    /* __BFF_INJECTION_POINT__ */ /* ---------------------------------------------------------------------- Founder Launch Video Agent — front-end logic. This file is served by the BFF (proxy/server.py mounts site/ at /). When deployed to Cloudflare Pages, the deploy script injects a `window.__BFF__` global with the absolute backend origin; the JS reads it once at startup and prefixes every /api/* and /output/* request with it. When served from the BFF itself, window.__BFF__ is empty and URLs stay relative. ---------------------------------------------------------------------- */ const BFF = (typeof window !== 'undefined' && window.__BFF__) || ''; const url = (path) => BFF + path; const $ = (s, root=document) => root.querySelector(s); const $$ = (s, root=document) => Array.from(root.querySelectorAll(s)); const paletteRow = $('#palette-row'); const storyboard = $('#storyboard'); const eventLog = $('#event-log'); const videoEl = $('#video-player'); const videoSrc = $('#video-source'); const videoLink = $('#video-link'); const renderStatusEl = $('#render-status'); const statusPill = $('#status-pill'); const eventCount = $('#event-count'); let chosenPalette = 'ember'; let pollTimer = null; let evt = 0; paletteRow.addEventListener('click', (e) => { const b = e.target.closest('.palette-btn'); if (!b) return; chosenPalette = b.dataset.pal; $$('.palette-btn').forEach(x => x.classList.remove('!bg-white/15')); b.classList.add('!bg-white/15'); }); $$('.palette-btn').find?.(b => b?.dataset?.pal === chosenPalette)?.classList?.add('!bg-white/15'); paletteRow.firstElementChild?.classList?.add('!bg-white/15'); // default visual highlight function setStatus(text, kind='idle') { statusPill.textContent = text; statusPill.className = 'pill text-xs px-3 py-1.5 rounded-full ' + (kind==='ok' ? 'badge-ok ' : kind==='err' ? 'badge-err ' : kind==='warn'? 'badge-warn ' : ''); } function setRenderStatus(text, kind='idle') { renderStatusEl.textContent = text; renderStatusEl.className = 'pill text-[10px] px-2 py-0.5 rounded-full ' + (kind==='ok' ? 'badge-ok ' : kind==='err' ? 'badge-err ' : kind==='warn'? 'badge-warn ' : ''); } function appendEvent(line) { evt += 1; eventCount.textContent = `${evt} event${evt===1?'':'s'}`; eventLog.textContent += (line + '\n'); eventLog.scrollTop = eventLog.scrollHeight; } function renderStoryboard(scenes) { storyboard.innerHTML = ''; scenes.forEach((s, idx) => { const card = document.createElement('div'); card.className = 'card p-3'; card.innerHTML = `
    scene ${idx+1}
    ${escapeHTML(s.name)}
    ${escapeHTML(s.intent || '')}
    ${s.duration_s}s · track 1
    `; storyboard.appendChild(card); }); } function escapeHTML(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function setVideoSrc(mp4Url) { // mp4Url is the path returned by the BFF (/output/.mp4 or absolute). // Prefix with the BFF origin when the URL is relative. const full = mp4Url.startsWith('http') ? mp4Url : (BFF + mp4Url); videoSrc.src = full; videoEl.load(); videoLink.href = full; // Auto-play (muted) so judges see motion immediately. videoEl.muted = true; videoEl.play().catch(()=>{ /* user gesture may be required; that's OK */ }); } async function sendChat(brief, url) { appendEvent(`POST /api/chat brief_chars=${brief.length} url=${url || '-'} palette=${chosenPalette}`); const r = await fetch(url('/api/chat'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ brief, url, palette: chosenPalette }), }); if (!r.ok || !r.body) throw new Error('chat: ' + r.status); const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); let idx; while ((idx = buf.indexOf('\n\n')) !== -1) { const chunk = buf.slice(0, idx); buf = buf.slice(idx + 2); const lines = chunk.split('\n'); let event = '', dataLines = []; for (const line of lines) { if (line.startsWith('event:')) event = line.slice(6).trim(); else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim()); } const data = dataLines.join('\n'); handleEvent(event, data); } } } function handleEvent(event, data) { appendEvent(`◦ ${event || 'message'} — ${data.slice(0, 240)}`); // Tool card surfaced live in the agent loop trace. if (event === 'tool.started') { try { const o = JSON.parse(data); const name = o.tool_name || (o.data?.tool_name) || o.name || 'tool'; appendEvent(`tool.started: ${name}`); } catch {} return; } if (event === 'tool.completed') { try { const o = JSON.parse(data); const name = o.tool_name || (o.data?.tool_name) || 'tool'; const output = o.data?.output || o.output || o; const ok = (output && output.status === 'success') || output?.ok === true; appendEvent(`tool.completed: ${name} ${ok ? 'OK' : 'FAIL'}`); if (name === 'render_composition' && output?.mp4_url) { // Optimistically swap to the new MP4; poll /api/render-status for completion. setVideoSrc(output.mp4_url); pollRenderUntilDone(output.mp4_url.split('/').pop().replace('.mp4','')); } } catch {} return; } if (event === 'assistant.delta') { try { const obj = JSON.parse(data); const txt = obj.text || (obj.data?.text || ''); const parsed = JSON.parse(txt); if (parsed.storyboard) renderStoryboard(parsed.storyboard); if (parsed.mp4_url) setVideoSrc(parsed.mp4_url); if (parsed.render_status === 'done') setRenderStatus('render: done', 'ok'); if (parsed.render_status === 'failed') setRenderStatus('render: failed', 'err'); if (parsed.render_status === 'running') setRenderStatus('rendering…', 'warn'); if (parsed.guardrail_notes?.length) { appendEvent(`guardrail: ${parsed.guardrail_notes.length} note(s)`); } } catch (e) { appendEvent('assistant.delta (parse error): ' + e.message); } return; } if (event === 'turn.complete') { setRenderStatus('turn complete', 'ok'); } } async function pollRenderUntilDone(jobId) { setRenderStatus('rendering…', 'warn'); for (let i = 0; i < 60; i++) { await new Promise(r => setTimeout(r, 1000)); try { const r = await fetch(url(`/api/render-status/${encodeURIComponent(jobId)}`)); if (!r.ok) continue; const s = await r.json(); if (s.status === 'done') { setRenderStatus('render: done', 'ok'); return; } if (s.status === 'failed'){ setRenderStatus('render: failed', 'err'); return; } } catch {} } setRenderStatus('render: timeout', 'err'); } $('#btn-generate').addEventListener('click', async () => { const brief = $('#input-brief').value.trim(); const url = $('#input-url').value.trim(); if (!brief) { setStatus('add a brief first', 'warn'); return; } const ok = $('#btn-generate'); ok.disabled = true; setStatus('agent running…', 'warn'); setRenderStatus('queued', 'warn'); eventLog.textContent = ''; evt = 0; eventCount.textContent = '0 events'; try { await sendChat(brief, url); setStatus('done', 'ok'); } catch (e) { appendEvent('error: ' + e.message); setStatus('error: ' + e.message, 'err'); setRenderStatus('failed — see trace', 'err'); } finally { ok.disabled = false; } }); $('#btn-sample').addEventListener('click', async () => { $('#input-brief').value = "Launching Acme — a 60-second lap timer for runners that turns every phone into a personal coach. Brand-safe, energetic, modern."; $('#input-url').value = "https://example.com/"; chosenPalette = 'ember'; renderStoryboard([ {name: 'hook', duration_s: 3, intent: 'bold 1-line promise'}, {name: 'problem', duration_s: 6, intent: 'the pain the product solves'}, {name: 'product_intro', duration_s: 8, intent: 'name + tagline + visual'}, {name: 'cta', duration_s: 3, intent: 'call to action + url'}, ]); setVideoSrc('/output/sample.mp4'); setRenderStatus('sample loaded', 'ok'); setStatus('sample loaded — click Generate to run live', 'ok'); }); $$('.injection-btn').forEach(b => b.addEventListener('click', async () => { const n = b.dataset.n; const guardStatus = $('#guard-status'); guardStatus.textContent = 'running'; guardStatus.className = 'pill text-[10px] px-2 py-0.5 rounded-full badge-warn'; appendEvent(`security: firing injection test ${n}`); try { const r = await fetch(url(`/api/injection-test?n=${n}`), { method: 'POST' }); const j = await r.json(); const wrap = document.createElement('div'); wrap.className = 'card p-3'; wrap.innerHTML = `
    ${escapeHTML(j.test_label)}
    Expected rebuttal: ${j.expected_rebuttal?.map(escapeHTML).join('; ')}
    guardrail_notes
    Agent still produced a render → see ${j.mp4_url||'/output/sample.mp4'}
    `; $('#injection-results').prepend(wrap); guardStatus.textContent = 'ok — agent refused'; guardStatus.className = 'pill text-[10px] px-2 py-0.5 rounded-full badge-ok'; appendEvent(`security: injection test ${n} refused (ok)`); } catch (e) { appendEvent('security error: ' + e.message); guardStatus.textContent = 'error'; guardStatus.className = 'pill text-[10px] px-2 py-0.5 rounded-full badge-err'; } })); // Load agent config + sample storyboard on first paint (async () => { // Make the initial