Investigación · @full-self-browsing

PhantomStream.

Espejo en vivo del navegador nativo de DOM. Una pestaña real transmitida como DOM estructurado, no como píxeles.

PhantomStream envía una única instantánea con estilos integrados y, después, pequeños cambios de MutationObserver dirigidos mediante ID de nodos estables. El visor recibe una copia en vivo de la página que se puede direccionar semánticamente y controlar a distancia, con una fracción del ancho de banda de una transmisión de pantalla.

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

Una réplica en vivo económica, exacta y direccionable

Cuando un agente de AI controla un navegador, la persona que lo supervisa necesita ver en directo lo que hace. PhantomStream transmite el propio DOM en lugar de píxeles. Captura la página una vez como una instantánea con estilos integrados, asigna identificadores estables en un WeakMap, luego observa la página con un MutationObserver y envía solo pequeños cambios (add, rm, attr, text) identificados por ID de nodo y agrupados al ritmo de renderizado de la propia página.

La ventaja es una réplica cuyo uso de ancho de banda depende de cuánto cambia realmente la página, no de la frecuencia de fotogramas. El texto se renderiza de forma nativa en cualquier resolución y el control remoto actúa sobre elementos reales mediante sus ID estables, en lugar de adivinar coordenadas.

PhantomStream nació como el hito v0.9.9.1 "Phantom Stream" dentro de FSB, donde permite la vista previa en directo de las sesiones de navegación automatizada del panel. Este repositorio lo convierte en un marco de trabajo independiente y listo para usar, un SDK que FSB vuelve a integrar y el repositorio de trabajo de un artículo de investigación complementario.

Motivación

El problema de los píxeles

Las herramientas más obvias para ofrecer una vista en directo recurren al vídeo. WebRTC, una transmisión de pantalla de CDP o una secuencia de capturas de pantalla envían píxeles. Transmitir píxeles requiere muchos recursos. Cada fotograma consume ancho de banda tanto si la página ha cambiado como si no; la codificación y decodificación añaden latencia, y el resultado tiene pérdidas y queda limitado por la resolución.

Peor aún, los píxeles son opacos. Quien supervisa no puede preguntar qué elemento está manipulando el agente, resaltar un nodo, anotar la vista ni controlar la página de forma fiable. El control remoto sobre un flujo de vídeo implica hacer clic en coordenadas de píxeles de un fotograma que quizá ya esté obsoleto; por eso, un clic desalineado termina en el elemento equivocado.

Comparación

Por qué transmitir el DOM en lugar de vídeo

Transmitir un DOM estructurado en lugar de fotogramas cambia los costes y las capacidades de la réplica en todos los aspectos importantes para supervisar a un agente.

Vídeo / capturas de pantalla
PhantomStream
Ancho de banda
Fotogramas continuos, independientemente del contenido de la página
Una instantánea y después solo pequeños cambios cuando cambia la página
Latencia
Codificar, transmitir y después decodificar cada fotograma
Una mutación de texto es una pequeña operación JSON
Fidelidad
Con pérdidas y limitada por la resolución
DOM exacto, renderizado de texto nativo e independiente de la resolución
Control remoto
Coordenadas de píxeles sobre un fotograma que puede estar obsoleto
Elementos reales direccionados mediante ID de nodo estables
Capacidad de inspección
Píxeles opacos
La réplica es un DOM: se puede consultar, resaltar y anotar
Arquitectura

Un proceso de cuatro etapas

Se captura la página, el servidor encapsula cada mensaje para transportarlo, un relé distribuye los mensajes a los visores y el visor los reconstruye y aplica. El control remoto recorre el mismo camino en sentido inverso.

Página

Instantánea: clonar, integrar estilos, marcar ID de nodos y luego cambios agrupados por rAF

Servidor

Encapsulado con LZ-string, marcado de sesión y mecanismo de vigilancia

Relé

Distribución por WebSocket, límite de 1 MiB y descarte por contrapresión

Visor

iframe aislado, aplicación de cambios por ID, superposiciones y ajuste a escala

Mecanismos principales

Identidad estable de nodos

La captura gestiona la identidad en un WeakMap<Element, string> y emite datos auxiliares nodeIds con las instantáneas y las operaciones add, para que un cambio tardío siempre se aplique al elemento correcto sin modificar la página activa.

Captura selectiva de estilos calculados

Se integran unas 85 propiedades CSS de fidelidad visual por elemento, en lugar de las más de 300, omitiendo los valores predeterminados. Esto redujo una serialización de YouTube que tardaba 45 segundos hasta hacerla interactiva.

