Como Criar uma Extensão para Chrome Usando o Novita AI Sandbox

Como Criar uma Extensão para Chrome Usando o Novita AI Sandbox

Imagine navegar por sites de documentação ou tutoriais de programação e nunca se sentir sozinho. Em vez de navegar por tudo sozinho, você tem um assistente de IA que acompanha você de página em página. Ele não está vinculado a nenhuma página da web específica. Está sempre ao seu lado, pronto para ajudar. Ele pode executar com segurança códigos que você encontra online, oferecer explicações e dar insights no exato momento em que você precisar.

Então, como dar vida a essa experiência? Criando uma extensão de navegador. Essa extensão incluirá um agente de IA com o qual o usuário pode conversar, e o agente terá acesso a uma máquina sandbox segura onde pode executar códigos e realizar outras operações com segurança. Para criar esse sistema, vamos construir uma extensão para Chrome, usar um modelo Novita capaz de usar ferramentas e integrar o Novita Sandbox como ambiente de execução seguro para o agente. Neste artigo, vamos percorrer todo o processo de construção dela.

captura de tela do assistente de código

Ao final deste tutorial, você vai aprender:

  • Como criar uma extensão para Chrome que se integra a um agente de IA
  • Como utilizar os LLMs Agentic da Novita
  • Como configurar o Novita Sandbox como um ambiente seguro para o agente de navegador
  • Como fazer com que a extensão se comunique em tempo real com o agente

Uma Sandbox: A Única Ferramenta que Você Precisa

A extensão para Chrome que estamos construindo depende de um agente de IA para auxiliar o usuário. Como essa extensão deve funcionar como um assistente de programação, o agente precisa ter a capacidade de executar códigos, criar arquivos, inspecionar saídas e realizar todas as tarefas típicas que um desenvolvedor pode fazer. Você pode esperar que seja necessária uma longa lista de ferramentas para tornar isso possível, mas na realidade, ela precisa de apenas uma: uma sandbox.

Uma sandbox dá ao agente acesso a um ambiente Linux onde ele pode executar comandos, criar e modificar arquivos e realizar qualquer operação que você normalmente executaria em um terminal. Para este projeto, usaremos o Novita Sandbox.

Para configurá-lo, primeiro instale o pacote Novita Sandbox:

pip install novita-sandbox

Em seguida, defina a variável de ambiente NOVITA_API_KEY com sua chave de API. Depois de fazer isso, você pode criar e usar uma sandbox da seguinte forma:

from novita_sandbox.code_interpreter import Sandbox

sandbox = Sandbox.create()

result = sandbox.commands.run('ls -l')
print(result)

sandbox.kill()

Esse snippet cria uma sandbox, executa o comando ls -l, imprime a saída e depois encerra a sandbox. Esse fluxo de trabalho simples é a base de como nosso assistente de navegador usará a sandbox para ajudar os usuários.

Agora vamos aplicar esse princípio à extensão completa.

Arquitetura do Agente Assistente de Navegador

A arquitetura deste projeto segue um modelo cliente-servidor. A extensão para Chrome atua como cliente, enquanto um servidor backend dedicado hospeda tanto o agente de IA quanto o ambiente sandbox. A extensão se comunica com o servidor por meio de uma conexão WebSocket. Isso permite mensagens bidirecionais em tempo real, para que as solicitações do usuário e as respostas do agente fluam instantaneamente, sem atraso perceptível. Por sua vez, o servidor se comunica com as APIs da Novita, que incluem o endpoint do modelo e o serviço de sandbox. Juntos, a extensão e o backend formam um assistente de navegador inteligente, capaz de executar códigos com segurança, processar informações rapidamente e fornecer explicações úteis diretamente na experiência de navegação do usuário.

Construindo a Extensão

Agora que entendemos a arquitetura geral, podemos começar a implementar a própria extensão. Vamos começar pelo servidor do plugin.

O Servidor da Extensão

O servidor da extensão é um serviço WebSocket simples com um único endpoint /ws. Esse endpoint recebe mensagens do usuário e retorna as respostas do LLM em tempo real. Ele também lida com chamadas de ferramentas, invocando a sandbox sempre que o agente precisa executar um código ou realizar uma operação.

Dependências

O servidor depende de três bibliotecas principais:

  • FastAPI: O framework HTTP que fornece a implementação do WebSocket
  • OpenAI: O SDK usado para se comunicar com os modelos da Novita
  • Novita Sandbox: O ambiente seguro onde o código é executado com segurança

Instale-as com:

pip install novita-sandbox "fastapi[standard]" openai

Defina sua chave de API da Novita como uma variável de ambiente:

export NOVITA_API_KEY = sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Escrevendo o Código do Servidor

Comece importando os módulos necessários:

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 o Cliente LLM

Em seguida, crie um cliente OpenAI que aponte para a API da Novita. Neste exemplo, usamos o modelo llama-3.3-70b-instruct, mas qualquer modelo da Novita que suporte chamadas de ferramentas 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"

Definindo o Esquema de Ferramentas

O agente usará quatro ferramentas, cada uma interagindo com a sandbox:

  • read_file: Lê o conteúdo de um arquivo
  • write_file: Cria e escreve em um único arquivo
  • write_files: Cria e escreve em múltiplos arquivos
  • run_commands: Executa comandos de shell dentro da sandbox

Aqui está o esquema completo de ferramentas:

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 o Servidor HTTP

Configure o FastAPI e habilite o CORS para que a extensão para Chrome possa fazer solicitações ao servidor.

app = FastAPI()

app.add_middleware(
   CORSMiddleware,
   allow_origins=["*"],
   allow_methods=["*"],
   allow_headers=["*"],
)

Implementando as Ferramentas

