ruvnet/ruflo · critical · DemoLeakError

page-agent bundle still contains a demo/sandbox endpoint (ma

Error message

page-agent bundle still contains a demo/sandbox endpoint (matched /${signature}/) after stripping — refusing to inject to avoid leaking page content to Alibaba's sandbox

What it means

DemoLeakError from buildPageAgentInjection (browser-intent-tools.ts:348), the fail-closed firewall for browser_act page-agent injection. The shipped page-agent IIFE (page-agent.demo.js) auto-POSTs page content to an Alibaba Function Compute sandbox; after the best-effort stripDemoAutoInit() text slice, findDemoLeak() scans for three version-independent signatures — /page-ag-testing/i, /\.fcapp\.run/i, /\bDEMO_MODEL\b/. If ANY survive, injection is refused outright so page content can never silently leak to the demo sandbox (the old strip-only behavior fell open when the marker moved; this cannot).

Source

Thrown at v3/@claude-flow/cli/src/mcp-tools/browser-intent-tools.ts:348

 * the (demo-stripped) IIFE bundle, followed by our own controlled
 * construction + `execute()` call whose settled result lands on a window
 * global we can poll for.
 *
 * KEY-SAFETY: `pageConfig.apiKey` MUST be `PLACEHOLDER_API_KEY` (or another
 * non-secret placeholder) — callers are responsible for routing the real key
 * through `startLocalLLMProxy` first. This function only assembles strings;
 * it does not itself guarantee key safety, so callers MUST NOT pass a real
 * key here (see `browserIntentTools` handler for the enforced call site).
 */
export function buildPageAgentInjection(
  iifeSource: string,
  pageConfig: PageAgentPageConfig,
  task: string,
): string {
  const safeIife = stripDemoAutoInit(iifeSource);
  // Fail-closed: never inject a bundle that still carries the demo endpoint.
  const leak = findDemoLeak(safeIife);
  if (leak) throw new DemoLeakError(leak);
  const cfgJson = JSON.stringify(pageConfig);
  const taskJson = JSON.stringify(task);
  return `${safeIife}
;(function(){
  window.${BROWSER_ACT_RESULT_GLOBAL} = null;
  try {
    if (!window.PageAgent) {
      window.${BROWSER_ACT_RESULT_GLOBAL} = { success: false, error: 'PageAgent not defined after injection' };
      return;
    }
    var agent = new window.PageAgent(${cfgJson});
    window.__ruflo_pageAgent__ = agent;
    agent.execute(${taskJson}).then(function(r){
      window.${BROWSER_ACT_RESULT_GLOBAL} = { success: true, result: r };
    }).catch(function(e){
      window.${BROWSER_ACT_RESULT_GLOBAL} = { success: false, error: String((e && e.message) || e) };
    });
  } catch (e) {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Use a production page-agent build compiled without the demo auto-init tail instead of page-agent.demo.js
  2. If building the bundle yourself, ensure the strings page-ag-testing / .fcapp.run / DEMO_MODEL are absent from the final IIFE — they trip the firewall regardless of intent
  3. After a page-agent upgrade, update stripDemoAutoInit's DEMO_TAIL_MARKER to match the new tail format — until the strip actually removes the tail, the firewall keeps failing closed by design
  4. Do not catch-and-inject the raw source anyway; the refusal is the security guarantee

Example fix

// before: injecting whatever bundle is on disk
const iife = readFileSync('page-agent.demo.js', 'utf8');
const injectable = buildPageAgentInjection(iife, cfg, task); // may throw DemoLeakError

// after: guard with the same firewall first, use a clean build
import { findDemoLeak, stripDemoAutoInit } from './browser-intent-tools';
const iife = readFileSync('page-agent.production.js', 'utf8');
const leak = findDemoLeak(stripDemoAutoInit(iife));
if (leak) throw new Error(`refusing to inject: bundle matches /${leak}/`);
const injectable = buildPageAgentInjection(iife, cfg, task);
Defensive patterns

Strategy: try-catch

Validate before calling

import { findDemoLeak, stripDemoAutoInit } from './browser-intent-tools';
// run the same fail-closed firewall BEFORE assembling the injection
const leak = findDemoLeak(stripDemoAutoInit(iifeSource));
if (leak) {
  throw new Error(`bundle fails demo-leak firewall (/${leak}/): use a build without the demo tail`);
}

Type guard

const isDemoLeakError = (e: unknown): e is import('./browser-intent-tools').DemoLeakError =>
  e instanceof Error && e.name === 'DemoLeakError';

Try / catch

import { DemoLeakError } from './browser-intent-tools';
try {
  const injectable = buildPageAgentInjection(iife, cfg, task);
} catch (e) {
  if (e instanceof DemoLeakError) {
    // fail closed: report and stop — never fall back to injecting the raw bundle
    throw new Error(`page-agent bundle failed the demo-leak firewall: /${e.signature}/; rebuild without the demo auto-init tail`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Upgrading page-agent so the demo auto-init tail moved or was renamed — stripDemoAutoInit's DEMO_TAIL_MARKER no longer matches, the tail (with its fcapp.run endpoint) survives the strip, and the firewall trips. Also any custom/self-built bundle that legitimately contains the literal tokens 'page-ag-testing', '.fcapp.run', or 'DEMO_MODEL' anywhere in the IIFE source.

Common situations: A new page-agent release changes its minified tail format; injecting the unmodified page-agent.demo.js; a locally rebuilt bundle that keeps DEMO_MODEL as a build flag name; bundler config that reorders or duplicates the demo tail so the single indexOf-based slice misses part of it.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/13500b8655c54dbe. Report an issue: GitHub.