Forschung · @full-self-browsing

PhantomStream.

DOM-natives Live-Browser-Mirroring. Ein echter Tab als strukturiertes DOM gestreamt – nicht als Pixel.

PhantomStream sendet einen Snapshot mit inline eingebetteten Stilen und anschließend winzige MutationObserver-Diffs, die über stabile Knoten-IDs adressiert werden. Die Ansicht erhält eine live aktualisierte, semantisch adressierbare und aus der Ferne steuerbare Kopie der Seite – mit einem Bruchteil der Bandbreite eines Bildschirmstreams.

npm install @full-self-browsing/phantom-stream
v0.9.9.1Plain JS · ESMNode >=18 MIT
Überblick

Ein Live-Spiegel, der sparsam, exakt und adressierbar ist

Wenn ein AI-Agent einen Browser steuert, benötigt die beaufsichtigende Person eine Live-Ansicht dessen, was der Agent tut. PhantomStream streamt das DOM selbst statt Pixeln. Es erfasst die Seite einmal als Snapshot mit inline eingebetteten Stilen, weist in einer WeakMap stabile Kennungen zu, überwacht die Seite anschließend mit einem MutationObserver und sendet nur kleine Diffs (add, rm, attr, text) – nach Knoten-ID geordnet und im Zeichentakt der Seite gebündelt.

Das Ergebnis ist ein Spiegel, dessen Bandbreite den tatsächlichen Änderungen der Seite statt der Bildrate folgt. Text wird bei jeder Auflösung nativ dargestellt, und die Fernsteuerung spricht reale Elemente über ihre stabilen IDs an, statt Koordinaten zu erraten.

PhantomStream entstand als Meilenstein v0.9.9.1 "Phantom Stream" innerhalb von FSB und ermöglicht dort die Live-Vorschau automatisierter Browsersitzungen in der Übersicht. Dieses Repository macht daraus ein eigenständiges, sofort einsatzbereites Framework, ein SDK, das FSB wieder einbindet, sowie das Arbeits-Repository für eine begleitende Forschungsarbeit.

Beweggründe

Das Problem mit Pixeln

Für eine Live-Ansicht liegen videobasierte Werkzeuge nahe. WebRTC, ein CDP-Screencast oder eine Folge von Bildschirmaufnahmen – sie alle übertragen Pixel. Pixel sind datenintensiv. Jedes Bild verbraucht Bandbreite, unabhängig davon, ob sich die Seite geändert hat. Kodierung und Dekodierung erhöhen die Latenz, und das Ergebnis ist verlustbehaftet und an eine Auflösung gebunden.

Schlimmer noch: Pixel sind undurchsichtig. Die überwachende Person kann weder ermitteln, welches Element der Agent berührt, noch einen Knoten hervorheben, die Ansicht kommentieren oder die Seite zuverlässig fernsteuern. Fernsteuerung über einen Videostream bedeutet, Pixelkoordinaten in einem möglicherweise bereits veralteten Bild anzuklicken – ein versetzter Klick landet daher am falschen Ziel.

Vergleich

Warum DOM-Streaming statt Video

Ein strukturiertes DOM statt einzelner Bilder zu streamen, verändert Wirtschaftlichkeit und Fähigkeiten des Spiegels in jeder Hinsicht, die für die Beaufsichtigung eines Agenten zählt.

Video / Bildschirmaufnahmen
PhantomStream
Bandbreite
Fortlaufende Bilder, unabhängig vom Seiteninhalt
Ein Snapshot, danach winzige Diffs nur bei Seitenänderungen
Latenz
Jedes Bild kodieren, übertragen und anschließend dekodieren
Eine Textänderung ist nur eine kleine JSON-Operation
Wiedergabetreue
Verlustbehaftet und an eine Auflösung gebunden
Exaktes DOM, native Textdarstellung, unabhängig von der Auflösung
Fernsteuerung
Pixelkoordinaten in einem möglicherweise veralteten Bild
Reale Elemente werden über stabile Knoten-IDs adressiert
Inspizierbarkeit
Undurchsichtige Pixel
Der Spiegel ist ein DOM: abfragbar, hervorhebbar und kommentierbar
Systemarchitektur

