Imaginez que vous naviguez sur des sites de documentation ou des tutoriels de code et que vous ne vous sentez jamais seul. Au lieu de tout faire vous-même, vous avez un assistant IA qui vous suit de page en page. Il n’est lié à aucune page web en particulier. Il est toujours à vos côtés, prêt à vous aider. Il peut exécuter en toute sécurité le code que vous rencontrez en ligne, vous fournir des explications et vous donner des informations exactement au moment où vous en avez besoin.
Comment donner vie à cette expérience ? En créant une extension de navigateur. Cette extension intégrera un agent IA avec lequel l’utilisateur pourra discuter, et cet agent aura accès à une machine bac à sable sécurisée où il pourra exécuter du code et effectuer d’autres opérations en toute sécurité.
Pour créer ce système, nous allons développer une extension Chrome, utiliser un modèle Novita capable d’utiliser des outils, et intégrer le Novita Sandbox comme environnement d’exécution sécurisé pour l’agent. Dans cet article, nous allons vous guider à travers l’ensemble du processus de création.

À la fin de ce tutoriel, vous saurez :
- Comment créer une extension Chrome intégrant un agent IA
- Comment utiliser les LLM agentiques de Novita
- Comment configurer Novita Sandbox comme environnement sécurisé pour l’agent de navigateur
- Comment faire en sorte que l’extension communique en temps réel avec l’agent
Un bac à sable : le seul outil dont vous avez besoin
L’extension Chrome que nous développons repose sur un agent IA pour assister l’utilisateur. Comme cette extension est conçue pour fonctionner comme un assistant de codage, l’agent doit être capable d’exécuter du code, de créer des fichiers, d’inspecter les sorties et d’effectuer toutes les tâches typiques qu’un développeur pourrait réaliser. Vous pourriez penser qu’une longue liste d’outils est nécessaire pour y parvenir, mais en réalité, un seul suffit : un bac à sable.
Un bac à sable donne à l’agent accès à un environnement Linux où il peut exécuter des commandes, créer et modifier des fichiers, et effectuer toute opération que vous réaliseriez normalement dans un terminal. Pour ce projet, nous utiliserons le Novita Sandbox.
Pour le configurer, installez d’abord le package Novita Sandbox :
pip install novita-sandbox
Ensuite, définissez la variable d’environnement NOVITA_API_KEY avec votre clé API. Une fois cela fait, vous pouvez créer et utiliser un bac à sable comme ceci :
from novita_sandbox.code_interpreter import Sandbox
sandbox = Sandbox.create()
result = sandbox.commands.run('ls -l')
print(result)
sandbox.kill()
Cet extrait de code crée un bac à sable, exécute la commande ls -l, affiche le résultat, puis arrête le bac à sable. Ce flux de travail simple est la base sur laquelle notre assistant de navigateur s’appuiera pour utiliser le bac à sable et aider les utilisateurs.
Maintenant, appliquons ce principe à l’extension complète.
Architecture de l’agent assistant de navigateur
L’architecture de ce projet suit un modèle client-serveur. L’extension Chrome fait office de client, tandis qu’un serveur backend dédié héberge à la fois l’agent IA et l’environnement bac à sable.
L’extension communique avec le serveur via une connexion WebSocket. Cela permet une messagerie bidirectionnelle en temps réel, de sorte que les demandes de l’utilisateur et les réponses de l’agent s’échangent instantanément, sans délai perceptible. Le serveur, à son tour, communique avec les API de Novita, qui incluent le point de terminaison du modèle et le service de bac à sable.
Ensemble, l’extension et le backend forment un assistant de navigateur intelligent capable d’exécuter du code en toute sécurité, de traiter des informations rapidement et de fournir des explications utiles directement dans l’expérience de navigation de l’utilisateur.
Création de l’extension
Maintenant que nous comprenons l’architecture globale, nous pouvons commencer à implémenter l’extension elle-même. Nous allons d’abord nous occuper du serveur de l’extension.
Le serveur de l’extension
Le serveur de l’extension est un service WebSocket simple avec un seul point de terminaison /ws. Ce point de terminaison reçoit les messages de l’utilisateur et renvoie les réponses du LLM en temps réel. Il gère également les appels d’outils en invoquant le bac à sable chaque fois que l’agent doit exécuter du code ou effectuer une opération.
Dépendances
Le serveur repose sur trois bibliothèques principales :
- FastAPI : le framework HTTP qui fournit l’implémentation WebSocket
- OpenAI : le SDK utilisé pour communiquer avec les modèles Novita
- Novita Sandbox : l’environnement sécurisé où le code est exécuté en toute sécurité
Installez-les avec :
pip install novita-sandbox "fastapi[standard]" openai
Définissez votre clé API Novita comme variable d’environnement :
export NOVITA_API_KEY = sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Écriture du code du serveur
Commencez par importer les modules requis :
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
Initialisation du client LLM
Ensuite, créez un client OpenAI qui pointe vers l’API de Novita. Dans cet exemple, nous utilisons le modèle llama-3.3-70b-instruct, mais tout modèle Novita qui prend en charge l’appel d’outils fonctionnera.
client = OpenAI(
base_url="https://api.novita.ai/openai",
api_key=os.environ["NOVITA_API_KEY"],
)
model = "meta-llama/llama-3.3-70b-instruct"
Définition du schéma d’outils
L’agent utilisera quatre outils, chacun interagissant avec le bac à sable :
- read_file : lit le contenu d’un fichier
- write_file : crée et écrit dans un seul fichier
- write_files : crée et écrit dans plusieurs fichiers
- run_commands : exécute des commandes shell dans le bac à sable
Voici le schéma d’outils complet :
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"],
},
},
},
]
Configuration du serveur HTTP
Configurez FastAPI et activez CORS pour que l’extension Chrome puisse envoyer des requêtes au serveur.
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
Implémentation des outils
Définissez une fonction de gestionnaire qui implémente chaque outil et achemine les appels d’outils de l’agent vers le bac à sable :
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,
}
Création du point de terminaison WebSocket
Implémentons maintenant le point de terminaison WebSocket. Lorsque l’utilisateur se connecte au point de terminaison, une nouvelle instance de bac à sable est créée. Toute la communication entre l’utilisateur et l’agent passe par cette connexion. Si l’utilisateur demande à l’agent d’utiliser un outil, l’agent sélectionne et exécute l’outil approprié via la fonction de gestionnaire. Lorsque la connexion est fermée, le bac à sable est arrêté.
@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")
Lancement du serveur
Enfin, utilisez Uvicorn pour lancer le service :
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Cela termine le composant serveur.
L’extension
L’extension est l’interface avec laquelle l’utilisateur interagit. Elle est constituée d’un petit ensemble de fichiers qui fonctionnent ensemble pour s’exécuter sur n’importe quelle page web. Une fois activée, l’utilisateur peut communiquer avec le serveur de l’extension en temps réel directement depuis la page qu’il consulte.
L’extension comprend les fichiers suivants :
- manifest.json : définit la configuration et les autorisations de l’extension
- background.js : contient la logique du service worker et gère les actions du menu contextuel
- content.js : gère les interactions sur la page et affiche la boîte de dialogue de l’assistant
- styles.css : fournit le style de la fenêtre de l’assistant intégrée à la page
Chaque fichier a une responsabilité distincte, et ensemble ils forment une extension complète et fonctionnelle.
Fonctionnement de l’extension
Avant d’implémenter les fichiers, jetons un coup d’œil rapide au fonctionnement de l’extension du point de vue de l’utilisateur :

