Imagina navegar por sitios de documentación o tutoriales de código y nunca sentirte solo. En lugar de recorrerlo todo por tu cuenta, tienes un asistente de IA que te sigue de página en página. No está ligado a una sola página web. Siempre está ahí contigo, listo para ayudar. Puede ejecutar de forma segura el código que encuentras en línea, ofrecer explicaciones y brindarte información en el momento que la necesitas.
Entonces, ¿cómo haces realidad esta experiencia? Creando una extensión de navegador. Esta extensión incluirá un agente de IA con el que el usuario pueda chatear, y el agente tendrá acceso a una máquina sandbox segura donde pueda ejecutar código y realizar otras operaciones de forma segura.
Para crear este sistema, construiremos una extensión de Chrome, usaremos un modelo de Novita capaz de usar herramientas e integraremos el Sandbox de Novita como el entorno de ejecución seguro para el agente. En este artículo, recorreremos el proceso completo de su construcción.

Al final de este tutorial, aprenderás:
- Cómo construir una extensión de Chrome que se integra con un agente de IA
- Cómo utilizar los LLM agentivos de Novita
- Cómo configurar Novita Sandbox como un entorno seguro para el agente del navegador
- Cómo hacer que la extensión se comunique en tiempo real con el agente
Un Sandbox: La única herramienta que necesitas
La extensión de Chrome que estamos construyendo depende de un agente de IA para ayudar al usuario. Dado que esta extensión está pensada para funcionar como asistente de codificación, el agente necesita la capacidad de ejecutar código, crear archivos, inspeccionar resultados y realizar todas las tareas típicas que un desarrollador podría hacer. Podrías esperar que requiriera una larga lista de herramientas para lograrlo, pero en realidad solo necesita una: un sandbox.
Un sandbox le da al agente acceso a un entorno Linux donde puede ejecutar comandos, crear y modificar archivos, y llevar a cabo cualquier operación que normalmente harías en una terminal. Para este proyecto, usaremos el Novita Sandbox.
Para configurarlo, primero instala el paquete Novita Sandbox:
pip install novita-sandbox
A continuación, establece la variable de entorno NOVITA_API_KEY con tu clave de API. Una vez hecho esto, puedes crear y usar un sandbox de la siguiente manera:
from novita_sandbox.code_interpreter import Sandbox
sandbox = Sandbox.create()
result = sandbox.commands.run('ls -l')
print(result)
sandbox.kill()
Este fragmento crea un sandbox, ejecuta el comando ls -l, imprime la salida y luego apaga el sandbox. Este flujo de trabajo simple es la base de cómo nuestro asistente de navegador aprovechará el sandbox para ayudar a los usuarios.
Ahora apliquemos este principio a la extensión completa.
Arquitectura del agente asistente del navegador
La arquitectura de este proyecto sigue un modelo cliente-servidor. La extensión de Chrome actúa como cliente, mientras que un servidor back-end dedicado aloja tanto al agente de IA como al entorno sandbox.
La extensión se comunica con el servidor a través de una conexión WebSocket. Esto permite mensajería bidireccional en tiempo real, de modo que las solicitudes del usuario y las respuestas del agente fluyan instantáneamente sin demoras notables. El servidor, a su vez, se comunica con las APIs de Novita que incluyen el endpoint del modelo y el servicio sandbox.
Juntos, la extensión y el back-end forman un asistente de navegador inteligente capaz de ejecutar código de forma segura, procesar información rápidamente y proporcionar explicaciones útiles directamente en la experiencia de navegación del usuario.
Construyendo la extensión
Ahora que entendemos la arquitectura general, podemos comenzar a implementar la extensión en sí. Empezaremos con el servidor del plugin.
El servidor de la extensión
El servidor de la extensión es un servicio WebSocket simple con un único endpoint /ws. Este endpoint recibe mensajes del usuario y devuelve las respuestas del LLM en tiempo real. También maneja las llamadas a herramientas invocando el sandbox cada vez que el agente necesita ejecutar código o realizar una operación.
Dependencias
El servidor depende de tres bibliotecas principales:
- FastAPI: El framework HTTP que proporciona la implementación de WebSocket
- OpenAI: El SDK utilizado para comunicarse con los modelos de Novita
- Novita Sandbox: El entorno seguro donde se ejecuta el código de manera segura
Instálalos con:
pip install novita-sandbox "fastapi[standard]" openai
Establece tu clave de API de Novita como variable de entorno:
export NOVITA_API_KEY = sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Escribiendo el código del servidor
Comienza importando los módulos requeridos:
import os
import json
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from novita_sandbox.code_interpreter import Sandbox
from openai import OpenAI
Inicializando el cliente LLM
A continuación, crea un cliente de OpenAI que apunte a la API de Novita. En este ejemplo usamos el modelo llama-3.3-70b-instruct, pero cualquier modelo de Novita que soporte llamadas a herramientas funcionará.
client = OpenAI(
base_url="https://api.novita.ai/openai",
api_key=os.environ["NOVITA_API_KEY"],
)
model = "meta-llama/llama-3.3-70b-instruct"
Definiendo el esquema de herramientas
El agente usará cuatro herramientas, cada una interactuando con el sandbox:
- read_file: Lee el contenido de un archivo
- write_file: Crea y escribe en un solo archivo
- write_files: Crea y escribe en múltiples archivos
- run_commands: Ejecuta comandos de shell dentro del sandbox
Aquí está el esquema completo de herramientas:
tools = [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read contents of a file inside the sandbox",
"parameters": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
},
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Write a single file inside the sandbox",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string"},
"data": {"type": "string"},
},
"required": ["path", "data"],
},
},
},
{
"type": "function",
"function": {
"name": "write_files",
"description": "Write multiple files inside the sandbox",
"parameters": {
"type": "object",
"properties": {
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": {"type": "string"},
"data": {"type": "string"},
},
"required": ["path", "data"],
},
}
},
"required": ["files"],
},
},
},
{
"type": "function",
"function": {
"name": "run_commands",
"description": "Run a shell command inside the sandbox working directory",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string"},
},
"required": ["command"],
},
},
},
]
Configurando el servidor HTTP
Configura FastAPI y habilita CORS para que la extensión de Chrome pueda hacer solicitudes al servidor.
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
Implementando las herramientas
Define una función manejadora que implemente cada herramienta y enrute las llamadas a herramientas del agente hacia el sandbox:
def make_tool_handlers(sandbox):
def read_file(path: str):
print(f"[LOG] read_file called with path: {path}")
try:
content = sandbox.files.read(path)
print(f"[LOG] read_file result: {content}")
return content
except Exception as e:
return f"Error reading file: {e}"
def write_file(path: str, data: str):
print(f"[LOG] write_file called with path: {path}")
try:
sandbox.files.write(path, data)
return f"File created successfully at {path}"
except Exception as e:
return f"Error writing file: {e}"
def write_files(files: list):
print(f"[LOG] write_files called with {len(files)} files")
try:
sandbox.files.write_files(files)
return f"{len(files)} file(s) created successfully"
except Exception as e:
return f"Error writing multiple files: {e}"
def run_commands(command: str):
print(f"[LOG] run_commands called with command: {command}")
try:
result = sandbox.commands.run(command)
return result.stdout
except Exception as e:
return f"Error running command: {e}"
return {
"read_file": read_file,
"write_file": write_file,
"write_files": write_files,
"run_commands": run_commands,
}
Creando el endpoint WebSocket
Ahora implementemos el endpoint WebSocket. Cuando el usuario se conecta al endpoint, se crea una nueva instancia de sandbox.
Toda la comunicación entre el usuario y el agente fluye a través de esta conexión. Si el usuario le pide al agente que use una herramienta, el agente selecciona y ejecuta la herramienta adecuada a través de la función manejadora.
Cuando se cierra la conexión, el sandbox se termina.
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
print("\
[WS] Client connected")
# Create sandbox per connection
sandbox = Sandbox.create(timeout=1200)
print("[WS] Sandbox created")
tools_exec = make_tool_handlers(sandbox)
messages = [] # persistent inside this websocket
try:
while True:
data = await ws.receive_text()
print(f"[WS] Received message: {data}")
# Add user message
messages.append({"role": "user", "content": data})
# LLM call
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
)
assistant_msg = response.choices[0].message
messages.append(assistant_msg)
# If LLM wants to call tools
if assistant_msg.tool_calls:
print(f"[WS] Assistant requested {len(assistant_msg.tool_calls)} tool call(s).")
results = []
for tool_call in assistant_msg.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
print(f"[WS] Tool call: {fn_name} args={fn_args}")
if fn_name in tools_exec:
result = tools_exec[fn_name](**fn_args)
else:
result = f"Error: Unknown tool {fn_name}"
results.append(result)
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"content": str(result),
})
# Follow-up model call
follow_up = client.chat.completions.create(
model=model,
messages=messages,
)
final_answer = follow_up.choices[0].message
messages.append(final_answer)
await ws.send_json({
"reply": final_answer.content,
"tool_output": results,
})
else:
# Simple model text output
await ws.send_json({"reply": assistant_msg.content})
except WebSocketDisconnect:
print("[WS] Client disconnected")
finally:
sandbox.kill()
print("[WS] Sandbox terminated")
Ejecutando el servidor
Finalmente, usa Uvicorn para lanzar el servicio:
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Esto completa el componente del servidor.
La extensión
La extensión es la interfaz con la que el usuario interactúa. Consiste en un pequeño conjunto de archivos que trabajan juntos para ejecutarse en cualquier página web. Una vez activada, el usuario puede comunicarse con el servidor de la extensión en tiempo real directamente desde la página que está navegando.
La extensión incluye los siguientes archivos:
- manifest.json: Define la configuración y los permisos de la extensión
- background.js: Contiene la lógica del service worker y maneja las acciones del menú contextual
- content.js: Gestiona las interacciones dentro de la página y muestra el diálogo del asistente
- styles.css: Proporciona el estilo para la ventana del asistente en la página
Cada archivo tiene una responsabilidad distinta, y juntos forman una extensión completa y funcional.
Cómo funciona la extensión
Antes de implementar los archivos, echemos un vistazo rápido a cómo funciona la extensión desde la perspectiva del usuario:

