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 ( +
+

+ + $ {command} +

+
+ ); +} + +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, +};