← Files HA Interaction AuditARCHIVED FILE

skills/ha-interaction-audit/assets/harness/browserless-runner.js

7.43 KB · Oct 4, 2026 · 12:33 UTC

↓ Download file

export default async ({page: suppliedPage, browser: suppliedBrowser, context: input}) => {
  const automatic = [
    'gate.adapter-ready','gate.transport-probes','gate.unexpected-traffic',
    'gate.mock-contracts','gate.runtime-errors','gate.cleanup'
  ].map(id => ({id,required:true}));
  const ledger = createLedger(input.meta, [...automatic, ...input.plan]);
  const network = [], errors = [], owned = {context:null,page:null};
  const probePaths = new Set(['/__audit_probe__/get','/__audit_probe__/post']);
  let finished = false, deadlineTimer, interruption = null, contextCreationPending = false;
  const record = (id,status,evidence) => {
    if (finished) throw new Error('Pass already ended');
    ledger.record(id,status,evidence);
  };
  const execute = async () => {
    const browser = suppliedBrowser || suppliedPage?.browser();
    const createContext = browser?.createBrowserContext || browser?.createIncognitoBrowserContext;
    if (!createContext) throw new Error('Fresh browser context is unavailable');
    contextCreationPending = true;
    let created;
    try { created = await createContext.call(browser); }
    finally { contextCreationPending = false; }
    if (finished) {
      await created.close();
      throw new Error('Context arrived after pass deadline and was closed');
    }
    owned.context = created;
    const page = owned.page = await owned.context.newPage();
    page.setDefaultTimeout(input.stepTimeoutMs);
    await page.setViewport(input.viewport);
    await page.setCacheEnabled(false);
    if (typeof page.setBypassServiceWorker !== 'function') throw new Error('Service-worker bypass unavailable');
    await page.setBypassServiceWorker(true);
    await page.evaluateOnNewDocument(installBrowserGuards);
    const decide = createResourcePolicy(input.resources);
    const csp = "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; worker-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
    await page.setRequestInterception(true);
    page.on('request', req => {
      const decision = decide(req.method(),req.url());
      let path = '';
      try { path = new URL(req.url()).pathname; } catch {}
      network.push({method:req.method(),path,action:decision.action,probe:probePaths.has(path)});
      const promise = decision.action === 'fulfill' ? req.respond({status:200,
        contentType:decision.contentType,body:decision.body,
        headers:{'Content-Security-Policy':csp,'Cache-Control':'no-store'}}) : req.abort('blockedbyclient');
      promise.catch(() => { if(!finished) errors.push({kind:'interception',message:'Request handling failed'}); });
    });
    page.on('pageerror', e => { if(!finished) errors.push({kind:'runtime',message:String(e.message).slice(0,1000)}); });
    page.on('console', m => {
      if(finished || m.type() !== 'error') return;
      const location = m.location?.() || {};
      let expectedProbe = false;
      try {const url=new URL(location.url);expectedProbe=url.origin===VIRTUAL_ORIGIN&&probePaths.has(url.pathname);} catch {}
      if(!expectedProbe) errors.push({kind:'console',message:m.text().slice(0,1000)});
    });
    await page.goto(VIRTUAL_ORIGIN+input.entry,{waitUntil:'domcontentloaded',timeout:input.stepTimeoutMs});
    await page.waitForFunction(() => window.__HA_AUDIT?.ready === true,{timeout:input.stepTimeoutMs});
    const identity = await page.evaluate(() => ({identity:window.__HA_AUDIT.identity,
      mountedTag:window.__HA_AUDIT.app?.tagName?.toLowerCase()}));
    const parity = identity.mountedTag === input.expectedTag && input.meta.sourceStatus === 'verified';
    record('gate.adapter-ready',parity?'passed':'failed',{expectedTag:input.expectedTag,...identity});
    if(!parity) throw new Error('Mounted target or source verification does not match');
    const probes = await page.evaluate(async () => {
      for(const [method,path] of [['GET','get'],['POST','post']]) {
        try { await fetch('/__audit_probe__/'+path,{method}); } catch {}
      }
      const g=window.__HA_AUDIT_GUARDS;
      g.probe(() => {
        for(const name of ['WebSocket','EventSource','Worker','SharedWorker','RTCPeerConnection']) {
          try {new window[name]('https://ha-audit.invalid/__audit_probe__/transport');} catch {}
        }
        navigator.sendBeacon('/__audit_probe__/beacon','probe');
      });
      return g.events.filter(e=>e.probe);
    });
    const blockedHTTP = network.filter(n=>n.probe&&n.action==='abort');
    const requiredSurfaces=['WebSocket','EventSource','Worker','SharedWorker','RTCPeerConnection','beacon'];
    const contained = ['GET','POST'].every(m=>blockedHTTP.some(n=>n.method===m)) &&
      requiredSurfaces.every(s=>probes.some(p=>p.surface===s&&p.blocked));
    record('gate.transport-probes',contained?'passed':'failed',{http:blockedHTTP,transports:probes});
    if(!contained) throw new Error('Containment probes failed');
    const helpers = createBrowserHelpers(page);
    const advanced={generateSequence,runModelSequence,minimizeFailure,assessTemporalTrace};
    await runSuite({page,helpers,record,input,advanced});
    // Allow terminal events queued by the final browser action to be collected.
    await page.evaluate(() => new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
    const final = await page.evaluate(() => ({
      guards:window.__HA_AUDIT_GUARDS.events.filter(e=>!e.probe),
      intents:window.__HA_AUDIT?.mock?.intents ?? null,
      faults:window.__HA_AUDIT?.faults?.snapshot() ?? null
    }));
    const unexpected = network.filter(n=>!n.probe&&n.action==='abort');
    record('gate.unexpected-traffic',!unexpected.length&&!final.guards.length?'passed':'failed',
      {unexpectedHTTP:unexpected,blockedTransports:final.guards,forwardingPolicy:'no upstream forwarding branch'});
    if(!Array.isArray(final.intents)) record('gate.mock-contracts','blocked',{reason:'Adapter has no mock intent ledger'});
    else record('gate.mock-contracts',final.intents.every(i=>typeof i.mutation==='boolean'&&!['unsupported','pending'].includes(i.status)) &&
      (!final.faults || (!final.faults.pending.length && final.faults.unconsumedRules===0))?'passed':'failed',
      {intents:final.intents,faults:final.faults,mutationIntents:final.intents.filter(i=>i.mutation===true).length});
    record('gate.runtime-errors',errors.length?'failed':'passed',{errors});
  };
  try {
    await Promise.race([execute(),new Promise((_,reject)=>{
      deadlineTimer=setTimeout(()=>reject(new Error('Pass budget exceeded')),input.maxPassMs);
    })]);
  } catch(error) {
    interruption = String(error.message).slice(0,1000);
    // Missing results retain not-run. A timeout must never fabricate partial evidence.
    if(!ledger.hasResult('gate.adapter-ready')) record('gate.adapter-ready','blocked',{reason:interruption});
  } finally {
    clearTimeout(deadlineTimer);
    finished=true;
    let cleanup=contextCreationPending?'unconfirmed':'confirmed', cleanupTimer;
    try {
      if(owned.context) await Promise.race([owned.context.close(),new Promise((_,reject)=>{
        cleanupTimer=setTimeout(()=>reject(new Error('Context close timeout')),2000);
      })]);
    } catch { cleanup='unconfirmed'; }
    finally { clearTimeout(cleanupTimer); }
    ledger.record('gate.cleanup',cleanup==='confirmed'?'passed':'blocked',{cleanup,ownedContextCreated:!!owned.context});
  }
  return {data:ledger.finish({interruption,network,errors}),type:'application/json'};
};

SHA-256: 29d59cf04737975a195af22fe53745d4bbc4116c9969ab51162f4ef3abccf178