Eine vierstufige Pipeline

Die Seite wird erfasst, das Hostsystem verpackt jede Nachricht für den Transport, ein Vermittlungsdienst verteilt Nachrichten an Ansichten, und die Ansicht baut sie neu auf und wendet sie an. Die Fernsteuerung durchläuft denselben Weg in umgekehrter Richtung.

Seite

Snapshot: klonen, Stile inline einbetten, Knoten-IDs vergeben, dann rAF-gebündelte Diffs

Hostsystem

LZ-string-Umschlag, Sitzungskennung, Überwachung

Vermittlung

WebSocket-Verteilung, Obergrenze von 1 MiB, Verwerfen bei Rückstau

Ansicht

Sandbox-iframe, Diffs nach ID anwenden, Überlagerungen, passend skalieren

Kernmechanismen

Stabile Knotenidentität

Die Erfassung verwaltet die Identität in einer WeakMap<Element, string> und sendet nodeIds-Begleitdaten mit Snapshots und Hinzufügeoperationen, sodass ein verspäteter Diff stets beim richtigen Element ankommt, ohne die Live-Seite zu verändern.

Gezielte Erfassung berechneter Stile

Etwa 85 CSS-Eigenschaften für die visuelle Wiedergabetreue werden pro Element inline eingebettet statt aller über 300; Standardwerte werden ausgelassen. Dadurch sank die Serialisierung einer YouTube-Seite von 45 Sekunden auf ein interaktives Niveau.

An den Bildaufbau angepasste Diffs

Mutationen werden bei requestAnimationFrame gebündelt und gesendet, sodass sich der Spiegel im selben Takt aktualisiert, in dem die Seite gezeichnet wird.

Sitzungsidentität

Jede Nachricht enthält eine streamSessionId und eine snapshotId. Die Ansicht verwirft veraltete Nachrichten, sodass verspätete Diffs einer vorherigen Seite den Spiegel niemals beschädigen können.

Fähigkeiten

Was sofort verfügbar ist

Der Erfassungskern besteht aus reinem JavaScript und wird als Inhaltsskript, als addInitScript oder als Bookmarklet injiziert – ohne Buildschritt zur Laufzeit. Erfassung, Ansicht und Vermittlung kommunizieren über eine schlanke Transportschnittstelle.

Stabile Knoten-IDs

Diffs und Fernsteuerungsaktionen adressieren Knoten über die WeakMap-Identität, niemals über Koordinaten oder anfällige Selektoren.

Gezielt inline eingebettete Stile

Etwa 85 für die Wiedergabetreue entscheidende CSS-Eigenschaften pro Element; Standardwerte werden ausgelassen, damit auch umfangreiche Seiten interaktiv bleiben.

Diffs im Zeichentakt

Eine kompakte Operation pro tatsächlicher Änderung, per requestAnimationFrame mit der Aktualisierungsrate der Seite gesendet.

Größenbegrenzte Snapshots

Snapshots bleiben unter der Obergrenze der Vermittlung pro Nachricht, indem nicht sichtbare Teilbäume nur an Grenzen vollständiger Elemente ausgelassen werden.

Doppelte Überwachung

Ein Timer auf Erfassungsseite und ein Alarm auf Hostseite stellen einen blockierten Stream unabhängig voneinander wieder her, indem sie das Senden oder einen neuen Snapshot erzwingen.

Rendering in der Sandbox

Die Ansicht wird in einem iframe neu aufgebaut, dessen Sandbox exakt allow-same-origin zulässt (niemals allow-scripts). Hinzu kommen CSP und eine Bereinigung nach dem Parsen.

Datenschutzmaskierung