Cambios sincronizados con la visualización

Las mutaciones se agrupan y se envían en requestAnimationFrame, de modo que la réplica se actualiza al mismo ritmo que se renderiza la página.

Identidad de sesión

Cada mensaje incluye un streamSessionId y un snapshotId. El visor rechaza los mensajes obsoletos, por lo que los cambios tardíos de una página anterior nunca pueden corromper la réplica.

Capacidades

Todo lo que incluye

El núcleo de captura es JavaScript puro que se inyecta como script de contenido, addInitScript o un marcador ejecutable, sin ningún paso de compilación en tiempo de ejecución. La captura, el visor y el relé se comunican mediante una pequeña interfaz de transporte.

ID de nodo estables

Los cambios y las acciones de control remoto se dirigen a los nodos mediante la identidad de WeakMap, nunca por coordenadas ni selectores frágiles.

Integración selectiva de estilos

Unas 85 propiedades CSS esenciales para la fidelidad por elemento, con omisión de valores predeterminados, que mantienen interactivas las páginas pesadas.

Cambios al ritmo de renderizado

Una operación compacta por cada cambio real, enviada en requestAnimationFrame al ritmo de actualización de la propia página.

Instantáneas con límite de tamaño

Las instantáneas se mantienen por debajo del límite por mensaje del relé descartando subárboles fuera de pantalla sin dividir ningún elemento.

Dos mecanismos de vigilancia

Un temporizador del lado de la captura y una alarma del lado del servidor recuperan de forma independiente un flujo bloqueado al forzar el envío de cambios o una instantánea nueva.

Renderizado aislado

El visor reconstruye dentro de un iframe aislado exactamente con allow-same-origin (nunca allow-scripts), con CSP y una limpieza posterior al análisis.

Enmascaramiento de privacidad

blockSelector, maskTextSelector y las funciones de enmascaramiento ocultan el contenido sensible antes de que salga de la página. Las contraseñas siempre se enmascaran.

Contenido multimedia por referencia

Las imágenes, el vídeo y el audio se cargan desde su URL de origen en el navegador del propio visor, por lo que los bytes multimedia nunca atraviesan el relé.

Adaptador de Playwright

Incorpora el núcleo de captura en una página de Playwright o CDP mediante un adaptador listo para usar, con control remoto inverso autorizado.

Seguridad

El contrato de seguridad del contenido incrustado

PhantomStream renderiza HTML que puede estar controlado por atacantes y desactiva la ejecución de scripts por diseño. La serialización elimina el contenido peligroso únicamente de la copia para transmisión. La página activa nunca se modifica.

  • El aislamiento usa exactamente allow-same-origin, nunca allow-scripts
  • Se eliminan los controladores on*, los esquemas de URL peligrosos, srcdoc y object/embed
  • El texto privado y los valores de formularios se enmascaran durante la captura antes de transportarlos
  • Carga multimedia con bloqueo ante fallos: solo https, bloqueo de rangos privados y no-referrer

Garantías del entorno aislado

iframe sandbox = allow-same-origin únicamente
CSP: etiqueta meta + limpieza posterior al análisis
Saneamiento del clon de transmisión en el lado de captura
Control en la capa de cadenas antes del análisis de srcdoc
Política de no-referrer a nivel de documento
Inicio rápido

Captura, réplica y relé

PhantomStream ofrece una subruta por etapa. Conecta una captura en el contexto de la página, un visor en el contexto remoto y un relé en Node para distribuir mensajes entre ellos.

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' // excluir tu propia UI
});

capture.start(); // tomar una sola instantánea y después transmitir las diferencias
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('el visor está', e.state));
// connecting | live | stale | disconnected
import http from 'node:http';
import { createRelay, createWebSocketRelayBackend } from '@full-self-browsing/phantom-stream/relay';

const relay = createRelay();   // límite de 1 MiB por mensaje; descarte ante contrapresión
const server = http.createServer();
createWebSocketRelayBackend({ server, relay, path: '/ws' });
server.listen(8787);

// los clientes se unen con ?room=<id>&role=source|viewer

También puedes ejecutarlo directamente desde el repositorio: npm run demo replica una pestaña de origen en otra pestaña para visualizarla, y npm run demo:playwright controla una página mientras un visor la replica.

Referencia

Documentación

La documentación detallada se encuentra junto al código fuente. Cada documento es independiente.