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
- Use a production page-agent build compiled without the demo auto-init tail instead of page-agent.demo.js
- 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
- 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
- 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
- Ship a production page-agent build without the demo auto-init tail; never inject page-agent.demo.js as-is
- After every page-agent upgrade, verify stripDemoAutoInit's marker still matches the new tail — the firewall will keep refusing until it does
- Run findDemoLeak() over the bundle in CI so a leak is caught before runtime, not at injection time
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
- browser/eval: script contains disallowed pattern: ${pattern.
- Key contains disallowed characters
- Namespace contains disallowed characters
- unknown game "${key}". Known: ${Object.keys(GAMES).join(', '
- unknown strategy "${name}". Available: ${roster.map((r) => r
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/13500b8655c54dbe.
Report an issue: GitHub.