blockSelector, maskTextSelector sowie Maskierungsfunktionen schwärzen sensible Inhalte, bevor sie die Seite verlassen. Passwörter werden immer maskiert.

Medien per Verweis

Bilder, Video und Audio werden über ihre Quell-URL im Browser der Ansicht geladen, sodass Mediendaten niemals die Vermittlung durchlaufen.

Playwright-Adapter

Binden Sie den Erfassungskern über einen fertigen Adapter in eine Playwright- oder CDP-Seite ein – mit autorisierter Fernsteuerung in Gegenrichtung.

Sicherheit

Der Sicherheitsvertrag für die Einbettung

PhantomStream rendert von Angreifern beeinflussbares HTML bei konstruktionsbedingt deaktivierter Skriptausführung. Die Serialisierung entfernt gefährliche Inhalte ausschließlich aus der Übertragungskopie. Die Live-Seite bleibt stets unangetastet.

  • Die Sandbox verwendet exakt allow-same-origin, niemals allow-scripts
  • on*-Handler, gefährliche URL-Schemata, srcdoc, object/embed werden entfernt
  • Private Texte und Formularwerte werden vor dem Transport auf Erfassungsseite maskiert
  • Im Fehlerfall gesperrter Medienabruf: nur HTTPS, private Adressbereiche ausgeschlossen, no-referrer

Garantien der Sandbox

iframe sandbox = allow-same-origin ausschließlich
CSP-Meta-Tag + Bereinigung nach dem Parsen
Erfassungsseitige Bereinigung des Übertragungsklons
Prüfung auf Zeichenkettenebene vor dem Parsen von srcdoc
Dokumentweite no-referrer-Richtlinie
Schnellstart

Erfassen, spiegeln, vermitteln

PhantomStream stellt für jede Stufe einen eigenen Unterpfad bereit. Verbinden Sie eine Erfassung im Seitenkontext, eine Ansicht im entfernten Kontext und einen Vermittlungsdienst unter Node, um Nachrichten zwischen ihnen zu verteilen.

capture.js
import { createCapture } from '@full-self-browsing/phantom-stream/capture';
import { createWebSocketTransport } from '@full-self-browsing/phantom-stream/transport/websocket';

const transport = createWebSocketTransport({
  url: 'wss://relay.example.com/ws?room=ROOM&role=source',
  role: 'source'
});

const capture = createCapture({
  transport,
  skipElement: (el) => el.id === 'my-own-overlay' // eigene Benutzeroberfläche ausschließen
});

capture.start(); // einmal einen Snapshot erstellen, dann Diffs streamen
import { createViewer } from '@full-self-browsing/phantom-stream/renderer';
import { createWebSocketTransport } from '@full-self-browsing/phantom-stream/transport/websocket';

const transport = createWebSocketTransport({
  url: 'wss://relay.example.com/ws?room=ROOM&role=viewer',
  role: 'viewer'
});

const viewer = createViewer({
  container: document.getElementById('mirror'),
  transport
});

viewer.on('state', (e) => console.log('Viewer ist', e.state));
// connecting | live | stale | disconnected
import http from 'node:http';
import { createRelay, createWebSocketRelayBackend } from '@full-self-browsing/phantom-stream/relay';

const relay = createRelay();   // 1-MiB-Limit pro Nachricht, bei Backpressure verwerfen
const server = http.createServer();
createWebSocketRelayBackend({ server, relay, path: '/ws' });
server.listen(8787);

// Clients verbinden sich über ?room=<id>&role=source|viewer

Alternativ können Sie es direkt aus dem Repository ausführen: npm run demo spiegelt einen Quell-Tab in einen Ansichts-Tab, und npm run demo:playwright steuert eine Seite, während eine Ansicht sie spiegelt.

Referenz

Dokumentation

Die ausführlichere Darstellung befindet sich neben dem Quellcode. Jedes Dokument ist eigenständig.