- L’utilisateur fait un clic droit sur n’importe quelle page web, et le menu contextuel apparaît.
- Il sélectionne « Agent Sandbox » dans le menu.
- L’extension ouvre la boîte de dialogue de l’assistant sur la page.
- L’utilisateur clique sur Connecter pour établir une connexion avec le serveur de l’extension.
- Une fois la connexion établie, l’utilisateur peut commencer à taper directement dans la zone de message.
- Après avoir saisi son message, il lui suffit de cliquer sur Envoyer pour le transmettre au serveur.
- S’il souhaite fournir à l’agent le contexte de la page, il peut cliquer sur Extraire pour capturer tout le contenu visible de la page web.
- Il peut également ajouter du contexte supplémentaire manuellement avant de cliquer à nouveau sur Envoyer.

Maintenant que nous comprenons le flux de travail, commençons à implémenter les fichiers de l’extension.
manifest.json
Le premier fichier que nous créons est le manifest.json, qui configure les autorisations, la logique d’arrière-plan et les scripts de contenu de l’extension.
{
"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"]
}
]
}
Ce fichier indique à Chrome quels scripts charger, quelles autorisations nous avons besoin, et quel fichier sert de service worker.
background.js
Le script d’arrière-plan est le service worker qui s’exécute en arrière-plan. Il est responsable de l’ajout de notre extension au menu contextuel de Chrome et de l’écoute des interactions utilisateur. Lorsque l’utilisateur sélectionne notre option de menu, le script d’arrière-plan envoie un message au script de contenu, qui active ensuite la boîte de dialogue de l’extension.
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
Le script de contenu est responsable de l’affichage de la boîte de dialogue de l’extension dans la page web. Lorsqu’il reçoit un message du script d’arrière-plan, il ouvre la boîte de dialogue. Ce script est du JavaScript simple. Il gère l’interface utilisateur via des opérations DOM standard et utilise l’API WebSocket pour communiquer avec le serveur de l’extension.
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
Nous utilisons ensuite le fichier styles.css pour styliser l’interface et contrôler l’apparence de l’extension sur la page.
#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;
}
Comment configurer l’extension Chrome
Maintenant que tous les fichiers requis sont en place, l’étape suivante consiste à charger l’extension dans Chrome. Suivez ces étapes :
- Enregistrez le code du serveur de l’extension dans un fichier Python.
- Installez toutes les dépendances et configurez les variables d’environnement nécessaires pour le serveur.
- Créez un nouveau dossier sur votre machine nommé code-assistant-extension pour le client de l’extension.
- Ajoutez les fichiers suivants dans le dossier :
- manifest.json
- background.js
- content.js
- styles.css
- Ouvrez Chrome et accédez à :
chrome://extensions/ - Activez le Mode développeur en haut à droite.
- Cliquez sur Charger l’extension non empaquetée.
- Sélectionnez le dossier contenant les fichiers de votre extension.
- L’extension apparaîtra dans la barre d’outils.
- Faites un clic droit sur n’importe quelle sélection de texte sur une page web pour voir Agent Sandbox dans le menu contextuel.
Votre extension est maintenant prête à être utilisée.
Dépôt - Extension de navigateur Assistant de code IA
Conclusion
Dans cet article, nous avons créé une extension Chrome qui se connecte à un backend alimenté par Novita Sandbox, permettant à un agent IA d’exécuter du code en toute sécurité et d’assister les utilisateurs pendant leur navigation. Ce modèle va au-delà de l’aide au codage ; il peut alimenter des outils d’apprentissage interactifs, des assistants de débogage, des améliorateurs de documentation, et bien plus encore.
Cette architecture est indépendante du navigateur, ce qui signifie que la même approche peut être adaptée à tout navigateur moderne avec des modifications minimales. À partir de là, vous pouvez étendre les capacités de l’assistant, affiner l’interface utilisateur ou ajouter de nouveaux outils de bac à sable. Cette base ouvre la voie à la création de compagnons de navigateur puissants et intelligents.
Novita AI est une plateforme cloud IA leader qui fournit aux développeurs des API faciles à utiliser et une infrastructure GPU abordable et fiable pour créer et mettre à l’échelle des applications IA