- El usuario hace clic derecho en cualquier página web y aparece el menú contextual.
- Selecciona “Agent Sandbox” del menú.
- La extensión abre el diálogo del asistente en la página.
- El usuario hace clic en Connect para establecer una conexión con el servidor de la extensión.
- Una vez establecida la conexión, el usuario puede empezar a escribir directamente en el cuadro de mensajes.
- Después de escribir su mensaje, simplemente hace clic en Send para enviarlo al servidor.
- Si desea proporcionar al agente contexto de la página, puede hacer clic en Extract para capturar todo el contenido visible de la página web.
- También puede añadir contexto adicional manualmente antes de hacer clic nuevamente en Send.

Ahora que entendemos el flujo de trabajo, comencemos a implementar los archivos de la extensión.
manifest.json
El primer archivo que creamos es manifest.json, que configura los permisos de la extensión, la lógica de fondo y los scripts de contenido.
{
"manifest_version": 3,
"name": "Agent Sandbox",
"version": "1.0",
"description": "Chat with your Novita AI sandbox agent over WebSocket.",
"permissions": [
"contextMenus",
"activeTab",
"scripting"
],
"host_permissions": [
"ws://localhost:8000/*",
"http://localhost:8000/*",
"https://localhost:8000/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_icon": "icon.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": ["styles.css"]
}
]
}
Este archivo le dice a Chrome qué scripts cargar, qué permisos necesitamos y qué archivo actúa como service worker.
background.js
El script de fondo es el service worker que se ejecuta en segundo plano. Es responsable de añadir nuestra extensión al menú contextual de Chrome y de escuchar las interacciones del usuario. Cuando el usuario selecciona nuestra opción de menú, el script de fondo envía un mensaje al script de contenido, que entonces activa el diálogo de la extensión.
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: "ask-assistant",
title: "Agent Sandbox",
contexts: ["all"]
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
chrome.scripting.executeScript(
{
target: { tabId: tab.id },
files: ["content.js"]
},
() => {
chrome.tabs.sendMessage(tab.id, {
type: "OPEN_PANEL"
});
}
);
});
content.js
El script de contenido es responsable de mostrar el diálogo de la extensión dentro de la página web. Cuando recibe un mensaje del script de fondo, abre el diálogo. Este script es JavaScript simple. Gestiona la interfaz de usuario a través de operaciones DOM estándar y utiliza la API WebSocket para comunicarse con el servidor de la extensión.
let socket = null;
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === "OPEN_PANEL") {
openPanel();
}
});
// -------------------------------------------------
// WEBPAGE TEXT EXTRACTOR
// -------------------------------------------------
function extractWebpageContent() {
const cloned = document.cloneNode(true);
cloned.querySelectorAll("script, style, iframe, noscript").forEach(e => e.remove());
let main =
cloned.querySelector("article") ||
cloned.querySelector("main") ||
cloned.querySelector("#content") ||
cloned.body;
const text = Array.from(main.querySelectorAll("h1, h2, h3, p"))
.map(el => el.innerText.trim())
.filter(Boolean)
.join("\
\
");
return text;
}
// -------------------------------------------------
// PANEL UI
// -------------------------------------------------
function openPanel() {
const old = document.getElementById("assistant-box");
if (old) old.remove();
const box = document.createElement("div");
box.id = "assistant-box";
box.innerHTML = `
<div id="assistant-container">
<div id="assistant-header">
<h3>Code Assistant</h3>
<button id="assistant-close">×</button>
</div>
<textarea id="assistant-input" placeholder="Message Agent..."></textarea>
<div class="btn-row">
<button id="connect-btn">Connect</button>
<button id="disconnect-btn">Disconnect</button>
<button id="send-btn">Send</button>
<button id="extract-btn">Extract</button>
</div>
<div id="assistant-result">Not connected.</div>
</div>
`;
document.body.appendChild(box);
document.getElementById("assistant-close").onclick = () => box.remove();
const resultBox = document.getElementById("assistant-result");
const inputBox = document.getElementById("assistant-input");
// -------------------------------------------------
// TEXT SELECTION LISTENER
// -------------------------------------------------
document.addEventListener("mouseup", () => {
const selection = window.getSelection().toString().trim();
if (!selection) return;
// Append selected text to message box
inputBox.value += (inputBox.value ? "\
\
" : "") + selection;
// Scroll text area to bottom
inputBox.scrollTop = inputBox.scrollHeight;
});
// -------------------------------------------------
// CONNECT
// -------------------------------------------------
document.getElementById("connect-btn").onclick = () => {
if (socket && socket.readyState === WebSocket.OPEN) {
resultBox.innerText = "Already connected.";
return;
}
socket = new WebSocket("ws://localhost:8000/ws");
socket.onopen = () => {
resultBox.innerText = "Connected to WebSocket.";
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
resultBox.innerText += "\
\
Assistant:\
" + data.reply;
resultBox.scrollTop = resultBox.scrollHeight;
};
socket.onerror = () => {
resultBox.innerText = "WebSocket error.";
};
socket.onclose = () => {
resultBox.innerText = "Disconnected.";
};
};
// -------------------------------------------------
// DISCONNECT
// -------------------------------------------------
document.getElementById("disconnect-btn").onclick = () => {
if (socket) socket.close();
};
// -------------------------------------------------
// SEND MESSAGE
// -------------------------------------------------
document.getElementById("send-btn").onclick = () => {
const context = inputBox.value;
if (!socket || socket.readyState !== WebSocket.OPEN) {
resultBox.innerText = "Not connected.";
return;
}
socket.send(JSON.stringify({ message: context }));
inputBox.value = "";
resultBox.innerText += "\
\
You:\
" + context;
};
// -------------------------------------------------
// EXTRACT PAGE → ADD TO MESSAGE BOX
// -------------------------------------------------
document.getElementById("extract-btn").onclick = () => {
const extracted = extractWebpageContent();
if (!extracted || extracted.length < 10) {
resultBox.innerText = "Could not extract useful content.";
return;
}
// Add extracted text to input box (not sent automatically)
inputBox.value += (inputBox.value ? "\
\
" : "") + extracted;
// Scroll text area
inputBox.scrollTop = inputBox.scrollHeight;
resultBox.innerText = "📄 Extracted content added to message box.";
};
}
styles.css
Luego usamos el archivo styles.css para dar estilo a la interfaz y controlar cómo aparece la extensión en la página.
#assistant-box {
position: fixed;
top: 10%;
right: 10%;
width: 350px;
background: #000;
border: 1px solid #00ff7f;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,255,127,0.5);
z-index: 999999;
font-family: monospace;
color: #00ff7f;
}
#assistant-container {
padding: 12px;
}
#assistant-header {
display: flex;
justify-content: space-between;
align-items: center;
}
#assistant-close {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 0 5px;
font-weight: bold;
color: #00ff7f;
}
#assistant-container textarea {
width: 100%;
height: 60px;
margin-top: 8px;
background: #0d0d0d;
color: #00ff7f;
border: 1px solid #00ff7f;
border-radius: 4px;
padding: 6px;
resize: vertical;
outline: none;
}
#send-btn {
width: 100%;
margin-top: 10px;
background: #00ff7f;
border: none;
padding: 10px;
color: #000;
font-weight: bold;
cursor: pointer;
border-radius: 4px;
transition: 0.2s;
}
#send-btn:hover {
background: #00e66a;
}
#assistant-box pre {
background: #0a0a0a;
padding: 8px;
border-radius: 4px;
max-height: 120px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
margin-top: 5px;
}
#assistant-result {
margin-top: 12px;
background: #0d0d0d;
padding: 8px;
border-radius: 5px;
white-space: pre-wrap;
max-height: 150px;
overflow: auto;
}
Cómo configurar la extensión de Chrome
Ahora que todos los archivos necesarios están en su lugar, el siguiente paso es cargar la extensión en Chrome. Sigue estos pasos:
- Guarda el código del servidor de la extensión en un archivo Python.
- Instala todas las dependencias y configura las variables de entorno necesarias para el servidor.
- Crea una nueva carpeta en tu máquina llamada code-assistant-extension para el cliente de la extensión.
- Agrega los siguientes archivos dentro de la carpeta:
- manifest.json
- background.js
- content.js
- styles.css
- Abre Chrome y ve a: chrome://extensions/
- Activa el Modo desarrollador en la esquina superior derecha.
- Haz clic en Cargar extensión sin empaquetar.
- Selecciona la carpeta que contiene los archivos de tu extensión.
- La extensión aparecerá en la barra de herramientas.
- Haz clic derecho en cualquier selección de texto en una página web para ver Agent Sandbox en el menú contextual.
Tu extensión está ahora lista para usar.
Repositorio: AI Code Assistant Browser Extension
Conclusión
En este artículo construimos una extensión de Chrome que se conecta a un backend impulsado por Novita Sandbox, permitiendo que un agente de IA ejecute código de forma segura y asista a los usuarios mientras navegan. Este patrón va más allá de la ayuda para codificar; puede potenciar herramientas de aprendizaje interactivas, asistentes de depuración, mejoradores de documentación y más.
La arquitectura es independiente del navegador, lo que significa que el mismo enfoque puede adaptarse a cualquier navegador moderno con cambios mínimos. A partir de aquí puedes ampliar las capacidades del asistente, refinar la interfaz de usuario o agregar nuevas herramientas para el sandbox. Esta base abre la puerta a la creación de compañeros de navegación inteligentes y poderosos.
Novita AI es una plataforma líder en la nube de IA que proporciona a los desarrolladores APIs fáciles de usar e infraestructura GPU asequible y confiable para construir y escalar aplicaciones de IA.