Defina uma função de manipulador que implementa cada ferramenta e roteia as chamadas de ferramentas do agente para a 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,
   }

Criando o Endpoint WebSocket

Agora vamos implementar o endpoint WebSocket. Quando o usuário se conecta ao endpoint, uma nova instância de sandbox é criada. Toda a comunicação entre o usuário e o agente flui por essa conexão. Se o usuário pedir para o agente usar uma ferramenta, o agente seleciona e executa a ferramenta apropriada por meio da função de manipulador. Quando a conexão é fechada, a sandbox é encerrada.

@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")

Executando o Servidor

Por fim, use o Uvicorn para iniciar o serviço:

if __name__ == "__main__":
   uvicorn.run(app, host="0.0.0.0", port=8000)

Isso conclui o componente do servidor.

A Extensão

A extensão é a interface com a qual o usuário interage. Ela consiste em um pequeno conjunto de arquivos que funcionam juntos para executar em qualquer página da web. Uma vez ativada, o usuário pode se comunicar com o servidor da extensão em tempo real diretamente da página que está navegando.

A extensão inclui os seguintes arquivos:

  • manifest.json: Define a configuração e as permissões da extensão
  • background.js: Contém a lógica do service worker e lida com as ações do menu de contexto
  • content.js: Gerencia as interações na página e exibe a caixa de diálogo do assistente
  • styles.css: Fornece a estilização para a janela do assistente na página

Cada arquivo tem uma responsabilidade distinta, e juntos eles formam uma extensão completa e funcional.

Como a Extensão Funciona

Antes de implementarmos os arquivos, vamos dar uma olhada rápida em como a extensão funciona da perspectiva do usuário:

captura de tela do plugin de sandbox do agente

  1. O usuário clica com o botão direito em qualquer página da web, e o menu de contexto aparece.
  2. Ele seleciona “Agent Sandbox” no menu.
  3. A extensão abre a caixa de diálogo do assistente na página.
  4. O usuário clica em Connect para estabelecer uma conexão com o servidor da extensão.
  5. Assim que a conexão for estabelecida, o usuário pode começar a digitar diretamente na caixa de mensagens.
  6. Depois de digitar sua mensagem, ele basta clicar em Send para enviá-la ao servidor.
  7. Se ele quiser fornecer contexto da página para o agente, pode clicar em Extract para capturar todo o conteúdo visível da página da web.
  8. Ele também pode adicionar contexto extra manualmente antes de clicar em Send novamente.

captura de tela do assistente de código

Agora que entendemos o fluxo de trabalho, vamos começar a implementar os arquivos da extensão.

manifest.json

O primeiro arquivo que criamos é o manifest.json, que configura as permissões, a lógica de segundo plano e os scripts de conteúdo da extensão.

{
 "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"]
   }
 ]
}

Esse arquivo informa ao Chrome quais scripts carregar, quais permissões precisamos e qual arquivo atua como service worker.

background.js

O script de segundo plano é o service worker que executa nos bastidores. Ele é responsável por adicionar nossa extensão ao menu de contexto do Chrome e por escutar as interações do usuário. Quando o usuário seleciona nossa opção de menu, o script de segundo plano envia uma mensagem para o script de conteúdo, que então ativa a caixa de diálogo da extensão.

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

O script de conteúdo é responsável por exibir a caixa de diálogo da extensão dentro da página da web. Quando ele recebe uma mensagem do script de segundo plano, abre a caixa de diálogo. Este script é JavaScript simples. Ele gerencia a interface por meio de operações DOM padrão e usa a API WebSocket para se comunicar com o servidor da extensão.

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

Depois usamos o arquivo styles.css para estilizar a interface e controlar como a extensão aparece na 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;

}

Como Configurar a Extensão para Chrome

Agora que todos os arquivos necessários estão no lugar, o próximo passo é carregar a extensão no Chrome. Siga estas etapas:

  1. Salve o código do servidor da extensão em um arquivo Python.
  2. Instale todas as dependências e configure as variáveis de ambiente necessárias para o servidor.
  3. Crie uma nova pasta em sua máquina chamada code-assistant-extension para o cliente da extensão.
  4. Adicione os seguintes arquivos na pasta:
    • manifest.json
    • background.js
    • content.js
    • styles.css
  5. Abra o Chrome e acesse: chrome://extensions/
  6. Habilite o Modo de desenvolvedor no canto superior direito.
  7. Clique em Carregar sem compactação.
  8. Selecione a pasta que contém os arquivos da sua extensão.
  9. A extensão aparecerá na barra de ferramentas.
  10. Clique com o botão direito em qualquer seleção de texto em uma página da web para ver o Agent Sandbox no menu de contexto.

Sua extensão agora está pronta para uso.

Repositório - Extensão de Navegador Assistente de Código IA

Conclusão

Neste artigo, construímos uma extensão para Chrome que se conecta a um backend alimentado pelo Novita Sandbox, permitindo que um agente de IA execute códigos com segurança e auxilie os usuários enquanto eles navegam. Esse padrão vai além da ajuda com programação; ele pode alimentar ferramentas de aprendizado interativas, assistentes de depuração, aprimoradores de documentação e muito mais.

A arquitetura é agnóstica de navegador, o que significa que a mesma abordagem pode ser adaptada para qualquer navegador moderno com alterações mínimas. A partir daqui, você pode estender as capacidades do assistente, refinar a interface ou adicionar novas ferramentas de sandbox. Essa base abre as portas para a criação de companheiros de navegador inteligentes e poderosos.

A Novita AI é uma plataforma de nuvem de IA líder que fornece aos desenvolvedores APIs fáceis de usar e infraestrutura de GPU acessível e confiável para construir e escalar aplicações de IA.