diff --git a/Dockerfile.allinone b/Dockerfile.allinone
index e96618adc..a51e31814 100644
--- a/Dockerfile.allinone
+++ b/Dockerfile.allinone
@@ -216,6 +216,10 @@ RUN pip install --no-cache-dir playwright \
&& playwright install chromium \
&& rm -rf /root/.cache/ms-playwright/ffmpeg*
+# Install Microsandbox (optional secure code execution for deep agent).
+# Requires --device /dev/kvm at runtime. Enable via MICROSANDBOX_ENABLED=TRUE.
+RUN curl -sSL https://get.microsandbox.dev | sh || true
+
# Copy backend source
COPY surfsense_backend/ ./
@@ -260,6 +264,11 @@ ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL
ENV NEXT_PUBLIC_ETL_SERVICE=DOCLING
+# Microsandbox (optional - requires --device /dev/kvm and --privileged at runtime)
+ENV MICROSANDBOX_ENABLED=FALSE
+ENV MICROSANDBOX_SERVER_URL=http://localhost:5555
+# MICROSANDBOX_API_KEY is intentionally unset; set at runtime for production.
+
# Electric SQL configuration (ELECTRIC_DATABASE_URL is built dynamically by entrypoint from these values)
ENV ELECTRIC_DB_USER=electric
ENV ELECTRIC_DB_PASSWORD=electric_password
@@ -274,8 +283,8 @@ ENV NEXT_PUBLIC_ELECTRIC_AUTH_MODE=insecure
# Data volume
VOLUME ["/data"]
-# Expose ports (Frontend: 3000, Backend: 8000, Electric: 5133)
-EXPOSE 3000 8000 5133
+# Expose ports (Frontend: 3000, Backend: 8000, Electric: 5133, Microsandbox: 5555)
+EXPOSE 3000 8000 5133 5555
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
diff --git a/docker-compose.yml b/docker-compose.yml
index a94cea2e5..04231ff20 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -65,9 +65,14 @@ services:
- ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password}
- AUTH_TYPE=${AUTH_TYPE:-LOCAL}
- NEXT_FRONTEND_URL=${NEXT_FRONTEND_URL:-http://localhost:3000}
+ # Microsandbox – uncomment when microsandbox service is enabled
+ # - MICROSANDBOX_ENABLED=TRUE
+ # - MICROSANDBOX_SERVER_URL=http://microsandbox:5555
+ # - MICROSANDBOX_API_KEY=${MICROSANDBOX_API_KEY:-}
depends_on:
- db
- redis
+ # - microsandbox
# Run these services separately in production
# celery_worker:
@@ -124,6 +129,42 @@ services:
# - redis
# - celery_worker
+ # ============================================================
+ # Microsandbox (optional - secure code execution for deep agent)
+ # ============================================================
+ # Requires a Linux host with KVM support (/dev/kvm).
+ # To enable:
+ # 1. Uncomment this service
+ # 2. Set MICROSANDBOX_ENABLED=TRUE in surfsense_backend/.env
+ # 3. Run with: docker compose up -d
+ # The first sandbox creation will pull the OCI image (e.g. microsandbox/python),
+ # so the initial run takes a bit longer.
+ #
+ # microsandbox:
+ # image: ubuntu:22.04
+ # ports:
+ # - "${MICROSANDBOX_PORT:-5555}:5555"
+ # volumes:
+ # - microsandbox_data:/root/.microsandbox
+ # privileged: true
+ # devices:
+ # - /dev/kvm:/dev/kvm
+ # entrypoint: ["/bin/bash", "-c"]
+ # command:
+ # - |
+ # set -e
+ # if ! command -v msb &>/dev/null; then
+ # apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
+ # curl -sSL https://get.microsandbox.dev | sh
+ # fi
+ # exec msb server start --dev
+ # restart: unless-stopped
+ # healthcheck:
+ # test: ["CMD", "curl", "-f", "http://localhost:5555/health"]
+ # interval: 10s
+ # timeout: 5s
+ # retries: 5
+
electric:
image: electricsql/electric:latest
ports:
@@ -165,3 +206,4 @@ volumes:
pgadmin_data:
redis_data:
shared_temp:
+ # microsandbox_data:
diff --git a/scripts/docker/entrypoint-allinone.sh b/scripts/docker/entrypoint-allinone.sh
index 4f88b3382..9ca653979 100644
--- a/scripts/docker/entrypoint-allinone.sh
+++ b/scripts/docker/entrypoint-allinone.sh
@@ -42,6 +42,17 @@ if [ -z "$STT_SERVICE" ]; then
echo "✅ Using default STT_SERVICE: local/base"
fi
+# ================================================
+# Microsandbox (optional secure sandbox server)
+# ================================================
+if [ "${MICROSANDBOX_ENABLED:-FALSE}" = "TRUE" ]; then
+ export MICROSANDBOX_AUTOSTART=true
+ echo "✅ Microsandbox enabled (requires --device /dev/kvm)"
+else
+ export MICROSANDBOX_AUTOSTART=false
+ echo "ℹ️ Microsandbox disabled (set MICROSANDBOX_ENABLED=TRUE to enable)"
+fi
+
# ================================================
# Set Electric SQL configuration
# ================================================
@@ -232,6 +243,7 @@ echo " Auth Type: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}"
echo " ETL Service: ${NEXT_PUBLIC_ETL_SERVICE}"
echo " TTS Service: ${TTS_SERVICE}"
echo " STT Service: ${STT_SERVICE}"
+echo " Microsandbox: ${MICROSANDBOX_ENABLED:-FALSE}"
echo "==========================================="
echo ""
diff --git a/scripts/docker/supervisor-allinone.conf b/scripts/docker/supervisor-allinone.conf
index 1a21fcc04..b935737d9 100644
--- a/scripts/docker/supervisor-allinone.conf
+++ b/scripts/docker/supervisor-allinone.conf
@@ -114,8 +114,23 @@ stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment=NODE_ENV="production",PORT="3000",HOSTNAME="0.0.0.0"
+# Microsandbox (secure code execution sandbox server)
+# Autostart is controlled by the entrypoint based on MICROSANDBOX_ENABLED env var.
+# Requires --device /dev/kvm and --privileged when running the container.
+[program:microsandbox]
+command=msb server start --dev
+autostart=%(ENV_MICROSANDBOX_AUTOSTART)s
+autorestart=true
+priority=25
+startsecs=5
+startretries=3
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+
# Process Groups
[group:surfsense]
-programs=postgresql,redis,electric,backend,celery-worker,celery-beat,frontend
+programs=postgresql,redis,electric,backend,celery-worker,celery-beat,frontend,microsandbox
priority=999
diff --git a/surfsense_backend/app/agents/new_chat/chat_deepagent.py b/surfsense_backend/app/agents/new_chat/chat_deepagent.py
index f4af16b78..dbb1d4b4a 100644
--- a/surfsense_backend/app/agents/new_chat/chat_deepagent.py
+++ b/surfsense_backend/app/agents/new_chat/chat_deepagent.py
@@ -10,6 +10,7 @@ from collections.abc import Sequence
from typing import Any
from deepagents import create_deep_agent
+from deepagents.backends.protocol import SandboxBackendProtocol
from langchain_core.language_models import BaseChatModel
from langchain_core.tools import BaseTool
from langgraph.types import Checkpointer
@@ -128,6 +129,7 @@ async def create_surfsense_deep_agent(
additional_tools: Sequence[BaseTool] | None = None,
firecrawl_api_key: str | None = None,
thread_visibility: ChatVisibility | None = None,
+ sandbox_backend: SandboxBackendProtocol | None = None,
):
"""
Create a SurfSense deep agent with configurable tools and prompts.
@@ -167,6 +169,9 @@ async def create_surfsense_deep_agent(
These are always added regardless of enabled/disabled settings.
firecrawl_api_key: Optional Firecrawl API key for premium web scraping.
Falls back to Chromium/Trafilatura if not provided.
+ sandbox_backend: Optional sandbox backend (e.g. MicrosandboxBackend) for
+ secure code execution. When provided, the agent gets an
+ isolated ``execute`` tool for running shell commands.
Returns:
CompiledStateGraph: The configured deep agent
@@ -277,19 +282,26 @@ async def create_surfsense_deep_agent(
)
# Build system prompt based on agent_config
+ _sandbox_enabled = sandbox_backend is not None
if agent_config is not None:
- # Use configurable prompt with settings from NewLLMConfig
system_prompt = build_configurable_system_prompt(
custom_system_instructions=agent_config.system_instructions,
use_default_system_instructions=agent_config.use_default_system_instructions,
citations_enabled=agent_config.citations_enabled,
thread_visibility=thread_visibility,
+ sandbox_enabled=_sandbox_enabled,
)
else:
system_prompt = build_surfsense_system_prompt(
thread_visibility=thread_visibility,
+ sandbox_enabled=_sandbox_enabled,
)
+ # Build optional kwargs for the deep agent
+ deep_agent_kwargs: dict[str, Any] = {}
+ if sandbox_backend is not None:
+ deep_agent_kwargs["backend"] = sandbox_backend
+
# Create the deep agent with system prompt and checkpointer
# Note: TodoListMiddleware (write_todos) is included by default in create_deep_agent
agent = create_deep_agent(
@@ -298,6 +310,7 @@ async def create_surfsense_deep_agent(
system_prompt=system_prompt,
context_schema=SurfSenseContextSchema,
checkpointer=checkpointer,
+ **deep_agent_kwargs,
)
return agent
diff --git a/surfsense_backend/app/agents/new_chat/sandbox.py b/surfsense_backend/app/agents/new_chat/sandbox.py
new file mode 100644
index 000000000..53e71329a
--- /dev/null
+++ b/surfsense_backend/app/agents/new_chat/sandbox.py
@@ -0,0 +1,69 @@
+"""
+Microsandbox provider for SurfSense deep agent.
+
+Manages the lifecycle of sandboxed code execution environments.
+Each conversation thread gets its own isolated sandbox instance.
+"""
+
+import logging
+import os
+
+from deepagents_microsandbox import MicrosandboxBackend, MicrosandboxProvider
+
+logger = logging.getLogger(__name__)
+
+_provider: MicrosandboxProvider | None = None
+
+
+def is_sandbox_enabled() -> bool:
+ return os.environ.get("MICROSANDBOX_ENABLED", "FALSE").upper() == "TRUE"
+
+
+def _get_provider() -> MicrosandboxProvider:
+ global _provider
+ if _provider is None:
+ server_url = os.environ.get(
+ "MICROSANDBOX_SERVER_URL", "http://127.0.0.1:5555"
+ )
+ api_key = os.environ.get("MICROSANDBOX_API_KEY")
+ _provider = MicrosandboxProvider(
+ server_url=server_url,
+ api_key=api_key,
+ namespace="surfsense",
+ )
+ return _provider
+
+
+async def get_or_create_sandbox(thread_id: int | str) -> MicrosandboxBackend:
+ """Get or create a sandbox for a conversation thread.
+
+ Uses the thread_id as the sandbox name so the same sandbox persists
+ across multiple messages within the same conversation.
+
+ Args:
+ thread_id: The conversation thread identifier.
+
+ Returns:
+ MicrosandboxBackend connected to the sandbox.
+ """
+ provider = _get_provider()
+ sandbox_name = f"thread-{thread_id}"
+ sandbox = await provider.aget_or_create(
+ sandbox_id=sandbox_name,
+ timeout=120,
+ memory=512,
+ cpus=1.0,
+ )
+ logger.info("Sandbox ready: %s", sandbox.id)
+ return sandbox
+
+
+async def delete_sandbox(thread_id: int | str) -> None:
+ """Delete the sandbox for a conversation thread."""
+ provider = _get_provider()
+ sandbox_name = f"thread-{thread_id}"
+ try:
+ await provider.adelete(sandbox_id=sandbox_name)
+ logger.info("Sandbox deleted: surfsense/%s", sandbox_name)
+ except Exception:
+ logger.warning("Failed to delete sandbox surfsense/%s", sandbox_name, exc_info=True)
diff --git a/surfsense_backend/app/agents/new_chat/system_prompt.py b/surfsense_backend/app/agents/new_chat/system_prompt.py
index c8dcf5154..a965a0bca 100644
--- a/surfsense_backend/app/agents/new_chat/system_prompt.py
+++ b/surfsense_backend/app/agents/new_chat/system_prompt.py
@@ -645,6 +645,63 @@ However, from your video learning, it's important to note that asyncio is not su
"""
+# Sandbox / code execution instructions — appended when sandbox backend is enabled.
+# Inspired by Claude's computer-use prompt, scoped to code execution & data analytics.
+SANDBOX_EXECUTION_INSTRUCTIONS = """
+
+You have access to a secure, isolated Linux sandbox environment for running code and shell commands.
+This gives you the `execute` tool alongside the standard filesystem tools (`ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`).
+
+## When to Use Code Execution
+
+Use the sandbox when the task benefits from actually running code rather than just describing it:
+- **Data analysis**: Load CSVs/JSON, compute statistics, filter/aggregate data, pivot tables
+- **Visualization**: Generate charts and plots (matplotlib, plotly, seaborn)
+- **Calculations**: Math, financial modeling, unit conversions, simulations
+- **Code validation**: Run and test code snippets the user provides or asks about
+- **File processing**: Parse, transform, or convert data files
+- **Quick prototyping**: Demonstrate working code for the user's problem
+- **Package exploration**: Install and test libraries the user is evaluating
+
+## When NOT to Use Code Execution
+
+Do not use the sandbox for:
+- Answering factual questions from your own knowledge
+- Summarizing or explaining concepts
+- Simple formatting or text generation tasks
+- Tasks that don't require running code to answer
+
+## Package Management
+
+- Use `pip install ` to install Python packages as needed
+- Common data/analytics packages (pandas, numpy, matplotlib, scipy, scikit-learn) may need to be installed on first use
+- Always verify a package installed successfully before using it
+
+## Working Guidelines
+
+- **Working directory**: Use `/home` or `/tmp` for all work
+- **Iterative approach**: For complex tasks, break work into steps — write code, run it, check output, refine
+- **Error handling**: If code fails, read the error, fix the issue, and retry. Don't just report the error without attempting a fix.
+- **Show results**: When generating plots or outputs, present the key findings directly in your response. For plots, save to a file and describe the results.
+- **Be efficient**: Install packages once per session. Combine related commands when possible.
+- **Large outputs**: If command output is very large, use `head`, `tail`, or save to a file and read selectively.
+
+## Data Analytics Best Practices
+
+When the user asks you to analyze data:
+1. First, inspect the data structure (`head`, `shape`, `dtypes`, `describe()`)
+2. Clean and validate before computing (handle nulls, check types)
+3. Perform the analysis and present results clearly
+4. Offer follow-up insights or visualizations when appropriate
+
+## Security Notes
+
+- The sandbox is fully isolated — you cannot access the host system, the user's local files, or any secrets
+- Each conversation thread has its own sandbox environment
+- Installed packages and created files can persist for the thread while its sandbox is active; cleanup depends on sandbox lifecycle/deletion policy
+
+"""
+
# Anti-citation prompt - used when citations are disabled
# This explicitly tells the model NOT to include citations
SURFSENSE_NO_CITATION_INSTRUCTIONS = """
@@ -670,6 +727,7 @@ Your goal is to provide helpful, informative answers in a clean, readable format
def build_surfsense_system_prompt(
today: datetime | None = None,
thread_visibility: ChatVisibility | None = None,
+ sandbox_enabled: bool = False,
) -> str:
"""
Build the SurfSense system prompt with default settings.
@@ -678,10 +736,12 @@ def build_surfsense_system_prompt(
- Default system instructions
- Tools instructions (always included)
- Citation instructions enabled
+ - Sandbox execution instructions (when sandbox_enabled=True)
Args:
today: Optional datetime for today's date (defaults to current UTC date)
thread_visibility: Optional; when provided, used for conditional prompt (e.g. private vs shared memory wording). Defaults to private behavior when None.
+ sandbox_enabled: Whether the sandbox backend is active (adds code execution instructions).
Returns:
Complete system prompt string
@@ -691,7 +751,8 @@ def build_surfsense_system_prompt(
system_instructions = _get_system_instructions(visibility, today)
tools_instructions = _get_tools_instructions(visibility)
citation_instructions = SURFSENSE_CITATION_INSTRUCTIONS
- return system_instructions + tools_instructions + citation_instructions
+ sandbox_instructions = SANDBOX_EXECUTION_INSTRUCTIONS if sandbox_enabled else ""
+ return system_instructions + tools_instructions + citation_instructions + sandbox_instructions
def build_configurable_system_prompt(
@@ -700,14 +761,16 @@ def build_configurable_system_prompt(
citations_enabled: bool = True,
today: datetime | None = None,
thread_visibility: ChatVisibility | None = None,
+ sandbox_enabled: bool = False,
) -> str:
"""
Build a configurable SurfSense system prompt based on NewLLMConfig settings.
- The prompt is composed of three parts:
+ The prompt is composed of up to four parts:
1. System Instructions - either custom or default SURFSENSE_SYSTEM_INSTRUCTIONS
2. Tools Instructions - always included (SURFSENSE_TOOLS_INSTRUCTIONS)
3. Citation Instructions - either SURFSENSE_CITATION_INSTRUCTIONS or SURFSENSE_NO_CITATION_INSTRUCTIONS
+ 4. Sandbox Execution Instructions - when sandbox_enabled=True
Args:
custom_system_instructions: Custom system instructions to use. If empty/None and
@@ -719,6 +782,7 @@ def build_configurable_system_prompt(
anti-citation instructions (False).
today: Optional datetime for today's date (defaults to current UTC date)
thread_visibility: Optional; when provided, used for conditional prompt (e.g. private vs shared memory wording). Defaults to private behavior when None.
+ sandbox_enabled: Whether the sandbox backend is active (adds code execution instructions).
Returns:
Complete system prompt string
@@ -727,7 +791,6 @@ def build_configurable_system_prompt(
# Determine system instructions
if custom_system_instructions and custom_system_instructions.strip():
- # Use custom instructions, injecting the date placeholder if present
system_instructions = custom_system_instructions.format(
resolved_today=resolved_today
)
@@ -735,7 +798,6 @@ def build_configurable_system_prompt(
visibility = thread_visibility or ChatVisibility.PRIVATE
system_instructions = _get_system_instructions(visibility, today)
else:
- # No system instructions (edge case)
system_instructions = ""
# Tools instructions: conditional on thread_visibility (private vs shared memory wording)
@@ -748,7 +810,9 @@ def build_configurable_system_prompt(
else SURFSENSE_NO_CITATION_INSTRUCTIONS
)
- return system_instructions + tools_instructions + citation_instructions
+ sandbox_instructions = SANDBOX_EXECUTION_INSTRUCTIONS if sandbox_enabled else ""
+
+ return system_instructions + tools_instructions + citation_instructions + sandbox_instructions
def get_default_system_instructions() -> str:
diff --git a/surfsense_backend/app/tasks/chat/stream_new_chat.py b/surfsense_backend/app/tasks/chat/stream_new_chat.py
index 4ba12c171..ecf04ce08 100644
--- a/surfsense_backend/app/tasks/chat/stream_new_chat.py
+++ b/surfsense_backend/app/tasks/chat/stream_new_chat.py
@@ -10,6 +10,7 @@ Supports loading LLM configurations from:
"""
import json
+import re
from collections.abc import AsyncGenerator
from dataclasses import dataclass
from typing import Any
@@ -404,6 +405,21 @@ async def _stream_agent_events(
status="in_progress",
items=last_active_step_items,
)
+ elif tool_name == "execute":
+ cmd = (
+ tool_input.get("command", "")
+ if isinstance(tool_input, dict)
+ else str(tool_input)
+ )
+ display_cmd = cmd[:80] + ("…" if len(cmd) > 80 else "")
+ last_active_step_title = "Running command"
+ last_active_step_items = [f"$ {display_cmd}"]
+ yield streaming_service.format_thinking_step(
+ step_id=tool_step_id,
+ title="Running command",
+ status="in_progress",
+ items=last_active_step_items,
+ )
else:
last_active_step_title = f"Using {tool_name.replace('_', ' ')}"
last_active_step_items = []
@@ -620,6 +636,26 @@ async def _stream_agent_events(
status="completed",
items=completed_items,
)
+ elif tool_name == "execute":
+ raw_text = (
+ tool_output.get("result", "")
+ if isinstance(tool_output, dict)
+ else str(tool_output)
+ )
+ m = re.match(r"^Exit code:\s*(\d+)", raw_text)
+ exit_code_val = int(m.group(1)) if m else None
+ if exit_code_val is not None and exit_code_val == 0:
+ completed_items = [*last_active_step_items, "Completed successfully"]
+ elif exit_code_val is not None:
+ completed_items = [*last_active_step_items, f"Exit code: {exit_code_val}"]
+ else:
+ completed_items = [*last_active_step_items, "Finished"]
+ yield streaming_service.format_thinking_step(
+ step_id=original_step_id,
+ title="Running command",
+ status="completed",
+ items=completed_items,
+ )
elif tool_name == "ls":
if isinstance(tool_output, dict):
ls_output = tool_output.get("result", "")
@@ -811,6 +847,26 @@ async def _stream_agent_events(
if isinstance(tool_output, dict)
else {"result": tool_output},
)
+ elif tool_name == "execute":
+ raw_text = (
+ tool_output.get("result", "")
+ if isinstance(tool_output, dict)
+ else str(tool_output)
+ )
+ exit_code: int | None = None
+ output_text = raw_text
+ m = re.match(r"^Exit code:\s*(\d+)", raw_text)
+ if m:
+ exit_code = int(m.group(1))
+ om = re.search(r"\nOutput:\n([\s\S]*)", raw_text)
+ output_text = om.group(1) if om else ""
+ yield streaming_service.format_tool_output_available(
+ tool_call_id,
+ {
+ "exit_code": exit_code,
+ "output": output_text,
+ },
+ )
else:
yield streaming_service.format_tool_output_available(
tool_call_id,
@@ -975,6 +1031,17 @@ async def stream_new_chat(
# Get the PostgreSQL checkpointer for persistent conversation memory
checkpointer = await get_checkpointer()
+ # Optionally provision a sandboxed code execution environment
+ sandbox_backend = None
+ from app.agents.new_chat.sandbox import is_sandbox_enabled, get_or_create_sandbox
+ if is_sandbox_enabled():
+ try:
+ sandbox_backend = await get_or_create_sandbox(chat_id)
+ except Exception as sandbox_err:
+ logging.getLogger(__name__).warning(
+ "Sandbox creation failed, continuing without execute tool: %s", sandbox_err
+ )
+
visibility = thread_visibility or ChatVisibility.PRIVATE
agent = await create_surfsense_deep_agent(
llm=llm,
@@ -987,6 +1054,7 @@ async def stream_new_chat(
agent_config=agent_config,
firecrawl_api_key=firecrawl_api_key,
thread_visibility=visibility,
+ sandbox_backend=sandbox_backend,
)
# Build input with message history
@@ -1352,6 +1420,17 @@ async def stream_resume_chat(
firecrawl_api_key = webcrawler_connector.config.get("FIRECRAWL_API_KEY")
checkpointer = await get_checkpointer()
+
+ sandbox_backend = None
+ from app.agents.new_chat.sandbox import is_sandbox_enabled, get_or_create_sandbox
+ if is_sandbox_enabled():
+ try:
+ sandbox_backend = await get_or_create_sandbox(chat_id)
+ except Exception as sandbox_err:
+ logging.getLogger(__name__).warning(
+ "Sandbox creation failed, continuing without execute tool: %s", sandbox_err
+ )
+
visibility = thread_visibility or ChatVisibility.PRIVATE
agent = await create_surfsense_deep_agent(
@@ -1365,6 +1444,7 @@ async def stream_resume_chat(
agent_config=agent_config,
firecrawl_api_key=firecrawl_api_key,
thread_visibility=visibility,
+ sandbox_backend=sandbox_backend,
)
# Release the transaction before streaming (same rationale as stream_new_chat).
diff --git a/surfsense_backend/pyproject.toml b/surfsense_backend/pyproject.toml
index 7f52d4881..3df84141d 100644
--- a/surfsense_backend/pyproject.toml
+++ b/surfsense_backend/pyproject.toml
@@ -65,6 +65,7 @@ dependencies = [
"pypandoc_binary>=1.16.2",
"typst>=0.14.0",
"deepagents>=0.4.3",
+ "deepagents-microsandbox>=1.0.1",
]
[dependency-groups]
diff --git a/surfsense_backend/uv.lock b/surfsense_backend/uv.lock
index 8a6b7138a..50ed66617 100644
--- a/surfsense_backend/uv.lock
+++ b/surfsense_backend/uv.lock
@@ -67,7 +67,7 @@ wheels = [
[[package]]
name = "aiohttp"
-version = "3.12.13"
+version = "3.10.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -75,45 +75,40 @@ dependencies = [
{ name = "attrs" },
{ name = "frozenlist" },
{ name = "multidict" },
- { name = "propcache" },
{ name = "yarl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160 }
+sdist = { url = "https://files.pythonhosted.org/packages/25/a8/8e2ba36c6e3278d62e0c88aa42bb92ddbef092ac363b390dab4421da5cf5/aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7", size = 7551886 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491 },
- { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104 },
- { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948 },
- { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742 },
- { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393 },
- { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486 },
- { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643 },
- { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082 },
- { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884 },
- { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943 },
- { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398 },
- { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051 },
- { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611 },
- { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586 },
- { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197 },
- { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771 },
- { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869 },
- { url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910 },
- { url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566 },
- { url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856 },
- { url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683 },
- { url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946 },
- { url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017 },
- { url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390 },
- { url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719 },
- { url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424 },
- { url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447 },
- { url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110 },
- { url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706 },
- { url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839 },
- { url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311 },
- { url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202 },
- { url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794 },
- { url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735 },
+ { url = "https://files.pythonhosted.org/packages/01/16/077057ef3bd684dbf9a8273a5299e182a8d07b4b252503712ff8b5364fd1/aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710", size = 584830 },
+ { url = "https://files.pythonhosted.org/packages/2c/cf/348b93deb9597c61a51b6682e81f7c7d79290249e886022ef0705d858d90/aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d", size = 397090 },
+ { url = "https://files.pythonhosted.org/packages/70/bf/903df5cd739dfaf5b827b3d8c9d68ff4fcea16a0ca1aeb948c9da30f56c8/aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97", size = 392361 },
+ { url = "https://files.pythonhosted.org/packages/fb/97/e4792675448a2ac5bd56f377a095233b805dd1315235c940c8ba5624e3cb/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725", size = 1309839 },
+ { url = "https://files.pythonhosted.org/packages/96/d0/ba19b1260da6fbbda4d5b1550d8a53ba3518868f2c143d672aedfdbc6172/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636", size = 1348116 },
+ { url = "https://files.pythonhosted.org/packages/b3/b9/15100ee7113a2638bfdc91aecc54641609a92a7ce4fe533ebeaa8d43ff93/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385", size = 1391402 },
+ { url = "https://files.pythonhosted.org/packages/c5/36/831522618ac0dcd0b28f327afd18df7fb6bbf3eaf302f912a40e87714846/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087", size = 1304239 },
+ { url = "https://files.pythonhosted.org/packages/60/9f/b7230d0c48b076500ae57adb717aa0656432acd3d8febb1183dedfaa4e75/aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f", size = 1256565 },
+ { url = "https://files.pythonhosted.org/packages/63/c2/35c7b4699f4830b3b0a5c3d5619df16dca8052ae8b488e66065902d559f6/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03", size = 1269285 },
+ { url = "https://files.pythonhosted.org/packages/51/48/bc20ea753909bdeb09f9065260aefa7453e3a57f6a51f56f5216adc1a5e7/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d", size = 1276716 },
+ { url = "https://files.pythonhosted.org/packages/0c/7b/a8708616b3810f55ead66f8e189afa9474795760473aea734bbea536cd64/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a", size = 1315023 },
+ { url = "https://files.pythonhosted.org/packages/2a/d6/dfe9134a921e05b01661a127a37b7d157db93428905450e32f9898eef27d/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e", size = 1342735 },
+ { url = "https://files.pythonhosted.org/packages/ca/1a/3bd7f18e3909eabd57e5d17ecdbf5ea4c5828d91341e3676a07de7c76312/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4", size = 1302618 },
+ { url = "https://files.pythonhosted.org/packages/cf/51/d063133781cda48cfdd1e11fc8ef45ab3912b446feba41556385b3ae5087/aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb", size = 360497 },
+ { url = "https://files.pythonhosted.org/packages/55/4e/f29def9ed39826fe8f85955f2e42fe5cc0cbe3ebb53c97087f225368702e/aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27", size = 380577 },
+ { url = "https://files.pythonhosted.org/packages/1f/63/654c185dfe3cf5d4a0d35b6ee49ee6ca91922c694eaa90732e1ba4b40ef1/aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127", size = 577381 },
+ { url = "https://files.pythonhosted.org/packages/4e/c4/ee9c350acb202ba2eb0c44b0f84376b05477e870444192a9f70e06844c28/aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413", size = 393289 },
+ { url = "https://files.pythonhosted.org/packages/3d/7c/30d161a7e3b208cef1b922eacf2bbb8578b7e5a62266a6a2245a1dd044dc/aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461", size = 388859 },
+ { url = "https://files.pythonhosted.org/packages/79/10/8d050e04be447d3d39e5a4a910fa289d930120cebe1b893096bd3ee29063/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288", size = 1280983 },
+ { url = "https://files.pythonhosted.org/packages/31/b3/977eca40afe643dcfa6b8d8bb9a93f4cba1d8ed1ead22c92056b08855c7a/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067", size = 1317132 },
+ { url = "https://files.pythonhosted.org/packages/1a/43/b5ee8e697ed0f96a2b3d80b3058fa7590cda508e9cd256274246ba1cf37a/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e", size = 1362630 },
+ { url = "https://files.pythonhosted.org/packages/28/20/3ae8e993b2990fa722987222dea74d6bac9331e2f530d086f309b4aa8847/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1", size = 1276865 },
+ { url = "https://files.pythonhosted.org/packages/02/08/1afb0ab7dcff63333b683e998e751aa2547d1ff897b577d2244b00e6fe38/aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006", size = 1230448 },
+ { url = "https://files.pythonhosted.org/packages/c6/fd/ccd0ff842c62128d164ec09e3dd810208a84d79cd402358a3038ae91f3e9/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f", size = 1244626 },
+ { url = "https://files.pythonhosted.org/packages/9f/75/30e9537ab41ed7cb062338d8df7c4afb0a715b3551cd69fc4ea61cfa5a95/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6", size = 1243608 },
+ { url = "https://files.pythonhosted.org/packages/c2/e0/3e7a62d99b9080793affddc12a82b11c9bc1312916ad849700d2bddf9786/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31", size = 1286158 },
+ { url = "https://files.pythonhosted.org/packages/71/b8/df67886802e71e976996ed9324eb7dc379e53a7d972314e9c7fe3f6ac6bc/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d", size = 1313636 },
+ { url = "https://files.pythonhosted.org/packages/3c/3b/aea9c3e70ff4e030f46902df28b4cdf486695f4d78fd9c6698827e2bafab/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00", size = 1273772 },
+ { url = "https://files.pythonhosted.org/packages/e9/9e/4b4c5705270d1c4ee146516ad288af720798d957ba46504aaf99b86e85d9/aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71", size = 358679 },
+ { url = "https://files.pythonhosted.org/packages/28/1d/18ef37549901db94717d4389eb7be807acbfbdeab48a73ff2993fc909118/aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e", size = 378073 },
]
[[package]]
@@ -1261,6 +1256,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/58/f8/c076a841b68cc13d89c395cc97965b37751ed008691a304119efa0f5717e/deepagents-0.4.3-py3-none-any.whl", hash = "sha256:298d19c5c0b4c6fc6a74b68049a7bfea0ba481aece7201ab21e7172b71ee61b9", size = 94882 },
]
+[[package]]
+name = "deepagents-microsandbox"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "deepagents" },
+ { name = "microsandbox" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8d/d5/77562772b7bf868478e5e3badb4f66e60171c6b740be4cf9fd5ffa0c37e5/deepagents_microsandbox-1.0.1.tar.gz", hash = "sha256:b9471f251597fc56b9b2bc5f41a478cd6b87db2641a1e91210978b4abeeb1600", size = 140696 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/e5/7fc618dfa08d60a954bf3b13cb9c765ecb37cd3ad8c2174171dcbff8b00b/deepagents_microsandbox-1.0.1-py3-none-any.whl", hash = "sha256:8173ce8dbdf290a0fb5bf83f204814b587470ba9b93fcdad8980ca85e46604b1", size = 9736 },
+]
+
[[package]]
name = "defusedxml"
version = "0.7.1"
@@ -3703,6 +3711,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
+[[package]]
+name = "microsandbox"
+version = "0.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "frozenlist" },
+ { name = "python-dotenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/ad/200f7d89d9ae6f6066ee71e2dff3b3becece1858e8d795f8cc8a66c94516/microsandbox-0.1.8.tar.gz", hash = "sha256:38eac3310f05a238fc49c27cd9c6064a767ccb6f8a53c118b7ecfccb5df58b7a", size = 8949 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/89/2d6653e4c6bfa535da59d84d7c8bcc1678b35299ed43c1d11fb1c07a2179/microsandbox-0.1.8-py3-none-any.whl", hash = "sha256:b4503f6efd0f58e1acbac782399d3020cc704031279637fe5c60bdb5da267cd8", size = 12112 },
+]
+
[[package]]
name = "misaki"
version = "0.9.4"
@@ -6845,6 +6867,7 @@ dependencies = [
{ name = "composio" },
{ name = "datasets" },
{ name = "deepagents" },
+ { name = "deepagents-microsandbox" },
{ name = "discord-py" },
{ name = "docling" },
{ name = "elasticsearch" },
@@ -6915,6 +6938,7 @@ requires-dist = [
{ name = "composio", specifier = ">=0.10.9" },
{ name = "datasets", specifier = ">=2.21.0" },
{ name = "deepagents", specifier = ">=0.4.3" },
+ { name = "deepagents-microsandbox", specifier = ">=1.0.1" },
{ name = "discord-py", specifier = ">=2.5.2" },
{ name = "docling", specifier = ">=2.15.0" },
{ name = "elasticsearch", specifier = ">=9.1.1" },
diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
index dd11382a8..8720078cc 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
@@ -49,6 +49,7 @@ import {
DeleteNotionPageToolUI,
UpdateNotionPageToolUI,
} from "@/components/tool-ui/notion";
+import { SandboxExecuteToolUI } from "@/components/tool-ui/sandbox-execute";
import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage";
import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory";
import { Skeleton } from "@/components/ui/skeleton";
@@ -151,6 +152,7 @@ const TOOLS_WITH_UI = new Set([
"create_linear_issue",
"update_linear_issue",
"delete_linear_issue",
+ "execute",
// "write_todos", // Disabled for now
]);
@@ -1664,6 +1666,7 @@ export default function NewChatPage() {
+
{/* Disabled for now */}
diff --git a/surfsense_web/components/tool-ui/index.ts b/surfsense_web/components/tool-ui/index.ts
index 93b6229a0..c4f0dbde5 100644
--- a/surfsense_web/components/tool-ui/index.ts
+++ b/surfsense_web/components/tool-ui/index.ts
@@ -97,4 +97,11 @@ export {
SaveMemoryResultSchema,
SaveMemoryToolUI,
} from "./user-memory";
+export {
+ type ExecuteArgs,
+ ExecuteArgsSchema,
+ type ExecuteResult,
+ ExecuteResultSchema,
+ SandboxExecuteToolUI,
+} from "./sandbox-execute";
export { type WriteTodosData, WriteTodosSchema, WriteTodosToolUI } from "./write-todos";
diff --git a/surfsense_web/components/tool-ui/sandbox-execute.tsx b/surfsense_web/components/tool-ui/sandbox-execute.tsx
new file mode 100644
index 000000000..0dd853218
--- /dev/null
+++ b/surfsense_web/components/tool-ui/sandbox-execute.tsx
@@ -0,0 +1,266 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import {
+ AlertCircleIcon,
+ CheckCircle2Icon,
+ ChevronRightIcon,
+ Loader2Icon,
+ TerminalIcon,
+ XCircleIcon,
+} from "lucide-react";
+import { useMemo, useState } from "react";
+import { z } from "zod";
+import { Badge } from "@/components/ui/badge";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { cn } from "@/lib/utils";
+
+// ============================================================================
+// Zod Schemas
+// ============================================================================
+
+const ExecuteArgsSchema = z.object({
+ command: z.string(),
+ timeout: z.number().nullish(),
+});
+
+const ExecuteResultSchema = z.object({
+ result: z.string().nullish(),
+ exit_code: z.number().nullish(),
+ output: z.string().nullish(),
+ error: z.string().nullish(),
+ status: z.string().nullish(),
+});
+
+// ============================================================================
+// Types
+// ============================================================================
+
+type ExecuteArgs = z.infer
;
+type ExecuteResult = z.infer;
+
+interface ParsedOutput {
+ exitCode: number | null;
+ output: string;
+ truncated: boolean;
+ isError: boolean;
+}
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+function parseExecuteResult(result: ExecuteResult): ParsedOutput {
+ const raw = result.result || result.output || "";
+
+ if (result.error) {
+ return { exitCode: null, output: result.error, truncated: false, isError: true };
+ }
+
+ if (result.exit_code !== undefined && result.exit_code !== null) {
+ return {
+ exitCode: result.exit_code,
+ output: raw,
+ truncated: raw.includes("[Output was truncated"),
+ isError: result.exit_code !== 0,
+ };
+ }
+
+ const exitMatch = raw.match(/^Exit code:\s*(\d+)/);
+ if (exitMatch) {
+ const exitCode = parseInt(exitMatch[1], 10);
+ const outputMatch = raw.match(/\nOutput:\n([\s\S]*)/);
+ const output = outputMatch ? outputMatch[1] : "";
+ return {
+ exitCode,
+ output,
+ truncated: raw.includes("[Output was truncated"),
+ isError: exitCode !== 0,
+ };
+ }
+
+ if (raw.startsWith("Error:")) {
+ return { exitCode: null, output: raw, truncated: false, isError: true };
+ }
+
+ return { exitCode: null, output: raw, truncated: false, isError: false };
+}
+
+function truncateCommand(command: string, maxLen = 80): string {
+ if (command.length <= maxLen) return command;
+ return command.slice(0, maxLen) + "…";
+}
+
+// ============================================================================
+// Sub-Components
+// ============================================================================
+
+function ExecuteLoading({ command }: { command: string }) {
+ return (
+
+
+
+ {truncateCommand(command)}
+
+
+ );
+}
+
+function ExecuteErrorState({ command, error }: { command: string; error: string }) {
+ return (
+
+
+
+
+
Execution failed
+
+ $ {command}
+
+
{error}
+
+
+
+ );
+}
+
+function ExecuteCancelledState({ command }: { command: string }) {
+ return (
+
+ );
+}
+
+function ExecuteResult({
+ command,
+ parsed,
+}: {
+ command: string;
+ parsed: ParsedOutput;
+}) {
+ const [open, setOpen] = useState(false);
+ const hasOutput = parsed.output.trim().length > 0;
+
+ const exitBadge = useMemo(() => {
+ if (parsed.exitCode === null) return null;
+ const success = parsed.exitCode === 0;
+ return (
+
+ {success ? (
+
+ ) : (
+
+ )}
+ {parsed.exitCode}
+
+ );
+ }, [parsed.exitCode]);
+
+ return (
+
+
+
+
+
+
+ {truncateCommand(command)}
+
+ {exitBadge}
+
+
+
+
+
+ {parsed.output}
+
+ {parsed.truncated && (
+
+ Output was truncated due to size limits
+
+ )}
+
+
+
+
+ );
+}
+
+// ============================================================================
+// Tool UI
+// ============================================================================
+
+export const SandboxExecuteToolUI = makeAssistantToolUI({
+ toolName: "execute",
+ render: function SandboxExecuteUI({ args, result, status }) {
+ const command = args.command || "…";
+
+ if (status.type === "running" || status.type === "requires-action") {
+ return ;
+ }
+
+ if (status.type === "incomplete") {
+ if (status.reason === "cancelled") {
+ return ;
+ }
+ if (status.reason === "error") {
+ return (
+
+ );
+ }
+ }
+
+ if (!result) {
+ return ;
+ }
+
+ if (result.error && !result.result && !result.output) {
+ return ;
+ }
+
+ const parsed = parseExecuteResult(result);
+ return ;
+ },
+});
+
+export {
+ ExecuteArgsSchema,
+ ExecuteResultSchema,
+ type ExecuteArgs,
+ type ExecuteResult,
+};