mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-01-30 07:06:39 +08:00
feat: Implement pluggable multi-provider sandbox architecture (#12820)
## Summary Implement a flexible sandbox provider system supporting both self-managed (Docker) and SaaS (Aliyun Code Interpreter) backends for secure code execution in agent workflows. **Key Changes:** - ✅ Aliyun Code Interpreter provider using official `agentrun-sdk>=0.0.16` - ✅ Self-managed provider with gVisor (runsc) security - ✅ Arguments parameter support for dynamic code execution - ✅ Database-only configuration (removed fallback logic) - ✅ Configuration scripts for quick setup Issue #12479 ## Features ### 🔌 Provider Abstraction Layer **1. Self-Managed Provider** (`agent/sandbox/providers/self_managed.py`) - Wraps existing executor_manager HTTP API - gVisor (runsc) for secure container isolation - Configurable pool size, timeout, retry logic - Languages: Python, Node.js, JavaScript - ⚠️ **Requires**: gVisor installation, Docker, base images **2. Aliyun Code Interpreter** (`agent/sandbox/providers/aliyun_codeinterpreter.py`) - SaaS integration using official agentrun-sdk - Serverless microVM execution with auto-authentication - Hard timeout: 30 seconds max - Credentials: `AGENTRUN_ACCESS_KEY_ID`, `AGENTRUN_ACCESS_KEY_SECRET`, `AGENTRUN_ACCOUNT_ID`, `AGENTRUN_REGION` - Automatically wraps code to call `main()` function **3. E2B Provider** (`agent/sandbox/providers/e2b.py`) - Placeholder for future integration ### ⚙️ Configuration System - `conf/system_settings.json`: Default provider = `aliyun_codeinterpreter` - `agent/sandbox/client.py`: Enforces database-only configuration - Admin UI: `/admin/sandbox-settings` - Configuration validation via `validate_config()` method - Health checks for all providers ### 🎯 Key Capabilities **Arguments Parameter Support:** All providers support passing arguments to `main()` function: ```python # User code def main(name: str, count: int) -> dict: return {"message": f"Hello {name}!" * count} # Executed with: arguments={"name": "World", "count": 3} # Result: {"message": "Hello World!Hello World!Hello World!"} ``` **Self-Describing Providers:** Each provider implements `get_config_schema()` returning form configuration for Admin UI **Error Handling:** Structured `ExecutionResult` with stdout, stderr, exit_code, execution_time ## Configuration Scripts Two scripts for quick Aliyun sandbox setup: **Shell Script (requires jq):** ```bash source scripts/configure_aliyun_sandbox.sh ``` **Python Script (interactive):** ```bash python3 scripts/configure_aliyun_sandbox.py ``` ## Testing ```bash # Unit tests uv run pytest agent/sandbox/tests/test_providers.py -v # Aliyun provider tests uv run pytest agent/sandbox/tests/test_aliyun_codeinterpreter.py -v # Integration tests (requires credentials) uv run pytest agent/sandbox/tests/test_aliyun_codeinterpreter_integration.py -v # Quick SDK validation python3 agent/sandbox/tests/verify_sdk.py ``` **Test Coverage:** - 30 unit tests for provider abstraction - Provider-specific tests for Aliyun - Integration tests with real API - Security tests for executor_manager ## Documentation - `docs/develop/sandbox_spec.md` - Complete architecture specification - `agent/sandbox/tests/MIGRATION_GUIDE.md` - Migration from legacy sandbox - `agent/sandbox/tests/QUICKSTART.md` - Quick start guide - `agent/sandbox/tests/README.md` - Testing documentation ## Breaking Changes ⚠️ **Migration Required:** 1. **Directory Move**: `sandbox/` → `agent/sandbox/` - Update imports: `from sandbox.` → `from agent.sandbox.` 2. **Mandatory Configuration**: - SystemSettings must have `sandbox.provider_type` configured - Removed fallback default values - Configuration must exist in database (from `conf/system_settings.json`) 3. **Aliyun Credentials**: - Requires `AGENTRUN_*` environment variables (not `ALIYUN_*`) - `AGENTRUN_ACCOUNT_ID` is now required (Aliyun primary account ID) 4. **Self-Managed Provider**: - gVisor (runsc) must be installed for security - Install: `go install gvisor.dev/gvisor/runsc@latest` ## Database Schema Changes ```python # SystemSettings.value: CharField → TextField api/db/db_models.py: Changed for unlimited config length # SystemSettingsService.get_by_name(): Fixed query precision api/db/services/system_settings_service.py: startswith → exact match ``` ## Files Changed ### Backend (Python) - `agent/sandbox/providers/base.py` - SandboxProvider ABC interface - `agent/sandbox/providers/manager.py` - ProviderManager - `agent/sandbox/providers/self_managed.py` - Self-managed provider - `agent/sandbox/providers/aliyun_codeinterpreter.py` - Aliyun provider - `agent/sandbox/providers/e2b.py` - E2B provider (placeholder) - `agent/sandbox/client.py` - Unified client (enforces DB-only config) - `agent/tools/code_exec.py` - Updated to use provider system - `admin/server/services.py` - SandboxMgr with registry & validation - `admin/server/routes.py` - 5 sandbox API endpoints - `conf/system_settings.json` - Default: aliyun_codeinterpreter - `api/db/db_models.py` - TextField for SystemSettings.value - `api/db/services/system_settings_service.py` - Exact match query ### Frontend (TypeScript/React) - `web/src/pages/admin/sandbox-settings.tsx` - Settings UI - `web/src/services/admin-service.ts` - Sandbox service functions - `web/src/services/admin.service.d.ts` - Type definitions - `web/src/utils/api.ts` - Sandbox API endpoints ### Documentation - `docs/develop/sandbox_spec.md` - Architecture spec - `agent/sandbox/tests/MIGRATION_GUIDE.md` - Migration guide - `agent/sandbox/tests/QUICKSTART.md` - Quick start - `agent/sandbox/tests/README.md` - Testing guide ### Configuration Scripts - `scripts/configure_aliyun_sandbox.sh` - Shell script (jq) - `scripts/configure_aliyun_sandbox.py` - Python script ### Tests - `agent/sandbox/tests/test_providers.py` - 30 unit tests - `agent/sandbox/tests/test_aliyun_codeinterpreter.py` - Provider tests - `agent/sandbox/tests/test_aliyun_codeinterpreter_integration.py` - Integration tests - `agent/sandbox/tests/verify_sdk.py` - SDK validation ## Architecture ``` Admin UI → Admin API → SandboxMgr → ProviderManager → [SelfManaged|Aliyun|E2B] ↓ SystemSettings ``` ## Usage ### 1. Configure Provider **Via Admin UI:** 1. Navigate to `/admin/sandbox-settings` 2. Select provider (Aliyun Code Interpreter / Self-Managed) 3. Fill in configuration 4. Click "Test Connection" to verify 5. Click "Save" to apply **Via Configuration Scripts:** ```bash # Aliyun provider export AGENTRUN_ACCESS_KEY_ID="xxx" export AGENTRUN_ACCESS_KEY_SECRET="yyy" export AGENTRUN_ACCOUNT_ID="zzz" export AGENTRUN_REGION="cn-shanghai" source scripts/configure_aliyun_sandbox.sh ``` ### 2. Restart Service ```bash cd docker docker compose restart ragflow-server ``` ### 3. Execute Code in Agent ```python from agent.sandbox.client import execute_code result = execute_code( code='def main(name: str) -> dict: return {"message": f"Hello {name}!"}', language="python", timeout=30, arguments={"name": "World"} ) print(result.stdout) # {"message": "Hello World!"} ``` ## Troubleshooting ### "Container pool is busy" (Self-Managed) - **Cause**: Pool exhausted (default: 1 container in `.env`) - **Fix**: Increase `SANDBOX_EXECUTOR_MANAGER_POOL_SIZE` to 5+ ### "Sandbox provider type not configured" - **Cause**: Database missing configuration - **Fix**: Run config script or set via Admin UI ### "gVisor not found" - **Cause**: runsc not installed - **Fix**: `go install gvisor.dev/gvisor/runsc@latest && sudo cp ~/go/bin/runsc /usr/local/bin/` ### Aliyun authentication errors - **Cause**: Wrong environment variable names - **Fix**: Use `AGENTRUN_*` prefix (not `ALIYUN_*`) ## Checklist - [x] All tests passing (30 unit tests + integration tests) - [x] Documentation updated (spec, migration guide, quickstart) - [x] Type definitions added (TypeScript) - [x] Admin UI implemented - [x] Configuration validation - [x] Health checks implemented - [x] Error handling with structured results - [x] Breaking changes documented - [x] Configuration scripts created - [x] gVisor requirements documented Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
9
agent/sandbox/.env.example
Normal file
9
agent/sandbox/.env.example
Normal file
@ -0,0 +1,9 @@
|
||||
# Copy this file to `.env` and modify as needed
|
||||
|
||||
SANDBOX_EXECUTOR_MANAGER_POOL_SIZE=5
|
||||
SANDBOX_BASE_PYTHON_IMAGE=sandbox-base-python:latest
|
||||
SANDBOX_BASE_NODEJS_IMAGE=sandbox-base-nodejs:latest
|
||||
SANDBOX_EXECUTOR_MANAGER_PORT=9385
|
||||
SANDBOX_ENABLE_SECCOMP=false
|
||||
SANDBOX_MAX_MEMORY=256m # b, k, m, g
|
||||
SANDBOX_TIMEOUT=10s # s, m, 1m30s
|
||||
115
agent/sandbox/Makefile
Normal file
115
agent/sandbox/Makefile
Normal file
@ -0,0 +1,115 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Force using Bash to ensure the source command is available
|
||||
SHELL := /bin/bash
|
||||
|
||||
# Environment variable definitions
|
||||
VENV := .venv
|
||||
PYTHON := $(VENV)/bin/python
|
||||
UV := uv
|
||||
ACTIVATE_SCRIPT := $(VENV)/bin/activate
|
||||
SYS_PYTHON := python3
|
||||
PYTHONPATH := $(shell pwd)
|
||||
|
||||
.PHONY: all setup ensure_env ensure_uv start stop restart build clean test logs
|
||||
|
||||
all: setup start
|
||||
|
||||
# 🌱 Initialize environment + install dependencies
|
||||
setup: ensure_env ensure_uv
|
||||
@echo "📦 Installing dependencies with uv..."
|
||||
@$(UV) sync --python 3.12
|
||||
source $(ACTIVATE_SCRIPT) && \
|
||||
export PYTHONPATH=$(PYTHONPATH)
|
||||
@$(UV) pip install -r executor_manager/requirements.txt
|
||||
@echo "✅ Setup complete."
|
||||
|
||||
# 🔑 Ensure .env exists (copy from .env.example on first run)
|
||||
ensure_env:
|
||||
@if [ ! -f ".env" ]; then \
|
||||
if [ -f ".env.example" ]; then \
|
||||
echo "📝 Creating .env from .env.example..."; \
|
||||
cp .env.example .env; \
|
||||
else \
|
||||
echo "⚠️ Warning: .env.example not found, creating empty .env"; \
|
||||
touch .env; \
|
||||
fi; \
|
||||
else \
|
||||
echo "✅ .env already exists."; \
|
||||
fi
|
||||
|
||||
# 🔧 Ensure uv is executable (install using system Python)
|
||||
ensure_uv:
|
||||
@if ! command -v $(UV) >/dev/null 2>&1; then \
|
||||
echo "🛠️ Installing uv using system Python..."; \
|
||||
$(SYS_PYTHON) -m pip install -q --upgrade pip; \
|
||||
$(SYS_PYTHON) -m pip install -q uv || (echo "⚠️ uv install failed, check manually" && exit 1); \
|
||||
fi
|
||||
|
||||
# 🐳 Service control (using safer variable loading)
|
||||
start:
|
||||
@echo "🚀 Starting services..."
|
||||
source $(ACTIVATE_SCRIPT) && \
|
||||
export PYTHONPATH=$(PYTHONPATH) && \
|
||||
[ -f .env ] && source .env || true && \
|
||||
bash scripts/start.sh
|
||||
|
||||
stop:
|
||||
@echo "🛑 Stopping services..."
|
||||
source $(ACTIVATE_SCRIPT) && \
|
||||
bash scripts/stop.sh
|
||||
|
||||
restart: stop start
|
||||
@echo "🔁 Restarting services..."
|
||||
|
||||
build:
|
||||
@echo "🔧 Building base sandbox images..."
|
||||
@if [ -f .env ]; then \
|
||||
source .env && \
|
||||
echo "🐍 Building base sandbox image for Python ($$SANDBOX_BASE_PYTHON_IMAGE)..." && \
|
||||
docker build -t "$$SANDBOX_BASE_PYTHON_IMAGE" ./sandbox_base_image/python && \
|
||||
echo "⬢ Building base sandbox image for Nodejs ($$SANDBOX_BASE_NODEJS_IMAGE)..." && \
|
||||
docker build -t "$$SANDBOX_BASE_NODEJS_IMAGE" ./sandbox_base_image/nodejs; \
|
||||
else \
|
||||
echo "⚠️ .env file not found, skipping build."; \
|
||||
fi
|
||||
|
||||
test:
|
||||
@echo "🧪 Running sandbox security tests..."
|
||||
source $(ACTIVATE_SCRIPT) && \
|
||||
export PYTHONPATH=$(PYTHONPATH) && \
|
||||
$(PYTHON) tests/sandbox_security_tests_full.py
|
||||
|
||||
logs:
|
||||
@echo "📋 Showing logs from api-server and executor-manager..."
|
||||
docker compose logs -f
|
||||
|
||||
# 🧹 Clean all containers and volumes
|
||||
clean:
|
||||
@echo "🧹 Cleaning all containers and volumes..."
|
||||
@docker compose down -v || true
|
||||
@if [ -f .env ]; then \
|
||||
source .env && \
|
||||
for i in $$(seq 0 $$((SANDBOX_EXECUTOR_MANAGER_POOL_SIZE - 1))); do \
|
||||
echo "🧹 Deleting sandbox_python_$$i..." && \
|
||||
docker rm -f sandbox_python_$$i 2>/dev/null || true && \
|
||||
echo "🧹 Deleting sandbox_nodejs_$$i..." && \
|
||||
docker rm -f sandbox_nodejs_$$i 2>/dev/null || true; \
|
||||
done; \
|
||||
else \
|
||||
echo "⚠️ .env not found, skipping container cleanup"; \
|
||||
fi
|
||||
347
agent/sandbox/README.md
Normal file
347
agent/sandbox/README.md
Normal file
@ -0,0 +1,347 @@
|
||||
# RAGFlow Sandbox
|
||||
|
||||
A secure, pluggable code execution backend for RAGFlow and beyond.
|
||||
|
||||
## 🔧 Features
|
||||
|
||||
- ✅ **Seamless RAGFlow Integration** — Out-of-the-box compatibility with the `code` component.
|
||||
- 🔐 **High Security** — Leverages [gVisor](https://gvisor.dev/) for syscall-level sandboxing.
|
||||
- 🔧 **Customizable Sandboxing** — Easily modify `seccomp` settings as needed.
|
||||
- 🧩 **Pluggable Runtime Support** — Easily extend to support any programming language.
|
||||
- ⚙️ **Developer Friendly** — Get started with a single command using `Makefile`.
|
||||
|
||||
## 🏗 Architecture
|
||||
|
||||
<p align="center">
|
||||
<img src="asserts/code_executor_manager.svg" width="520" alt="Architecture Diagram">
|
||||
</p>
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 📋 Prerequisites
|
||||
|
||||
#### Required
|
||||
|
||||
- Linux distro compatible with gVisor
|
||||
- [gVisor](https://gvisor.dev/docs/user_guide/install/)
|
||||
- Docker >= `25.0` (API 1.44+) — executor manager now bundles Docker CLI `29.1.0` to match newer daemons.
|
||||
- Docker Compose >= `v2.26.1` like [RAGFlow](https://github.com/infiniflow/ragflow)
|
||||
- [uv](https://docs.astral.sh/uv/) as package and project manager
|
||||
|
||||
#### Optional (Recommended)
|
||||
|
||||
- [GNU Make](https://www.gnu.org/software/make/) for simplified CLI management
|
||||
|
||||
---
|
||||
|
||||
> ⚠️ **New Docker CLI requirement**
|
||||
>
|
||||
> If you see `client version 1.43 is too old. Minimum supported API version is 1.44`, pull the latest `infiniflow/sandbox-executor-manager:latest` (rebuilt with Docker CLI `29.1.0`) or rebuild it in `./sandbox/executor_manager`. Older images shipped Docker 24.x, which cannot talk to newer Docker daemons.
|
||||
|
||||
### 🐳 Build Docker Base Images
|
||||
|
||||
We use isolated base images for secure containerized execution:
|
||||
|
||||
```bash
|
||||
# Build base images manually
|
||||
docker build -t sandbox-base-python:latest ./sandbox_base_image/python
|
||||
docker build -t sandbox-base-nodejs:latest ./sandbox_base_image/nodejs
|
||||
|
||||
# OR use Makefile
|
||||
make build
|
||||
```
|
||||
|
||||
Then, build the executor manager image:
|
||||
|
||||
```bash
|
||||
docker build -t sandbox-executor-manager:latest ./executor_manager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📦 Running with RAGFlow
|
||||
|
||||
1. Ensure gVisor is correctly installed.
|
||||
2. Configure your `.env` in `docker/.env`:
|
||||
|
||||
- Uncomment sandbox-related variables.
|
||||
- Enable sandbox profile at the bottom.
|
||||
3. Add the following line to `/etc/hosts` as recommended:
|
||||
|
||||
```text
|
||||
127.0.0.1 sandbox-executor-manager
|
||||
```
|
||||
|
||||
4. Start RAGFlow service.
|
||||
|
||||
---
|
||||
|
||||
### 🧭 Running Standalone
|
||||
|
||||
#### Manual Setup
|
||||
|
||||
1. Initialize environment:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Launch:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml up
|
||||
```
|
||||
|
||||
3. Test:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
export PYTHONPATH=$(pwd)
|
||||
uv pip install -r executor_manager/requirements.txt
|
||||
uv run tests/sandbox_security_tests_full.py
|
||||
```
|
||||
|
||||
#### With Make
|
||||
|
||||
```bash
|
||||
make # setup + build + launch + test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📈 Monitoring
|
||||
|
||||
```bash
|
||||
docker logs -f sandbox-executor-manager # Manual
|
||||
make logs # With Make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🧰 Makefile Toolbox
|
||||
|
||||
| Command | Description |
|
||||
|-------------------|--------------------------------------------------|
|
||||
| `make` | Setup, build, launch and test all at once |
|
||||
| `make setup` | Initialize environment and install uv |
|
||||
| `make ensure_env` | Auto-create `.env` if missing |
|
||||
| `make ensure_uv` | Install `uv` package manager if missing |
|
||||
| `make build` | Build all Docker base images |
|
||||
| `make start` | Start services with safe env loading and testing |
|
||||
| `make stop` | Gracefully stop all services |
|
||||
| `make restart` | Shortcut for `stop` + `start` |
|
||||
| `make test` | Run full test suite |
|
||||
| `make logs` | Stream container logs |
|
||||
| `make clean` | Stop and remove orphan containers and volumes |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
The RAGFlow sandbox is designed to balance security and usability, offering solid protection without compromising developer experience.
|
||||
|
||||
### ✅ gVisor Isolation
|
||||
|
||||
At its core, we use [gVisor](https://gvisor.dev/docs/architecture_guide/security/), a user-space kernel, to isolate code execution from the host system. gVisor intercepts and restricts syscalls, offering robust protection against container escapes and privilege escalations.
|
||||
|
||||
### 🔒 Optional seccomp Support (Advanced)
|
||||
|
||||
For users who need **zero-trust-level syscall control**, we support an additional `seccomp` profile. This feature restricts containers to only a predefined set of system calls, as specified in `executor_manager/seccomp-profile-default.json`.
|
||||
|
||||
> ⚠️ This feature is **disabled by default** to maintain compatibility and usability. Enabling it may cause compatibility issues with some dependencies.
|
||||
|
||||
#### To enable seccomp
|
||||
|
||||
1. Edit your `.env` file:
|
||||
|
||||
```dotenv
|
||||
SANDBOX_ENABLE_SECCOMP=true
|
||||
```
|
||||
|
||||
2. Customize allowed syscalls in:
|
||||
|
||||
```
|
||||
executor_manager/seccomp-profile-default.json
|
||||
```
|
||||
|
||||
This profile is passed to the container with:
|
||||
|
||||
```bash
|
||||
--security-opt seccomp=/app/seccomp-profile-default.json
|
||||
```
|
||||
|
||||
### 🧠 Python Code AST Inspection
|
||||
|
||||
In addition to sandboxing, Python code is **statically analyzed via AST (Abstract Syntax Tree)** before execution. Potentially malicious code (e.g. file operations, subprocess calls, etc.) is rejected early, providing an extra layer of protection.
|
||||
|
||||
---
|
||||
|
||||
This security model strikes a balance between **robust isolation** and **developer usability**. While `seccomp` can be highly restrictive, our default setup aims to keep things usable for most developers — no obscure crashes or cryptic setup required.
|
||||
|
||||
## 📦 Add Extra Dependencies for Supported Languages
|
||||
|
||||
Currently, the following languages are officially supported:
|
||||
|
||||
| Language | Priority |
|
||||
|----------|----------|
|
||||
| Python | High |
|
||||
| Node.js | Medium |
|
||||
|
||||
### 🐍 Python
|
||||
|
||||
To add Python dependencies, simply edit the following file:
|
||||
|
||||
```bash
|
||||
sandbox_base_image/python/requirements.txt
|
||||
```
|
||||
|
||||
Add any additional packages you need, one per line (just like a normal pip requirements file).
|
||||
|
||||
### 🟨 Node.js
|
||||
|
||||
To add Node.js dependencies:
|
||||
|
||||
1. Navigate to the Node.js base image directory:
|
||||
|
||||
```bash
|
||||
cd sandbox_base_image/nodejs
|
||||
```
|
||||
|
||||
2. Use `npm` to install the desired packages. For example:
|
||||
|
||||
```bash
|
||||
npm install lodash
|
||||
```
|
||||
|
||||
3. The dependencies will be saved to `package.json` and `package-lock.json`, and included in the Docker image when rebuilt.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### 🐍 A Python example
|
||||
|
||||
```python
|
||||
def main(arg1: str, arg2: str) -> str:
|
||||
return f"result: {arg1 + arg2}"
|
||||
```
|
||||
|
||||
### 🟨 JavaScript examples
|
||||
|
||||
A simple sync function
|
||||
|
||||
```javascript
|
||||
function main({arg1, arg2}) {
|
||||
return arg1+arg2
|
||||
}
|
||||
```
|
||||
|
||||
Async funcion with aioxs
|
||||
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
async function main() {
|
||||
try {
|
||||
const response = await axios.get('https://github.com/infiniflow/ragflow');
|
||||
return 'Body:' + response.data;
|
||||
} catch (error) {
|
||||
return 'Error:' + error.message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 FAQ
|
||||
|
||||
### ❓Sandbox Not Working?
|
||||
|
||||
Follow this checklist to troubleshoot:
|
||||
|
||||
- [ ] **Is your machine compatible with gVisor?**
|
||||
|
||||
Ensure that your system supports gVisor. Refer to the [gVisor installation guide](https://gvisor.dev/docs/user_guide/install/).
|
||||
|
||||
- [ ] **Is gVisor properly installed?**
|
||||
|
||||
**Common error:**
|
||||
|
||||
`HTTPConnectionPool(host='sandbox-executor-manager', port=9385): Read timed out.`
|
||||
|
||||
Cause: `runsc` is an unknown or invalid Docker runtime.
|
||||
**Fix:**
|
||||
|
||||
- Install gVisor
|
||||
|
||||
- Restart Docker
|
||||
|
||||
- Test with:
|
||||
|
||||
```bash
|
||||
docker run --rm --runtime=runsc hello-world
|
||||
```
|
||||
|
||||
- [ ] **Is `sandbox-executor-manager` mapped in `/etc/hosts`?**
|
||||
|
||||
**Common error:**
|
||||
|
||||
`HTTPConnectionPool(host='none', port=9385): Max retries exceeded.`
|
||||
|
||||
**Fix:**
|
||||
|
||||
Add the following entry to `/etc/hosts`:
|
||||
|
||||
```text
|
||||
127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
|
||||
```
|
||||
|
||||
- [ ] **Are you running the latest executor manager image?**
|
||||
|
||||
**Common error:**
|
||||
|
||||
`docker: Error response from daemon: client version 1.43 is too old. Minimum supported API version is 1.44`
|
||||
|
||||
**Fix:**
|
||||
|
||||
Pull the refreshed image that bundles Docker CLI `29.1.0`, or rebuild it in `./sandbox/executor_manager`:
|
||||
|
||||
```bash
|
||||
docker pull infiniflow/sandbox-executor-manager:latest
|
||||
# or
|
||||
docker build -t sandbox-executor-manager:latest ./sandbox/executor_manager
|
||||
```
|
||||
|
||||
- [ ] **Have you enabled sandbox-related configurations in RAGFlow?**
|
||||
|
||||
Double-check that all sandbox settings are correctly enabled in your RAGFlow configuration.
|
||||
|
||||
- [ ] **Have you pulled the required base images for the runners?**
|
||||
|
||||
**Common error:**
|
||||
|
||||
`HTTPConnectionPool(host='sandbox-executor-manager', port=9385): Read timed out.`
|
||||
|
||||
Cause: no runner was started.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Pull the necessary base images:
|
||||
|
||||
```bash
|
||||
docker pull infiniflow/sandbox-base-nodejs:latest
|
||||
docker pull infiniflow/sandbox-base-python:latest
|
||||
```
|
||||
|
||||
- [ ] **Did you restart the service after making changes?**
|
||||
|
||||
Any changes to configuration or environment require a full service restart to take effect.
|
||||
|
||||
|
||||
### ❓Container pool is busy?
|
||||
|
||||
All available runners are currently in use, executing tasks/running code. Please try again shortly, or consider increasing the pool size in the configuration to improve availability and reduce wait times.
|
||||
|
||||
## 🤝 Contribution
|
||||
|
||||
Contributions are welcome!
|
||||
4
agent/sandbox/asserts/code_executor_manager.svg
Normal file
4
agent/sandbox/asserts/code_executor_manager.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 45 KiB |
239
agent/sandbox/client.py
Normal file
239
agent/sandbox/client.py
Normal file
@ -0,0 +1,239 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Sandbox client for agent components.
|
||||
|
||||
This module provides a unified interface for agent components to interact
|
||||
with the configured sandbox provider.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from api.db.services.system_settings_service import SystemSettingsService
|
||||
from agent.sandbox.providers import ProviderManager
|
||||
from agent.sandbox.providers.base import ExecutionResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Global provider manager instance
|
||||
_provider_manager: Optional[ProviderManager] = None
|
||||
|
||||
|
||||
def get_provider_manager() -> ProviderManager:
|
||||
"""
|
||||
Get the global provider manager instance.
|
||||
|
||||
Returns:
|
||||
ProviderManager instance with active provider loaded
|
||||
"""
|
||||
global _provider_manager
|
||||
|
||||
if _provider_manager is not None:
|
||||
return _provider_manager
|
||||
|
||||
# Initialize provider manager with system settings
|
||||
_provider_manager = ProviderManager()
|
||||
_load_provider_from_settings()
|
||||
|
||||
return _provider_manager
|
||||
|
||||
|
||||
def _load_provider_from_settings() -> None:
|
||||
"""
|
||||
Load sandbox provider from system settings and configure the provider manager.
|
||||
|
||||
This function reads the system settings to determine which provider is active
|
||||
and initializes it with the appropriate configuration.
|
||||
"""
|
||||
global _provider_manager
|
||||
|
||||
if _provider_manager is None:
|
||||
return
|
||||
|
||||
try:
|
||||
# Get active provider type
|
||||
provider_type_settings = SystemSettingsService.get_by_name("sandbox.provider_type")
|
||||
if not provider_type_settings:
|
||||
raise RuntimeError(
|
||||
"Sandbox provider type not configured. Please set 'sandbox.provider_type' in system settings."
|
||||
)
|
||||
provider_type = provider_type_settings[0].value
|
||||
|
||||
# Get provider configuration
|
||||
provider_config_settings = SystemSettingsService.get_by_name(f"sandbox.{provider_type}")
|
||||
|
||||
if not provider_config_settings:
|
||||
logger.warning(f"No configuration found for provider: {provider_type}")
|
||||
config = {}
|
||||
else:
|
||||
try:
|
||||
config = json.loads(provider_config_settings[0].value)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse sandbox config for {provider_type}: {e}")
|
||||
config = {}
|
||||
|
||||
# Import and instantiate the provider
|
||||
from agent.sandbox.providers import (
|
||||
SelfManagedProvider,
|
||||
AliyunCodeInterpreterProvider,
|
||||
E2BProvider,
|
||||
)
|
||||
|
||||
provider_classes = {
|
||||
"self_managed": SelfManagedProvider,
|
||||
"aliyun_codeinterpreter": AliyunCodeInterpreterProvider,
|
||||
"e2b": E2BProvider,
|
||||
}
|
||||
|
||||
if provider_type not in provider_classes:
|
||||
logger.error(f"Unknown provider type: {provider_type}")
|
||||
return
|
||||
|
||||
provider_class = provider_classes[provider_type]
|
||||
provider = provider_class()
|
||||
|
||||
# Initialize the provider
|
||||
if not provider.initialize(config):
|
||||
logger.error(f"Failed to initialize sandbox provider: {provider_type}. Config keys: {list(config.keys())}")
|
||||
return
|
||||
|
||||
# Set the active provider
|
||||
_provider_manager.set_provider(provider_type, provider)
|
||||
logger.info(f"Sandbox provider '{provider_type}' initialized successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load sandbox provider from settings: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def reload_provider() -> None:
|
||||
"""
|
||||
Reload the sandbox provider from system settings.
|
||||
|
||||
Use this function when sandbox settings have been updated.
|
||||
"""
|
||||
global _provider_manager
|
||||
_provider_manager = None
|
||||
_load_provider_from_settings()
|
||||
|
||||
|
||||
def execute_code(
|
||||
code: str,
|
||||
language: str = "python",
|
||||
timeout: int = 30,
|
||||
arguments: Optional[Dict[str, Any]] = None
|
||||
) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in the configured sandbox.
|
||||
|
||||
This is the main entry point for agent components to execute code.
|
||||
|
||||
Args:
|
||||
code: Source code to execute
|
||||
language: Programming language (python, nodejs, javascript)
|
||||
timeout: Maximum execution time in seconds
|
||||
arguments: Optional arguments dict to pass to main() function
|
||||
|
||||
Returns:
|
||||
ExecutionResult containing stdout, stderr, exit_code, and metadata
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no provider is configured or execution fails
|
||||
"""
|
||||
provider_manager = get_provider_manager()
|
||||
|
||||
if not provider_manager.is_configured():
|
||||
raise RuntimeError(
|
||||
"No sandbox provider configured. Please configure sandbox settings in the admin panel."
|
||||
)
|
||||
|
||||
provider = provider_manager.get_provider()
|
||||
|
||||
# Create a sandbox instance
|
||||
instance = provider.create_instance(template=language)
|
||||
|
||||
try:
|
||||
# Execute the code
|
||||
result = provider.execute_code(
|
||||
instance_id=instance.instance_id,
|
||||
code=code,
|
||||
language=language,
|
||||
timeout=timeout,
|
||||
arguments=arguments
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
finally:
|
||||
# Clean up the instance
|
||||
try:
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to destroy sandbox instance {instance.instance_id}: {e}")
|
||||
|
||||
|
||||
def health_check() -> bool:
|
||||
"""
|
||||
Check if the sandbox provider is healthy.
|
||||
|
||||
Returns:
|
||||
True if provider is configured and healthy, False otherwise
|
||||
"""
|
||||
try:
|
||||
provider_manager = get_provider_manager()
|
||||
|
||||
if not provider_manager.is_configured():
|
||||
return False
|
||||
|
||||
provider = provider_manager.get_provider()
|
||||
return provider.health_check()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Sandbox health check failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_provider_info() -> Dict[str, Any]:
|
||||
"""
|
||||
Get information about the current sandbox provider.
|
||||
|
||||
Returns:
|
||||
Dictionary with provider information:
|
||||
- provider_type: Type of the active provider
|
||||
- configured: Whether provider is configured
|
||||
- healthy: Whether provider is healthy
|
||||
"""
|
||||
try:
|
||||
provider_manager = get_provider_manager()
|
||||
|
||||
return {
|
||||
"provider_type": provider_manager.get_provider_name(),
|
||||
"configured": provider_manager.is_configured(),
|
||||
"healthy": health_check(),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get provider info: {e}")
|
||||
return {
|
||||
"provider_type": None,
|
||||
"configured": False,
|
||||
"healthy": False,
|
||||
}
|
||||
32
agent/sandbox/docker-compose.yml
Normal file
32
agent/sandbox/docker-compose.yml
Normal file
@ -0,0 +1,32 @@
|
||||
services:
|
||||
sandbox-executor-manager:
|
||||
build:
|
||||
context: ./executor_manager
|
||||
dockerfile: Dockerfile
|
||||
image: sandbox-executor-manager:latest
|
||||
runtime: runc
|
||||
privileged: true
|
||||
ports:
|
||||
- "${EXECUTOR_PORT:-9385}:9385"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- sandbox-network
|
||||
restart: always
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
environment:
|
||||
- SANDBOX_EXECUTOR_MANAGER_POOL_SIZE=${SANDBOX_EXECUTOR_MANAGER_POOL_SIZE:-5}
|
||||
- SANDBOX_BASE_PYTHON_IMAGE=${SANDBOX_BASE_PYTHON_IMAGE-sandbox-base-python:latest}
|
||||
- SANDBOX_BASE_NODEJS_IMAGE=${SANDBOX_BASE_NODEJS_IMAGE-sandbox-base-nodejs:latest}
|
||||
- SANDBOX_ENABLE_SECCOMP=${SANDBOX_ENABLE_SECCOMP:-false}
|
||||
- SANDBOX_MAX_MEMORY=${SANDBOX_MAX_MEMORY:-256m} # b, k, m, g
|
||||
- SANDBOX_TIMEOUT=${SANDBOX_TIMEOUT:-10s} # s, m, 1m30s
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl --fail http://localhost:9385/healthz || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
sandbox-network:
|
||||
driver: bridge
|
||||
37
agent/sandbox/executor_manager/Dockerfile
Normal file
37
agent/sandbox/executor_manager/Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
RUN grep -rl 'deb.debian.org' /etc/apt/ | xargs sed -i 's|http[s]*://deb.debian.org|https://mirrors.tuna.tsinghua.edu.cn|g' && \
|
||||
apt-get update && \
|
||||
apt-get install -y curl gcc && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
RUN set -eux; \
|
||||
case "${TARGETARCH}${TARGETVARIANT}" in \
|
||||
amd64) DOCKER_ARCH=x86_64 ;; \
|
||||
arm64) DOCKER_ARCH=aarch64 ;; \
|
||||
armv7) DOCKER_ARCH=armhf ;; \
|
||||
armv6) DOCKER_ARCH=armel ;; \
|
||||
arm64v8) DOCKER_ARCH=aarch64 ;; \
|
||||
arm64v7) DOCKER_ARCH=armhf ;; \
|
||||
arm*) DOCKER_ARCH=armhf ;; \
|
||||
ppc64le) DOCKER_ARCH=ppc64le ;; \
|
||||
s390x) DOCKER_ARCH=s390x ;; \
|
||||
*) echo "Unsupported architecture: ${TARGETARCH}${TARGETVARIANT}" && exit 1 ;; \
|
||||
esac; \
|
||||
echo "Downloading Docker for architecture: ${DOCKER_ARCH}"; \
|
||||
curl -fsSL "https://download.docker.com/linux/static/stable/${DOCKER_ARCH}/docker-29.1.0.tgz" | \
|
||||
tar xz -C /usr/local/bin --strip-components=1 docker/docker; \
|
||||
ln -sf /usr/local/bin/docker /usr/bin/docker
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.7.5 /uv /uvx /bin/
|
||||
ENV UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
RUN uv pip install --system -r requirements.txt
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9385"]
|
||||
15
agent/sandbox/executor_manager/api/__init__.py
Normal file
15
agent/sandbox/executor_manager/api/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
49
agent/sandbox/executor_manager/api/handlers.py
Normal file
49
agent/sandbox/executor_manager/api/handlers.py
Normal file
@ -0,0 +1,49 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import base64
|
||||
|
||||
from core.container import _CONTAINER_EXECUTION_SEMAPHORES
|
||||
from core.logger import logger
|
||||
from fastapi import Request
|
||||
from models.enums import ResultStatus, SupportLanguage
|
||||
from models.schemas import CodeExecutionRequest, CodeExecutionResult
|
||||
from services.execution import execute_code
|
||||
from services.limiter import limiter
|
||||
from services.security import analyze_code_security
|
||||
|
||||
|
||||
async def healthz_handler():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@limiter.limit("5/second")
|
||||
async def run_code_handler(req: CodeExecutionRequest, request: Request):
|
||||
logger.info("🟢 Received /run request")
|
||||
|
||||
async with _CONTAINER_EXECUTION_SEMAPHORES[req.language]:
|
||||
code = base64.b64decode(req.code_b64).decode("utf-8")
|
||||
if req.language == SupportLanguage.NODEJS:
|
||||
code += "\n\nmodule.exports = { main };"
|
||||
req.code_b64 = base64.b64encode(code.encode("utf-8")).decode("utf-8")
|
||||
is_safe, issues = analyze_code_security(code, language=req.language)
|
||||
if not is_safe:
|
||||
issue_details = "\n".join([f"Line {lineno}: {issue}" for issue, lineno in issues])
|
||||
return CodeExecutionResult(status=ResultStatus.PROGRAM_RUNNER_ERROR, stdout="", stderr=issue_details, exit_code=-999, detail="Code is unsafe")
|
||||
|
||||
try:
|
||||
return await execute_code(req)
|
||||
except Exception as e:
|
||||
return CodeExecutionResult(status=ResultStatus.PROGRAM_RUNNER_ERROR, stdout="", stderr=str(e), exit_code=-999, detail="unhandled_exception")
|
||||
24
agent/sandbox/executor_manager/api/routes.py
Normal file
24
agent/sandbox/executor_manager/api/routes.py
Normal file
@ -0,0 +1,24 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from fastapi import APIRouter
|
||||
|
||||
from api.handlers import healthz_handler, run_code_handler
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.get("/healthz")(healthz_handler)
|
||||
router.post("/run")(run_code_handler)
|
||||
|
||||
15
agent/sandbox/executor_manager/core/__init__.py
Normal file
15
agent/sandbox/executor_manager/core/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
43
agent/sandbox/executor_manager/core/config.py
Normal file
43
agent/sandbox/executor_manager/core/config.py
Normal file
@ -0,0 +1,43 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from util import format_timeout_duration, parse_timeout_duration
|
||||
|
||||
from core.container import init_containers, teardown_containers
|
||||
from core.logger import logger
|
||||
|
||||
TIMEOUT = parse_timeout_duration(os.getenv("SANDBOX_TIMEOUT", "10s"))
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _lifespan(app: FastAPI):
|
||||
"""Asynchronous lifecycle management"""
|
||||
size = int(os.getenv("SANDBOX_EXECUTOR_MANAGER_POOL_SIZE", 1))
|
||||
|
||||
success_count, total_task_count = await init_containers(size)
|
||||
logger.info(f"\n📊 Container pool initialization complete: {success_count}/{total_task_count} available")
|
||||
|
||||
yield
|
||||
|
||||
await teardown_containers()
|
||||
|
||||
|
||||
def init():
|
||||
logger.info(f"Global timeout: {format_timeout_duration(TIMEOUT)}")
|
||||
return _lifespan
|
||||
191
agent/sandbox/executor_manager/core/container.py
Normal file
191
agent/sandbox/executor_manager/core/container.py
Normal file
@ -0,0 +1,191 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
from queue import Empty, Queue
|
||||
|
||||
from models.enums import SupportLanguage
|
||||
from util import env_setting_enabled, is_valid_memory_limit
|
||||
from utils.common import async_run_command
|
||||
|
||||
from core.logger import logger
|
||||
|
||||
_CONTAINER_QUEUES: dict[SupportLanguage, Queue] = {}
|
||||
_CONTAINER_LOCK: asyncio.Lock = asyncio.Lock()
|
||||
_CONTAINER_EXECUTION_SEMAPHORES: dict[SupportLanguage, asyncio.Semaphore] = {}
|
||||
|
||||
|
||||
async def init_containers(size: int) -> tuple[int, int]:
|
||||
global _CONTAINER_QUEUES
|
||||
_CONTAINER_QUEUES = {SupportLanguage.PYTHON: Queue(), SupportLanguage.NODEJS: Queue()}
|
||||
|
||||
async with _CONTAINER_LOCK:
|
||||
while not _CONTAINER_QUEUES[SupportLanguage.PYTHON].empty():
|
||||
_CONTAINER_QUEUES[SupportLanguage.PYTHON].get_nowait()
|
||||
while not _CONTAINER_QUEUES[SupportLanguage.NODEJS].empty():
|
||||
_CONTAINER_QUEUES[SupportLanguage.NODEJS].get_nowait()
|
||||
|
||||
for language in SupportLanguage:
|
||||
_CONTAINER_EXECUTION_SEMAPHORES[language] = asyncio.Semaphore(size)
|
||||
|
||||
create_tasks = []
|
||||
for i in range(size):
|
||||
name = f"sandbox_python_{i}"
|
||||
logger.info(f"🛠️ Creating Python container {i + 1}/{size}")
|
||||
create_tasks.append(_prepare_container(name, SupportLanguage.PYTHON))
|
||||
|
||||
name = f"sandbox_nodejs_{i}"
|
||||
logger.info(f"🛠️ Creating Node.js container {i + 1}/{size}")
|
||||
create_tasks.append(_prepare_container(name, SupportLanguage.NODEJS))
|
||||
|
||||
results = await asyncio.gather(*create_tasks, return_exceptions=True)
|
||||
success_count = sum(1 for r in results if r is True)
|
||||
total_task_count = len(create_tasks)
|
||||
return success_count, total_task_count
|
||||
|
||||
|
||||
async def teardown_containers():
|
||||
async with _CONTAINER_LOCK:
|
||||
while not _CONTAINER_QUEUES[SupportLanguage.PYTHON].empty():
|
||||
name = _CONTAINER_QUEUES[SupportLanguage.PYTHON].get_nowait()
|
||||
await async_run_command("docker", "rm", "-f", name, timeout=5)
|
||||
while not _CONTAINER_QUEUES[SupportLanguage.NODEJS].empty():
|
||||
name = _CONTAINER_QUEUES[SupportLanguage.NODEJS].get_nowait()
|
||||
await async_run_command("docker", "rm", "-f", name, timeout=5)
|
||||
|
||||
|
||||
async def _prepare_container(name: str, language: SupportLanguage) -> bool:
|
||||
"""Prepare a single container"""
|
||||
with contextlib.suppress(Exception):
|
||||
await async_run_command("docker", "rm", "-f", name, timeout=5)
|
||||
|
||||
if await create_container(name, language):
|
||||
_CONTAINER_QUEUES[language].put(name)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def create_container(name: str, language: SupportLanguage) -> bool:
|
||||
"""Asynchronously create a container"""
|
||||
create_args = [
|
||||
"docker",
|
||||
"run",
|
||||
"-d",
|
||||
"--runtime=runsc",
|
||||
"--name",
|
||||
name,
|
||||
"--read-only",
|
||||
"--tmpfs",
|
||||
"/workspace:rw,exec,size=100M,uid=65534,gid=65534",
|
||||
"--tmpfs",
|
||||
"/tmp:rw,exec,size=50M",
|
||||
"--user",
|
||||
"nobody",
|
||||
"--workdir",
|
||||
"/workspace",
|
||||
]
|
||||
if os.getenv("SANDBOX_MAX_MEMORY"):
|
||||
memory_limit = os.getenv("SANDBOX_MAX_MEMORY") or "256m"
|
||||
if is_valid_memory_limit(memory_limit):
|
||||
logger.info(f"SANDBOX_MAX_MEMORY: {os.getenv('SANDBOX_MAX_MEMORY')}")
|
||||
else:
|
||||
logger.info("Invalid SANDBOX_MAX_MEMORY, using default value: 256m")
|
||||
memory_limit = "256m"
|
||||
create_args.extend(["--memory", memory_limit])
|
||||
else:
|
||||
logger.info("Set default SANDBOX_MAX_MEMORY: 256m")
|
||||
create_args.extend(["--memory", "256m"])
|
||||
|
||||
if env_setting_enabled("SANDBOX_ENABLE_SECCOMP", "false"):
|
||||
logger.info(f"SANDBOX_ENABLE_SECCOMP: {os.getenv('SANDBOX_ENABLE_SECCOMP')}")
|
||||
create_args.extend(["--security-opt", "seccomp=/app/seccomp-profile-default.json"])
|
||||
|
||||
if language == SupportLanguage.PYTHON:
|
||||
create_args.append(os.getenv("SANDBOX_BASE_PYTHON_IMAGE", "sandbox-base-python:latest"))
|
||||
elif language == SupportLanguage.NODEJS:
|
||||
create_args.append(os.getenv("SANDBOX_BASE_NODEJS_IMAGE", "sandbox-base-nodejs:latest"))
|
||||
|
||||
logger.info(f"Sandbox config:\n\t {create_args}")
|
||||
|
||||
try:
|
||||
return_code, _, stderr = await async_run_command(*create_args, timeout=10)
|
||||
if return_code != 0:
|
||||
logger.error(f"❌ Container creation failed {name}: {stderr}")
|
||||
return False
|
||||
|
||||
if language == SupportLanguage.NODEJS:
|
||||
copy_cmd = ["docker", "exec", name, "bash", "-c", "cp -a /app/node_modules /workspace/"]
|
||||
return_code, _, stderr = await async_run_command(*copy_cmd, timeout=10)
|
||||
if return_code != 0:
|
||||
logger.error(f"❌ Failed to prepare dependencies for {name}: {stderr}")
|
||||
return False
|
||||
|
||||
return await container_is_running(name)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Container creation exception {name}: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def recreate_container(name: str, language: SupportLanguage) -> bool:
|
||||
"""Asynchronously recreate a container"""
|
||||
logger.info(f"🛠️ Recreating container: {name}")
|
||||
try:
|
||||
await async_run_command("docker", "rm", "-f", name, timeout=5)
|
||||
|
||||
return await create_container(name, language)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Container {name} recreation failed: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def release_container(name: str, language: SupportLanguage):
|
||||
"""Asynchronously release a container"""
|
||||
async with _CONTAINER_LOCK:
|
||||
if await container_is_running(name):
|
||||
_CONTAINER_QUEUES[language].put(name)
|
||||
logger.info(f"🟢 Released container: {name} (remaining available: {_CONTAINER_QUEUES[language].qsize()})")
|
||||
else:
|
||||
logger.warning(f"⚠️ Container {name} has crashed, attempting to recreate...")
|
||||
if await recreate_container(name, language):
|
||||
_CONTAINER_QUEUES[language].put(name)
|
||||
logger.info(f"✅ Container {name} successfully recreated and returned to queue")
|
||||
|
||||
|
||||
async def allocate_container_blocking(language: SupportLanguage, timeout=10) -> str:
|
||||
"""Asynchronously allocate an available container"""
|
||||
start_time = asyncio.get_running_loop().time()
|
||||
while asyncio.get_running_loop().time() - start_time < timeout:
|
||||
try:
|
||||
name = _CONTAINER_QUEUES[language].get_nowait()
|
||||
async with _CONTAINER_LOCK:
|
||||
if not await container_is_running(name) and not await recreate_container(name, language):
|
||||
continue
|
||||
|
||||
return name
|
||||
except Empty:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
async def container_is_running(name: str) -> bool:
|
||||
"""Asynchronously check the container status"""
|
||||
try:
|
||||
return_code, stdout, _ = await async_run_command("docker", "inspect", "-f", "{{.State.Running}}", name, timeout=2)
|
||||
return return_code == 0 and stdout.strip() == "true"
|
||||
except Exception:
|
||||
return False
|
||||
19
agent/sandbox/executor_manager/core/logger.py
Normal file
19
agent/sandbox/executor_manager/core/logger.py
Normal file
@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("sandbox")
|
||||
25
agent/sandbox/executor_manager/main.py
Normal file
25
agent/sandbox/executor_manager/main.py
Normal file
@ -0,0 +1,25 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from api.routes import router as api_router
|
||||
from core.config import init
|
||||
from fastapi import FastAPI
|
||||
from services.limiter import limiter, rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
|
||||
app = FastAPI(lifespan=init())
|
||||
app.include_router(api_router)
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler)
|
||||
15
agent/sandbox/executor_manager/models/__init__.py
Normal file
15
agent/sandbox/executor_manager/models/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
47
agent/sandbox/executor_manager/models/enums.py
Normal file
47
agent/sandbox/executor_manager/models/enums.py
Normal file
@ -0,0 +1,47 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SupportLanguage(str, Enum):
|
||||
PYTHON = "python"
|
||||
NODEJS = "nodejs"
|
||||
|
||||
|
||||
class ResultStatus(str, Enum):
|
||||
SUCCESS = "success"
|
||||
PROGRAM_ERROR = "program_error"
|
||||
RESOURCE_LIMIT_EXCEEDED = "resource_limit_exceeded"
|
||||
UNAUTHORIZED_ACCESS = "unauthorized_access"
|
||||
RUNTIME_ERROR = "runtime_error"
|
||||
PROGRAM_RUNNER_ERROR = "program_runner_error"
|
||||
|
||||
|
||||
class ResourceLimitType(str, Enum):
|
||||
TIME = "time"
|
||||
MEMORY = "memory"
|
||||
OUTPUT = "output"
|
||||
|
||||
|
||||
class UnauthorizedAccessType(str, Enum):
|
||||
DISALLOWED_SYSCALL = "disallowed_syscall"
|
||||
FILE_ACCESS = "file_access"
|
||||
NETWORK_ACCESS = "network_access"
|
||||
|
||||
|
||||
class RuntimeErrorType(str, Enum):
|
||||
SIGNALLED = "signalled"
|
||||
NONZERO_EXIT = "nonzero_exit"
|
||||
53
agent/sandbox/executor_manager/models/schemas.py
Normal file
53
agent/sandbox/executor_manager/models/schemas.py
Normal file
@ -0,0 +1,53 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import base64
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from models.enums import ResourceLimitType, ResultStatus, RuntimeErrorType, SupportLanguage, UnauthorizedAccessType
|
||||
|
||||
|
||||
class CodeExecutionResult(BaseModel):
|
||||
status: ResultStatus
|
||||
stdout: str
|
||||
stderr: str
|
||||
exit_code: int
|
||||
detail: Optional[str] = None
|
||||
|
||||
# Resource usage
|
||||
time_used_ms: Optional[float] = None
|
||||
memory_used_kb: Optional[float] = None
|
||||
|
||||
# Error details
|
||||
resource_limit_type: Optional[ResourceLimitType] = None
|
||||
unauthorized_access_type: Optional[UnauthorizedAccessType] = None
|
||||
runtime_error_type: Optional[RuntimeErrorType] = None
|
||||
|
||||
|
||||
class CodeExecutionRequest(BaseModel):
|
||||
code_b64: str = Field(..., description="Base64 encoded code string")
|
||||
language: SupportLanguage = Field(default=SupportLanguage.PYTHON, description="Programming language")
|
||||
arguments: Optional[dict] = Field(default={}, description="Arguments")
|
||||
|
||||
@field_validator("code_b64")
|
||||
@classmethod
|
||||
def validate_base64(cls, v: str) -> str:
|
||||
try:
|
||||
base64.b64decode(v, validate=True)
|
||||
return v
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid base64 encoding: {str(e)}")
|
||||
3
agent/sandbox/executor_manager/requirements.txt
Normal file
3
agent/sandbox/executor_manager/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
slowapi
|
||||
55
agent/sandbox/executor_manager/seccomp-profile-default.json
Normal file
55
agent/sandbox/executor_manager/seccomp-profile-default.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"defaultAction": "SCMP_ACT_ERRNO",
|
||||
"archMap": [
|
||||
{
|
||||
"architecture": "SCMP_ARCH_X86_64",
|
||||
"subArchitectures": [
|
||||
"SCMP_ARCH_X86",
|
||||
"SCMP_ARCH_X32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"syscalls": [
|
||||
{
|
||||
"names": [
|
||||
"read",
|
||||
"write",
|
||||
"exit",
|
||||
"sigreturn",
|
||||
"brk",
|
||||
"mmap",
|
||||
"munmap",
|
||||
"rt_sigaction",
|
||||
"rt_sigprocmask",
|
||||
"futex",
|
||||
"clone",
|
||||
"execve",
|
||||
"arch_prctl",
|
||||
"access",
|
||||
"openat",
|
||||
"close",
|
||||
"stat",
|
||||
"fstat",
|
||||
"lstat",
|
||||
"getpid",
|
||||
"gettid",
|
||||
"getuid",
|
||||
"getgid",
|
||||
"geteuid",
|
||||
"getegid",
|
||||
"clock_gettime",
|
||||
"nanosleep",
|
||||
"uname",
|
||||
"writev",
|
||||
"readlink",
|
||||
"getrandom",
|
||||
"statx",
|
||||
"faccessat2",
|
||||
"pread64",
|
||||
"pwrite64",
|
||||
"rt_sigreturn"
|
||||
],
|
||||
"action": "SCMP_ACT_ALLOW"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
agent/sandbox/executor_manager/services/__init__.py
Normal file
15
agent/sandbox/executor_manager/services/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
265
agent/sandbox/executor_manager/services/execution.py
Normal file
265
agent/sandbox/executor_manager/services/execution.py
Normal file
@ -0,0 +1,265 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from core.config import TIMEOUT
|
||||
from core.container import allocate_container_blocking, release_container
|
||||
from core.logger import logger
|
||||
from models.enums import ResourceLimitType, ResultStatus, RuntimeErrorType, SupportLanguage, UnauthorizedAccessType
|
||||
from models.schemas import CodeExecutionRequest, CodeExecutionResult
|
||||
from utils.common import async_run_command
|
||||
|
||||
|
||||
async def execute_code(req: CodeExecutionRequest):
|
||||
"""Fully asynchronous execution logic"""
|
||||
language = req.language
|
||||
container = await allocate_container_blocking(language)
|
||||
if not container:
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.PROGRAM_RUNNER_ERROR,
|
||||
stdout="",
|
||||
stderr="Container pool is busy",
|
||||
exit_code=-10,
|
||||
detail="no_available_container",
|
||||
)
|
||||
|
||||
task_id = str(uuid.uuid4())
|
||||
workdir = f"/tmp/sandbox_{task_id}"
|
||||
os.makedirs(workdir, mode=0o700, exist_ok=True)
|
||||
|
||||
try:
|
||||
if language == SupportLanguage.PYTHON:
|
||||
code_name = "main.py"
|
||||
# code
|
||||
code_path = os.path.join(workdir, code_name)
|
||||
with open(code_path, "wb") as f:
|
||||
f.write(base64.b64decode(req.code_b64))
|
||||
# runner
|
||||
runner_name = "runner.py"
|
||||
runner_path = os.path.join(workdir, runner_name)
|
||||
with open(runner_path, "w") as f:
|
||||
f.write("""import json
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from main import main
|
||||
if __name__ == "__main__":
|
||||
args = json.loads(sys.argv[1])
|
||||
result = main(**args)
|
||||
if result is not None:
|
||||
print(result)
|
||||
""")
|
||||
|
||||
elif language == SupportLanguage.NODEJS:
|
||||
code_name = "main.js"
|
||||
code_path = os.path.join(workdir, "main.js")
|
||||
with open(code_path, "wb") as f:
|
||||
f.write(base64.b64decode(req.code_b64))
|
||||
|
||||
runner_name = "runner.js"
|
||||
runner_path = os.path.join(workdir, "runner.js")
|
||||
with open(runner_path, "w") as f:
|
||||
f.write("""
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const args = JSON.parse(process.argv[2]);
|
||||
const mainPath = path.join(__dirname, 'main.js');
|
||||
|
||||
function isPromise(value) {
|
||||
return Boolean(value && typeof value.then === 'function');
|
||||
}
|
||||
|
||||
if (fs.existsSync(mainPath)) {
|
||||
const mod = require(mainPath);
|
||||
const main = typeof mod === 'function' ? mod : mod.main;
|
||||
|
||||
if (typeof main !== 'function') {
|
||||
console.error('Error: main is not a function');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (typeof args === 'object' && args !== null) {
|
||||
try {
|
||||
const result = main(args);
|
||||
if (isPromise(result)) {
|
||||
result.then(output => {
|
||||
if (output !== null) {
|
||||
console.log(output);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Error in async main function:', err);
|
||||
});
|
||||
} else {
|
||||
if (result !== null) {
|
||||
console.log(result);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error when executing main:', err);
|
||||
}
|
||||
} else {
|
||||
console.error('Error: args is not a valid object:', args);
|
||||
}
|
||||
} else {
|
||||
console.error('main.js not found in the current directory');
|
||||
}
|
||||
""")
|
||||
# dirs
|
||||
returncode, _, stderr = await async_run_command("docker", "exec", container, "mkdir", "-p", f"/workspace/{task_id}", timeout=5)
|
||||
if returncode != 0:
|
||||
raise RuntimeError(f"Directory creation failed: {stderr}")
|
||||
|
||||
# archive
|
||||
tar_proc = await asyncio.create_subprocess_exec("tar", "czf", "-", "-C", workdir, code_name, runner_name, stdout=asyncio.subprocess.PIPE)
|
||||
tar_stdout, _ = await tar_proc.communicate()
|
||||
|
||||
# unarchive
|
||||
docker_proc = await asyncio.create_subprocess_exec(
|
||||
"docker", "exec", "-i", container, "tar", "xzf", "-", "-C", f"/workspace/{task_id}", stdin=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await docker_proc.communicate(input=tar_stdout)
|
||||
|
||||
if docker_proc.returncode != 0:
|
||||
raise RuntimeError(stderr.decode())
|
||||
|
||||
# exec
|
||||
start_time = time.time()
|
||||
try:
|
||||
logger.info(f"Passed in args: {req.arguments}")
|
||||
args_json = json.dumps(req.arguments or {})
|
||||
run_args = [
|
||||
"docker",
|
||||
"exec",
|
||||
"--workdir",
|
||||
f"/workspace/{task_id}",
|
||||
container,
|
||||
"timeout",
|
||||
str(TIMEOUT),
|
||||
language,
|
||||
]
|
||||
# flags
|
||||
if language == SupportLanguage.PYTHON:
|
||||
run_args.extend(["-I", "-B"])
|
||||
elif language == SupportLanguage.NODEJS:
|
||||
run_args.extend([])
|
||||
else:
|
||||
assert False, "Will never reach here"
|
||||
run_args.extend([runner_name, args_json])
|
||||
|
||||
returncode, stdout, stderr = await async_run_command(
|
||||
*run_args,
|
||||
timeout=TIMEOUT + 5,
|
||||
)
|
||||
|
||||
time_used_ms = (time.time() - start_time) * 1000
|
||||
|
||||
logger.info("----------------------------------------------")
|
||||
logger.info(f"Code: {str(base64.b64decode(req.code_b64))}")
|
||||
logger.info(f"{returncode=}")
|
||||
logger.info(f"{stdout=}")
|
||||
logger.info(f"{stderr=}")
|
||||
logger.info(f"{args_json=}")
|
||||
|
||||
if returncode == 0:
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.SUCCESS,
|
||||
stdout=str(stdout),
|
||||
stderr=stderr,
|
||||
exit_code=0,
|
||||
time_used_ms=time_used_ms,
|
||||
)
|
||||
elif returncode == 124:
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.RESOURCE_LIMIT_EXCEEDED,
|
||||
stdout="",
|
||||
stderr="Execution timeout",
|
||||
exit_code=-124,
|
||||
resource_limit_type=ResourceLimitType.TIME,
|
||||
time_used_ms=time_used_ms,
|
||||
)
|
||||
elif returncode == 137:
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.RESOURCE_LIMIT_EXCEEDED,
|
||||
stdout="",
|
||||
stderr="Memory limit exceeded (killed by OOM)",
|
||||
exit_code=-137,
|
||||
resource_limit_type=ResourceLimitType.MEMORY,
|
||||
time_used_ms=time_used_ms,
|
||||
)
|
||||
return analyze_error_result(stderr, returncode)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
await async_run_command("docker", "exec", container, "pkill", "-9", language)
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.RESOURCE_LIMIT_EXCEEDED,
|
||||
stdout="",
|
||||
stderr="Execution timeout",
|
||||
exit_code=-1,
|
||||
resource_limit_type=ResourceLimitType.TIME,
|
||||
time_used_ms=(time.time() - start_time) * 1000,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Execution exception: {str(e)}")
|
||||
return CodeExecutionResult(status=ResultStatus.PROGRAM_RUNNER_ERROR, stdout="", stderr=str(e), exit_code=-3, detail="internal_error")
|
||||
|
||||
finally:
|
||||
# cleanup
|
||||
cleanup_tasks = [async_run_command("docker", "exec", container, "rm", "-rf", f"/workspace/{task_id}"), async_run_command("rm", "-rf", workdir)]
|
||||
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
||||
await release_container(container, language)
|
||||
|
||||
|
||||
def analyze_error_result(stderr: str, exit_code: int) -> CodeExecutionResult:
|
||||
"""Analyze the error result and classify it"""
|
||||
if "Permission denied" in stderr:
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.UNAUTHORIZED_ACCESS,
|
||||
stdout="",
|
||||
stderr=stderr,
|
||||
exit_code=exit_code,
|
||||
unauthorized_access_type=UnauthorizedAccessType.FILE_ACCESS,
|
||||
)
|
||||
elif "Operation not permitted" in stderr:
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.UNAUTHORIZED_ACCESS,
|
||||
stdout="",
|
||||
stderr=stderr,
|
||||
exit_code=exit_code,
|
||||
unauthorized_access_type=UnauthorizedAccessType.DISALLOWED_SYSCALL,
|
||||
)
|
||||
elif "MemoryError" in stderr:
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.RESOURCE_LIMIT_EXCEEDED,
|
||||
stdout="",
|
||||
stderr=stderr,
|
||||
exit_code=exit_code,
|
||||
resource_limit_type=ResourceLimitType.MEMORY,
|
||||
)
|
||||
else:
|
||||
return CodeExecutionResult(
|
||||
status=ResultStatus.PROGRAM_ERROR,
|
||||
stdout="",
|
||||
stderr=stderr,
|
||||
exit_code=exit_code,
|
||||
runtime_error_type=RuntimeErrorType.NONZERO_EXIT,
|
||||
)
|
||||
38
agent/sandbox/executor_manager/services/limiter.py
Normal file
38
agent/sandbox/executor_manager/services/limiter.py
Normal file
@ -0,0 +1,38 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from models.enums import ResultStatus
|
||||
from models.schemas import CodeExecutionResult
|
||||
from slowapi import Limiter
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
|
||||
async def rate_limit_exceeded_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
if isinstance(exc, RateLimitExceeded):
|
||||
return JSONResponse(
|
||||
content=CodeExecutionResult(
|
||||
status=ResultStatus.PROGRAM_RUNNER_ERROR,
|
||||
stdout="",
|
||||
stderr="Too many requests, please try again later",
|
||||
exit_code=-429,
|
||||
detail="Too many requests, please try again later",
|
||||
).model_dump(),
|
||||
)
|
||||
raise exc
|
||||
173
agent/sandbox/executor_manager/services/security.py
Normal file
173
agent/sandbox/executor_manager/services/security.py
Normal file
@ -0,0 +1,173 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import ast
|
||||
from typing import List, Tuple
|
||||
|
||||
from core.logger import logger
|
||||
from models.enums import SupportLanguage
|
||||
|
||||
|
||||
class SecurePythonAnalyzer(ast.NodeVisitor):
|
||||
"""
|
||||
An AST-based analyzer for detecting unsafe Python code patterns.
|
||||
"""
|
||||
|
||||
DANGEROUS_IMPORTS = {"os", "subprocess", "sys", "shutil", "socket", "ctypes", "pickle", "threading", "multiprocessing", "asyncio", "http.client", "ftplib", "telnetlib"}
|
||||
|
||||
DANGEROUS_CALLS = {
|
||||
"eval",
|
||||
"exec",
|
||||
"open",
|
||||
"__import__",
|
||||
"compile",
|
||||
"input",
|
||||
"system",
|
||||
"popen",
|
||||
"remove",
|
||||
"rename",
|
||||
"rmdir",
|
||||
"chdir",
|
||||
"chmod",
|
||||
"chown",
|
||||
"getattr",
|
||||
"setattr",
|
||||
"globals",
|
||||
"locals",
|
||||
"shutil.rmtree",
|
||||
"subprocess.call",
|
||||
"subprocess.Popen",
|
||||
"ctypes",
|
||||
"pickle.load",
|
||||
"pickle.loads",
|
||||
"pickle.dump",
|
||||
"pickle.dumps",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.unsafe_items: List[Tuple[str, int]] = []
|
||||
|
||||
def visit_Import(self, node: ast.Import):
|
||||
"""Check for dangerous imports."""
|
||||
for alias in node.names:
|
||||
if alias.name.split(".")[0] in self.DANGEROUS_IMPORTS:
|
||||
self.unsafe_items.append((f"Import: {alias.name}", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom):
|
||||
"""Check for dangerous imports from specific modules."""
|
||||
if node.module and node.module.split(".")[0] in self.DANGEROUS_IMPORTS:
|
||||
self.unsafe_items.append((f"From Import: {node.module}", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Call(self, node: ast.Call):
|
||||
"""Check for dangerous function calls."""
|
||||
if isinstance(node.func, ast.Name) and node.func.id in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append((f"Call: {node.func.id}", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Attribute(self, node: ast.Attribute):
|
||||
"""Check for dangerous attribute access."""
|
||||
if isinstance(node.value, ast.Name) and node.value.id in self.DANGEROUS_IMPORTS:
|
||||
self.unsafe_items.append((f"Attribute Access: {node.value.id}.{node.attr}", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_BinOp(self, node: ast.BinOp):
|
||||
"""Check for possible unsafe operations like concatenating strings with commands."""
|
||||
# This could be useful to detect `eval("os." + "system")`
|
||||
if isinstance(node.left, ast.Constant) and isinstance(node.right, ast.Constant):
|
||||
self.unsafe_items.append(("Possible unsafe string concatenation", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef):
|
||||
"""Check for dangerous function definitions (e.g., user-defined eval)."""
|
||||
if node.name in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append((f"Function Definition: {node.name}", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Assign(self, node: ast.Assign):
|
||||
"""Check for assignments to variables that might lead to dangerous operations."""
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append((f"Assignment to dangerous variable: {target.id}", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Lambda(self, node: ast.Lambda):
|
||||
"""Check for lambda functions with dangerous operations."""
|
||||
if isinstance(node.body, ast.Call) and isinstance(node.body.func, ast.Name) and node.body.func.id in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append(("Lambda with dangerous function call", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ListComp(self, node: ast.ListComp):
|
||||
"""Check for list comprehensions with dangerous operations."""
|
||||
# First, visit the generators to check for any issues there
|
||||
for elem in node.generators:
|
||||
if isinstance(elem, ast.comprehension):
|
||||
self.generic_visit(elem)
|
||||
|
||||
if isinstance(node.elt, ast.Call) and isinstance(node.elt.func, ast.Name) and node.elt.func.id in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append(("List comprehension with dangerous function call", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_DictComp(self, node: ast.DictComp):
|
||||
"""Check for dictionary comprehensions with dangerous operations."""
|
||||
# Check for dangerous calls in both the key and value expressions of the dictionary comprehension
|
||||
if isinstance(node.key, ast.Call) and isinstance(node.key.func, ast.Name) and node.key.func.id in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append(("Dict comprehension with dangerous function call in key", node.lineno))
|
||||
|
||||
if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) and node.value.func.id in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append(("Dict comprehension with dangerous function call in value", node.lineno))
|
||||
|
||||
# Visit other sub-nodes (e.g., the generators in the comprehension)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_SetComp(self, node: ast.SetComp):
|
||||
"""Check for set comprehensions with dangerous operations."""
|
||||
for elt in node.generators:
|
||||
if isinstance(elt, ast.comprehension):
|
||||
self.generic_visit(elt)
|
||||
|
||||
if isinstance(node.elt, ast.Call) and isinstance(node.elt.func, ast.Name) and node.elt.func.id in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append(("Set comprehension with dangerous function call", node.lineno))
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Yield(self, node: ast.Yield):
|
||||
"""Check for yield statements that could be used to produce unsafe values."""
|
||||
if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) and node.value.func.id in self.DANGEROUS_CALLS:
|
||||
self.unsafe_items.append(("Yield with dangerous function call", node.lineno))
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
def analyze_code_security(code: str, language: SupportLanguage) -> Tuple[bool, List[Tuple[str, int]]]:
|
||||
"""
|
||||
Analyze the provided code string and return whether it's safe and why.
|
||||
|
||||
:param code: The source code to analyze.
|
||||
:param language: The programming language of the code.
|
||||
:return: (is_safe: bool, issues: List of (description, line number))
|
||||
"""
|
||||
if language == SupportLanguage.PYTHON:
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
analyzer = SecurePythonAnalyzer()
|
||||
analyzer.visit(tree)
|
||||
return len(analyzer.unsafe_items) == 0, analyzer.unsafe_items
|
||||
except Exception as e:
|
||||
logger.error(f"[SafeCheck] Python parsing failed: {str(e)}")
|
||||
return False, [(f"Parsing Error: {str(e)}", -1)]
|
||||
else:
|
||||
logger.warning(f"[SafeCheck] Unsupported language for security analysis: {language} — defaulting to SAFE (manual review recommended)")
|
||||
return True, [(f"Unsupported language for security analysis: {language} — defaulted to SAFE, manual review recommended", -1)]
|
||||
76
agent/sandbox/executor_manager/util.py
Normal file
76
agent/sandbox/executor_manager/util.py
Normal file
@ -0,0 +1,76 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def is_enabled(value: str) -> bool:
|
||||
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def env_setting_enabled(env_key: str, default: str = "false") -> bool:
|
||||
value = os.getenv(env_key, default)
|
||||
return is_enabled(value)
|
||||
|
||||
|
||||
def is_valid_memory_limit(mem: str | None) -> bool:
|
||||
"""
|
||||
Return True if the input string is a valid Docker memory limit (e.g. '256m', '1g').
|
||||
Units allowed: b, k, m, g (case-insensitive).
|
||||
Disallows zero or negative values.
|
||||
"""
|
||||
if not mem or not isinstance(mem, str):
|
||||
return False
|
||||
|
||||
mem = mem.strip().lower()
|
||||
|
||||
return re.fullmatch(r"[1-9]\d*(b|k|m|g)", mem) is not None
|
||||
|
||||
|
||||
def parse_timeout_duration(timeout: str | None, default_seconds: int = 10) -> int:
|
||||
"""
|
||||
Parses a string like '90s', '2m', '1m30s' into total seconds (int).
|
||||
Supports 's', 'm' (lower or upper case). Returns default if invalid.
|
||||
'1m30s' -> 90
|
||||
"""
|
||||
if not timeout or not isinstance(timeout, str):
|
||||
return default_seconds
|
||||
|
||||
timeout = timeout.strip().lower()
|
||||
|
||||
pattern = r"^(?:(\d+)m)?(?:(\d+)s)?$"
|
||||
match = re.fullmatch(pattern, timeout)
|
||||
if not match:
|
||||
return default_seconds
|
||||
|
||||
minutes = int(match.group(1)) if match.group(1) else 0
|
||||
seconds = int(match.group(2)) if match.group(2) else 0
|
||||
total = minutes * 60 + seconds
|
||||
|
||||
return total if total > 0 else default_seconds
|
||||
|
||||
|
||||
def format_timeout_duration(seconds: int) -> str:
|
||||
"""
|
||||
Formats an integer number of seconds into a string like '1m30s'.
|
||||
90 -> '1m30s'
|
||||
"""
|
||||
if seconds < 60:
|
||||
return f"{seconds}s"
|
||||
minutes, sec = divmod(seconds, 60)
|
||||
if sec == 0:
|
||||
return f"{minutes}m"
|
||||
return f"{minutes}m{sec}s"
|
||||
15
agent/sandbox/executor_manager/utils/__init__.py
Normal file
15
agent/sandbox/executor_manager/utils/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
36
agent/sandbox/executor_manager/utils/common.py
Normal file
36
agent/sandbox/executor_manager/utils/common.py
Normal file
@ -0,0 +1,36 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import asyncio
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
async def async_run_command(*args, timeout: float = 5) -> Tuple[int, str, str]:
|
||||
"""Safe asynchronous command execution tool"""
|
||||
proc = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||
if proc.returncode is None:
|
||||
raise RuntimeError("Process finished but returncode is None")
|
||||
return proc.returncode, stdout.decode(), stderr.decode()
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
raise RuntimeError("Command timed out")
|
||||
except Exception as e:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
raise e
|
||||
43
agent/sandbox/providers/__init__.py
Normal file
43
agent/sandbox/providers/__init__.py
Normal file
@ -0,0 +1,43 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Sandbox providers package.
|
||||
|
||||
This package contains:
|
||||
- base.py: Base interface for all sandbox providers
|
||||
- manager.py: Provider manager for managing active provider
|
||||
- self_managed.py: Self-managed provider implementation (wraps existing executor_manager)
|
||||
- aliyun_codeinterpreter.py: Aliyun Code Interpreter provider implementation
|
||||
Official Documentation: https://help.aliyun.com/zh/functioncompute/fc/sandbox-sandbox-code-interepreter
|
||||
- e2b.py: E2B provider implementation
|
||||
"""
|
||||
|
||||
from .base import SandboxProvider, SandboxInstance, ExecutionResult
|
||||
from .manager import ProviderManager
|
||||
from .self_managed import SelfManagedProvider
|
||||
from .aliyun_codeinterpreter import AliyunCodeInterpreterProvider
|
||||
from .e2b import E2BProvider
|
||||
|
||||
__all__ = [
|
||||
"SandboxProvider",
|
||||
"SandboxInstance",
|
||||
"ExecutionResult",
|
||||
"ProviderManager",
|
||||
"SelfManagedProvider",
|
||||
"AliyunCodeInterpreterProvider",
|
||||
"E2BProvider",
|
||||
]
|
||||
512
agent/sandbox/providers/aliyun_codeinterpreter.py
Normal file
512
agent/sandbox/providers/aliyun_codeinterpreter.py
Normal file
@ -0,0 +1,512 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Aliyun Code Interpreter provider implementation.
|
||||
|
||||
This provider integrates with Aliyun Function Compute Code Interpreter service
|
||||
for secure code execution in serverless microVMs using the official agentrun-sdk.
|
||||
|
||||
Official Documentation: https://help.aliyun.com/zh/functioncompute/fc/sandbox-sandbox-code-interepreter
|
||||
Official SDK: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
|
||||
https://api.aliyun.com/api/AgentRun/2025-09-10/CreateTemplate?lang=PYTHON
|
||||
https://api.aliyun.com/api/AgentRun/2025-09-10/CreateSandbox?lang=PYTHON
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from agentrun.sandbox import TemplateType, CodeLanguage, Template, TemplateInput, Sandbox
|
||||
from agentrun.utils.config import Config
|
||||
from agentrun.utils.exception import ServerError
|
||||
|
||||
from .base import SandboxProvider, SandboxInstance, ExecutionResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AliyunCodeInterpreterProvider(SandboxProvider):
|
||||
"""
|
||||
Aliyun Code Interpreter provider implementation.
|
||||
|
||||
This provider uses the official agentrun-sdk to interact with
|
||||
Aliyun Function Compute's Code Interpreter service.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.access_key_id: Optional[str] = None
|
||||
self.access_key_secret: Optional[str] = None
|
||||
self.account_id: Optional[str] = None
|
||||
self.region: str = "cn-hangzhou"
|
||||
self.template_name: str = ""
|
||||
self.timeout: int = 30
|
||||
self._initialized: bool = False
|
||||
self._config: Optional[Config] = None
|
||||
|
||||
def initialize(self, config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Initialize the provider with Aliyun credentials.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary with keys:
|
||||
- access_key_id: Aliyun AccessKey ID
|
||||
- access_key_secret: Aliyun AccessKey Secret
|
||||
- account_id: Aliyun primary account ID (主账号ID)
|
||||
- region: Region (default: "cn-hangzhou")
|
||||
- template_name: Optional sandbox template name
|
||||
- timeout: Request timeout in seconds (default: 30, max 30)
|
||||
|
||||
Returns:
|
||||
True if initialization successful, False otherwise
|
||||
"""
|
||||
# Get values from config or environment variables
|
||||
access_key_id = config.get("access_key_id") or os.getenv("AGENTRUN_ACCESS_KEY_ID")
|
||||
access_key_secret = config.get("access_key_secret") or os.getenv("AGENTRUN_ACCESS_KEY_SECRET")
|
||||
account_id = config.get("account_id") or os.getenv("AGENTRUN_ACCOUNT_ID")
|
||||
region = config.get("region") or os.getenv("AGENTRUN_REGION", "cn-hangzhou")
|
||||
|
||||
self.access_key_id = access_key_id
|
||||
self.access_key_secret = access_key_secret
|
||||
self.account_id = account_id
|
||||
self.region = region
|
||||
self.template_name = config.get("template_name", "")
|
||||
self.timeout = min(config.get("timeout", 30), 30) # Max 30 seconds
|
||||
|
||||
logger.info(f"Aliyun Code Interpreter: Initializing with account_id={self.account_id}, region={self.region}")
|
||||
|
||||
# Validate required fields
|
||||
if not self.access_key_id or not self.access_key_secret:
|
||||
logger.error("Aliyun Code Interpreter: Missing access_key_id or access_key_secret")
|
||||
return False
|
||||
|
||||
if not self.account_id:
|
||||
logger.error("Aliyun Code Interpreter: Missing account_id (主账号ID)")
|
||||
return False
|
||||
|
||||
# Create SDK configuration
|
||||
try:
|
||||
logger.info(f"Aliyun Code Interpreter: Creating Config object with account_id={self.account_id}")
|
||||
self._config = Config(
|
||||
access_key_id=self.access_key_id,
|
||||
access_key_secret=self.access_key_secret,
|
||||
account_id=self.account_id,
|
||||
region_id=self.region,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
logger.info("Aliyun Code Interpreter: Config object created successfully")
|
||||
|
||||
# Verify connection with health check
|
||||
if not self.health_check():
|
||||
logger.error(f"Aliyun Code Interpreter: Health check failed for region {self.region}")
|
||||
return False
|
||||
|
||||
self._initialized = True
|
||||
logger.info(f"Aliyun Code Interpreter: Initialized successfully for region {self.region}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Aliyun Code Interpreter: Initialization failed - {str(e)}")
|
||||
return False
|
||||
|
||||
def create_instance(self, template: str = "python") -> SandboxInstance:
|
||||
"""
|
||||
Create a new sandbox instance in Aliyun Code Interpreter.
|
||||
|
||||
Args:
|
||||
template: Programming language (python, javascript)
|
||||
|
||||
Returns:
|
||||
SandboxInstance object
|
||||
|
||||
Raises:
|
||||
RuntimeError: If instance creation fails
|
||||
"""
|
||||
if not self._initialized or not self._config:
|
||||
raise RuntimeError("Provider not initialized. Call initialize() first.")
|
||||
|
||||
# Normalize language
|
||||
language = self._normalize_language(template)
|
||||
|
||||
try:
|
||||
# Get or create template
|
||||
from agentrun.sandbox import Sandbox
|
||||
|
||||
if self.template_name:
|
||||
# Use existing template
|
||||
template_name = self.template_name
|
||||
else:
|
||||
# Try to get default template, or create one if it doesn't exist
|
||||
default_template_name = f"ragflow-{language}-default"
|
||||
try:
|
||||
# Check if template exists
|
||||
Template.get_by_name(default_template_name, config=self._config)
|
||||
template_name = default_template_name
|
||||
except Exception:
|
||||
# Create default template if it doesn't exist
|
||||
template_input = TemplateInput(
|
||||
template_name=default_template_name,
|
||||
template_type=TemplateType.CODE_INTERPRETER,
|
||||
)
|
||||
Template.create(template_input, config=self._config)
|
||||
template_name = default_template_name
|
||||
|
||||
# Create sandbox directly
|
||||
sandbox = Sandbox.create(
|
||||
template_type=TemplateType.CODE_INTERPRETER,
|
||||
template_name=template_name,
|
||||
sandbox_idle_timeout_seconds=self.timeout,
|
||||
config=self._config,
|
||||
)
|
||||
|
||||
instance_id = sandbox.sandbox_id
|
||||
|
||||
return SandboxInstance(
|
||||
instance_id=instance_id,
|
||||
provider="aliyun_codeinterpreter",
|
||||
status="READY",
|
||||
metadata={
|
||||
"language": language,
|
||||
"region": self.region,
|
||||
"account_id": self.account_id,
|
||||
"template_name": template_name,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
except ServerError as e:
|
||||
raise RuntimeError(f"Failed to create sandbox instance: {str(e)}")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Unexpected error creating instance: {str(e)}")
|
||||
|
||||
def execute_code(self, instance_id: str, code: str, language: str, timeout: int = 10, arguments: Optional[Dict[str, Any]] = None) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in the Aliyun Code Interpreter instance.
|
||||
|
||||
Args:
|
||||
instance_id: ID of the sandbox instance
|
||||
code: Source code to execute
|
||||
language: Programming language (python, javascript)
|
||||
timeout: Maximum execution time in seconds (max 30)
|
||||
arguments: Optional arguments dict to pass to main() function
|
||||
|
||||
Returns:
|
||||
ExecutionResult containing stdout, stderr, exit_code, and metadata
|
||||
|
||||
Raises:
|
||||
RuntimeError: If execution fails
|
||||
TimeoutError: If execution exceeds timeout
|
||||
"""
|
||||
if not self._initialized or not self._config:
|
||||
raise RuntimeError("Provider not initialized. Call initialize() first.")
|
||||
|
||||
# Normalize language
|
||||
normalized_lang = self._normalize_language(language)
|
||||
|
||||
# Enforce 30-second hard limit
|
||||
timeout = min(timeout or self.timeout, 30)
|
||||
|
||||
try:
|
||||
# Connect to existing sandbox instance
|
||||
sandbox = Sandbox.connect(sandbox_id=instance_id, config=self._config)
|
||||
|
||||
# Convert language string to CodeLanguage enum
|
||||
code_language = CodeLanguage.PYTHON if normalized_lang == "python" else CodeLanguage.JAVASCRIPT
|
||||
|
||||
# Wrap code to call main() function
|
||||
# Matches self_managed provider behavior: call main(**arguments)
|
||||
if normalized_lang == "python":
|
||||
# Build arguments string for main() call
|
||||
if arguments:
|
||||
import json as json_module
|
||||
args_json = json_module.dumps(arguments)
|
||||
wrapped_code = f'''{code}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
result = main(**{args_json})
|
||||
print(json.dumps(result) if isinstance(result, dict) else result)
|
||||
'''
|
||||
else:
|
||||
wrapped_code = f'''{code}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
result = main()
|
||||
print(json.dumps(result) if isinstance(result, dict) else result)
|
||||
'''
|
||||
else: # javascript
|
||||
if arguments:
|
||||
import json as json_module
|
||||
args_json = json_module.dumps(arguments)
|
||||
wrapped_code = f'''{code}
|
||||
|
||||
// Call main and output result
|
||||
const result = main({args_json});
|
||||
console.log(typeof result === 'object' ? JSON.stringify(result) : String(result));
|
||||
'''
|
||||
else:
|
||||
wrapped_code = f'''{code}
|
||||
|
||||
// Call main and output result
|
||||
const result = main();
|
||||
console.log(typeof result === 'object' ? JSON.stringify(result) : String(result));
|
||||
'''
|
||||
logger.debug(f"Aliyun Code Interpreter: Wrapped code (first 200 chars): {wrapped_code[:200]}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Execute code using SDK's simplified execute endpoint
|
||||
logger.info(f"Aliyun Code Interpreter: Executing code (language={normalized_lang}, timeout={timeout})")
|
||||
logger.debug(f"Aliyun Code Interpreter: Original code (first 200 chars): {code[:200]}")
|
||||
result = sandbox.context.execute(
|
||||
code=wrapped_code,
|
||||
language=code_language,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
logger.info(f"Aliyun Code Interpreter: Execution completed in {execution_time:.2f}s")
|
||||
logger.debug(f"Aliyun Code Interpreter: Raw SDK result: {result}")
|
||||
|
||||
# Parse execution result
|
||||
results = result.get("results", []) if isinstance(result, dict) else []
|
||||
logger.info(f"Aliyun Code Interpreter: Parsed {len(results)} result items")
|
||||
|
||||
# Extract stdout and stderr from results
|
||||
stdout_parts = []
|
||||
stderr_parts = []
|
||||
exit_code = 0
|
||||
execution_status = "ok"
|
||||
|
||||
for item in results:
|
||||
result_type = item.get("type", "")
|
||||
text = item.get("text", "")
|
||||
|
||||
if result_type == "stdout":
|
||||
stdout_parts.append(text)
|
||||
elif result_type == "stderr":
|
||||
stderr_parts.append(text)
|
||||
exit_code = 1 # Error occurred
|
||||
elif result_type == "endOfExecution":
|
||||
execution_status = item.get("status", "ok")
|
||||
if execution_status != "ok":
|
||||
exit_code = 1
|
||||
elif result_type == "error":
|
||||
stderr_parts.append(text)
|
||||
exit_code = 1
|
||||
|
||||
stdout = "\n".join(stdout_parts)
|
||||
stderr = "\n".join(stderr_parts)
|
||||
|
||||
logger.info(f"Aliyun Code Interpreter: stdout length={len(stdout)}, stderr length={len(stderr)}, exit_code={exit_code}")
|
||||
if stdout:
|
||||
logger.debug(f"Aliyun Code Interpreter: stdout (first 200 chars): {stdout[:200]}")
|
||||
if stderr:
|
||||
logger.debug(f"Aliyun Code Interpreter: stderr (first 200 chars): {stderr[:200]}")
|
||||
|
||||
return ExecutionResult(
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
exit_code=exit_code,
|
||||
execution_time=execution_time,
|
||||
metadata={
|
||||
"instance_id": instance_id,
|
||||
"language": normalized_lang,
|
||||
"context_id": result.get("contextId") if isinstance(result, dict) else None,
|
||||
"timeout": timeout,
|
||||
},
|
||||
)
|
||||
|
||||
except ServerError as e:
|
||||
if "timeout" in str(e).lower():
|
||||
raise TimeoutError(f"Execution timed out after {timeout} seconds")
|
||||
raise RuntimeError(f"Failed to execute code: {str(e)}")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Unexpected error during execution: {str(e)}")
|
||||
|
||||
def destroy_instance(self, instance_id: str) -> bool:
|
||||
"""
|
||||
Destroy an Aliyun Code Interpreter instance.
|
||||
|
||||
Args:
|
||||
instance_id: ID of the instance to destroy
|
||||
|
||||
Returns:
|
||||
True if destruction successful, False otherwise
|
||||
"""
|
||||
if not self._initialized or not self._config:
|
||||
raise RuntimeError("Provider not initialized. Call initialize() first.")
|
||||
|
||||
try:
|
||||
# Delete sandbox by ID directly
|
||||
Sandbox.delete_by_id(sandbox_id=instance_id)
|
||||
|
||||
logger.info(f"Successfully destroyed sandbox instance {instance_id}")
|
||||
return True
|
||||
|
||||
except ServerError as e:
|
||||
logger.error(f"Failed to destroy instance {instance_id}: {str(e)}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error destroying instance {instance_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
def health_check(self) -> bool:
|
||||
"""
|
||||
Check if the Aliyun Code Interpreter service is accessible.
|
||||
|
||||
Returns:
|
||||
True if provider is healthy, False otherwise
|
||||
"""
|
||||
if not self._initialized and not (self.access_key_id and self.account_id):
|
||||
return False
|
||||
|
||||
try:
|
||||
# Try to list templates to verify connection
|
||||
from agentrun.sandbox import Template
|
||||
|
||||
templates = Template.list(config=self._config)
|
||||
return templates is not None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Aliyun Code Interpreter health check failed: {str(e)}")
|
||||
# If we get any response (even an error), the service is reachable
|
||||
return "connection" not in str(e).lower()
|
||||
|
||||
def get_supported_languages(self) -> List[str]:
|
||||
"""
|
||||
Get list of supported programming languages.
|
||||
|
||||
Returns:
|
||||
List of language identifiers
|
||||
"""
|
||||
return ["python", "javascript"]
|
||||
|
||||
@staticmethod
|
||||
def get_config_schema() -> Dict[str, Dict]:
|
||||
"""
|
||||
Return configuration schema for Aliyun Code Interpreter provider.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping field names to their schema definitions
|
||||
"""
|
||||
return {
|
||||
"access_key_id": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"label": "Access Key ID",
|
||||
"placeholder": "LTAI5t...",
|
||||
"description": "Aliyun AccessKey ID for authentication",
|
||||
"secret": False,
|
||||
},
|
||||
"access_key_secret": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"label": "Access Key Secret",
|
||||
"placeholder": "••••••••••••••••",
|
||||
"description": "Aliyun AccessKey Secret for authentication",
|
||||
"secret": True,
|
||||
},
|
||||
"account_id": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"label": "Account ID",
|
||||
"placeholder": "1234567890...",
|
||||
"description": "Aliyun primary account ID (主账号ID), required for API calls",
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"label": "Region",
|
||||
"default": "cn-hangzhou",
|
||||
"description": "Aliyun region for Code Interpreter service",
|
||||
"options": ["cn-hangzhou", "cn-beijing", "cn-shanghai", "cn-shenzhen", "cn-guangzhou"],
|
||||
},
|
||||
"template_name": {
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"label": "Template Name",
|
||||
"placeholder": "my-interpreter",
|
||||
"description": "Optional sandbox template name for pre-configured environments",
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"required": False,
|
||||
"label": "Execution Timeout (seconds)",
|
||||
"default": 30,
|
||||
"min": 1,
|
||||
"max": 30,
|
||||
"description": "Code execution timeout (max 30 seconds - hard limit)",
|
||||
},
|
||||
}
|
||||
|
||||
def validate_config(self, config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate Aliyun-specific configuration.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
# Validate access key format
|
||||
access_key_id = config.get("access_key_id", "")
|
||||
if access_key_id and not access_key_id.startswith("LTAI"):
|
||||
return False, "Invalid AccessKey ID format (should start with 'LTAI')"
|
||||
|
||||
# Validate account ID
|
||||
account_id = config.get("account_id", "")
|
||||
if not account_id:
|
||||
return False, "Account ID is required"
|
||||
|
||||
# Validate region
|
||||
valid_regions = ["cn-hangzhou", "cn-beijing", "cn-shanghai", "cn-shenzhen", "cn-guangzhou"]
|
||||
region = config.get("region", "cn-hangzhou")
|
||||
if region and region not in valid_regions:
|
||||
return False, f"Invalid region. Must be one of: {', '.join(valid_regions)}"
|
||||
|
||||
# Validate timeout range (max 30 seconds)
|
||||
timeout = config.get("timeout", 30)
|
||||
if isinstance(timeout, int) and (timeout < 1 or timeout > 30):
|
||||
return False, "Timeout must be between 1 and 30 seconds"
|
||||
|
||||
return True, None
|
||||
|
||||
def _normalize_language(self, language: str) -> str:
|
||||
"""
|
||||
Normalize language identifier to Aliyun format.
|
||||
|
||||
Args:
|
||||
language: Language identifier (python, python3, javascript, nodejs)
|
||||
|
||||
Returns:
|
||||
Normalized language identifier
|
||||
"""
|
||||
if not language:
|
||||
return "python"
|
||||
|
||||
lang_lower = language.lower()
|
||||
if lang_lower in ("python", "python3"):
|
||||
return "python"
|
||||
elif lang_lower in ("javascript", "nodejs"):
|
||||
return "javascript"
|
||||
else:
|
||||
return language
|
||||
212
agent/sandbox/providers/base.py
Normal file
212
agent/sandbox/providers/base.py
Normal file
@ -0,0 +1,212 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Base interface for sandbox providers.
|
||||
|
||||
Each sandbox provider (self-managed, SaaS) implements this interface
|
||||
to provide code execution capabilities.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
@dataclass
|
||||
class SandboxInstance:
|
||||
"""Represents a sandbox execution instance"""
|
||||
instance_id: str
|
||||
provider: str
|
||||
status: str # running, stopped, error
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
def __post_init__(self):
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionResult:
|
||||
"""Result of code execution in a sandbox"""
|
||||
stdout: str
|
||||
stderr: str
|
||||
exit_code: int
|
||||
execution_time: float # in seconds
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
def __post_init__(self):
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
|
||||
|
||||
class SandboxProvider(ABC):
|
||||
"""
|
||||
Base interface for all sandbox providers.
|
||||
|
||||
Each provider implementation (self-managed, Aliyun OpenSandbox, E2B, etc.)
|
||||
must implement these methods to provide code execution capabilities.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self, config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Initialize the provider with configuration.
|
||||
|
||||
Args:
|
||||
config: Provider-specific configuration dictionary
|
||||
|
||||
Returns:
|
||||
True if initialization successful, False otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_instance(self, template: str = "python") -> SandboxInstance:
|
||||
"""
|
||||
Create a new sandbox instance.
|
||||
|
||||
Args:
|
||||
template: Programming language/template for the instance
|
||||
(e.g., "python", "nodejs", "bash")
|
||||
|
||||
Returns:
|
||||
SandboxInstance object representing the created instance
|
||||
|
||||
Raises:
|
||||
RuntimeError: If instance creation fails
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def execute_code(
|
||||
self,
|
||||
instance_id: str,
|
||||
code: str,
|
||||
language: str,
|
||||
timeout: int = 10,
|
||||
arguments: Optional[Dict[str, Any]] = None
|
||||
) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in a sandbox instance.
|
||||
|
||||
Args:
|
||||
instance_id: ID of the sandbox instance
|
||||
code: Source code to execute
|
||||
language: Programming language (python, javascript, etc.)
|
||||
timeout: Maximum execution time in seconds
|
||||
arguments: Optional arguments dict to pass to main() function
|
||||
|
||||
Returns:
|
||||
ExecutionResult containing stdout, stderr, exit_code, and metadata
|
||||
|
||||
Raises:
|
||||
RuntimeError: If execution fails
|
||||
TimeoutError: If execution exceeds timeout
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def destroy_instance(self, instance_id: str) -> bool:
|
||||
"""
|
||||
Destroy a sandbox instance.
|
||||
|
||||
Args:
|
||||
instance_id: ID of the instance to destroy
|
||||
|
||||
Returns:
|
||||
True if destruction successful, False otherwise
|
||||
|
||||
Raises:
|
||||
RuntimeError: If destruction fails
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def health_check(self) -> bool:
|
||||
"""
|
||||
Check if the provider is healthy and accessible.
|
||||
|
||||
Returns:
|
||||
True if provider is healthy, False otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_supported_languages(self) -> List[str]:
|
||||
"""
|
||||
Get list of supported programming languages.
|
||||
|
||||
Returns:
|
||||
List of language identifiers (e.g., ["python", "javascript", "go"])
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_config_schema() -> Dict[str, Dict]:
|
||||
"""
|
||||
Return configuration schema for this provider.
|
||||
|
||||
The schema defines what configuration fields are required/optional,
|
||||
their types, validation rules, and UI labels.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping field names to their schema definitions.
|
||||
|
||||
Example:
|
||||
{
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"label": "API Endpoint",
|
||||
"placeholder": "http://localhost:9385"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"default": 30,
|
||||
"label": "Timeout (seconds)",
|
||||
"min": 5,
|
||||
"max": 300
|
||||
}
|
||||
}
|
||||
"""
|
||||
return {}
|
||||
|
||||
def validate_config(self, config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate provider-specific configuration.
|
||||
|
||||
This method allows providers to implement custom validation logic beyond
|
||||
the basic schema validation. Override this method to add provider-specific
|
||||
checks like URL format validation, API key format validation, etc.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message):
|
||||
- is_valid: True if configuration is valid, False otherwise
|
||||
- error_message: Error message if invalid, None if valid
|
||||
|
||||
Example:
|
||||
>>> def validate_config(self, config):
|
||||
>>> endpoint = config.get("endpoint", "")
|
||||
>>> if not endpoint.startswith(("http://", "https://")):
|
||||
>>> return False, "Endpoint must start with http:// or https://"
|
||||
>>> return True, None
|
||||
"""
|
||||
# Default implementation: no custom validation
|
||||
return True, None
|
||||
233
agent/sandbox/providers/e2b.py
Normal file
233
agent/sandbox/providers/e2b.py
Normal file
@ -0,0 +1,233 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
E2B provider implementation.
|
||||
|
||||
This provider integrates with E2B Cloud for cloud-based code execution
|
||||
using Firecracker microVMs.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from .base import SandboxProvider, SandboxInstance, ExecutionResult
|
||||
|
||||
|
||||
class E2BProvider(SandboxProvider):
|
||||
"""
|
||||
E2B provider implementation.
|
||||
|
||||
This provider uses E2B Cloud service for secure code execution
|
||||
in Firecracker microVMs.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key: str = ""
|
||||
self.region: str = "us"
|
||||
self.timeout: int = 30
|
||||
self._initialized: bool = False
|
||||
|
||||
def initialize(self, config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Initialize the provider with E2B credentials.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary with keys:
|
||||
- api_key: E2B API key
|
||||
- region: Region (us, eu) (default: "us")
|
||||
- timeout: Request timeout in seconds (default: 30)
|
||||
|
||||
Returns:
|
||||
True if initialization successful, False otherwise
|
||||
"""
|
||||
self.api_key = config.get("api_key", "")
|
||||
self.region = config.get("region", "us")
|
||||
self.timeout = config.get("timeout", 30)
|
||||
|
||||
# Validate required fields
|
||||
if not self.api_key:
|
||||
return False
|
||||
|
||||
# TODO: Implement actual E2B API client initialization
|
||||
# For now, we'll mark as initialized but actual API calls will fail
|
||||
self._initialized = True
|
||||
return True
|
||||
|
||||
def create_instance(self, template: str = "python") -> SandboxInstance:
|
||||
"""
|
||||
Create a new sandbox instance in E2B.
|
||||
|
||||
Args:
|
||||
template: Programming language template (python, nodejs, go, bash)
|
||||
|
||||
Returns:
|
||||
SandboxInstance object
|
||||
|
||||
Raises:
|
||||
RuntimeError: If instance creation fails
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Provider not initialized. Call initialize() first.")
|
||||
|
||||
# Normalize language
|
||||
language = self._normalize_language(template)
|
||||
|
||||
# TODO: Implement actual E2B API call
|
||||
# POST /sandbox with template
|
||||
instance_id = str(uuid.uuid4())
|
||||
|
||||
return SandboxInstance(
|
||||
instance_id=instance_id,
|
||||
provider="e2b",
|
||||
status="running",
|
||||
metadata={
|
||||
"language": language,
|
||||
"region": self.region,
|
||||
}
|
||||
)
|
||||
|
||||
def execute_code(
|
||||
self,
|
||||
instance_id: str,
|
||||
code: str,
|
||||
language: str,
|
||||
timeout: int = 10
|
||||
) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in the E2B instance.
|
||||
|
||||
Args:
|
||||
instance_id: ID of the sandbox instance
|
||||
code: Source code to execute
|
||||
language: Programming language (python, nodejs, go, bash)
|
||||
timeout: Maximum execution time in seconds
|
||||
|
||||
Returns:
|
||||
ExecutionResult containing stdout, stderr, exit_code, and metadata
|
||||
|
||||
Raises:
|
||||
RuntimeError: If execution fails
|
||||
TimeoutError: If execution exceeds timeout
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Provider not initialized. Call initialize() first.")
|
||||
|
||||
# TODO: Implement actual E2B API call
|
||||
# POST /sandbox/{sandboxID}/execute
|
||||
|
||||
raise RuntimeError(
|
||||
"E2B provider is not yet fully implemented. "
|
||||
"Please use the self-managed provider or implement the E2B API integration. "
|
||||
"See https://github.com/e2b-dev/e2b for API documentation."
|
||||
)
|
||||
|
||||
def destroy_instance(self, instance_id: str) -> bool:
|
||||
"""
|
||||
Destroy an E2B instance.
|
||||
|
||||
Args:
|
||||
instance_id: ID of the instance to destroy
|
||||
|
||||
Returns:
|
||||
True if destruction successful, False otherwise
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Provider not initialized. Call initialize() first.")
|
||||
|
||||
# TODO: Implement actual E2B API call
|
||||
# DELETE /sandbox/{sandboxID}
|
||||
return True
|
||||
|
||||
def health_check(self) -> bool:
|
||||
"""
|
||||
Check if the E2B service is accessible.
|
||||
|
||||
Returns:
|
||||
True if provider is healthy, False otherwise
|
||||
"""
|
||||
if not self._initialized:
|
||||
return False
|
||||
|
||||
# TODO: Implement actual E2B health check API call
|
||||
# GET /healthz or similar
|
||||
# For now, return True if initialized with API key
|
||||
return bool(self.api_key)
|
||||
|
||||
def get_supported_languages(self) -> List[str]:
|
||||
"""
|
||||
Get list of supported programming languages.
|
||||
|
||||
Returns:
|
||||
List of language identifiers
|
||||
"""
|
||||
return ["python", "nodejs", "javascript", "go", "bash"]
|
||||
|
||||
@staticmethod
|
||||
def get_config_schema() -> Dict[str, Dict]:
|
||||
"""
|
||||
Return configuration schema for E2B provider.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping field names to their schema definitions
|
||||
"""
|
||||
return {
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"label": "API Key",
|
||||
"placeholder": "e2b_sk_...",
|
||||
"description": "E2B API key for authentication",
|
||||
"secret": True,
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"label": "Region",
|
||||
"default": "us",
|
||||
"description": "E2B service region (us or eu)",
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"required": False,
|
||||
"label": "Request Timeout (seconds)",
|
||||
"default": 30,
|
||||
"min": 5,
|
||||
"max": 300,
|
||||
"description": "API request timeout for code execution",
|
||||
}
|
||||
}
|
||||
|
||||
def _normalize_language(self, language: str) -> str:
|
||||
"""
|
||||
Normalize language identifier to E2B template format.
|
||||
|
||||
Args:
|
||||
language: Language identifier
|
||||
|
||||
Returns:
|
||||
Normalized language identifier
|
||||
"""
|
||||
if not language:
|
||||
return "python"
|
||||
|
||||
lang_lower = language.lower()
|
||||
if lang_lower in ("python", "python3"):
|
||||
return "python"
|
||||
elif lang_lower in ("javascript", "nodejs"):
|
||||
return "nodejs"
|
||||
else:
|
||||
return language
|
||||
78
agent/sandbox/providers/manager.py
Normal file
78
agent/sandbox/providers/manager.py
Normal file
@ -0,0 +1,78 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Provider manager for sandbox providers.
|
||||
|
||||
Since sandbox configuration is global (system-level), we only use one
|
||||
active provider at a time. This manager is a thin wrapper that holds a reference
|
||||
to the currently active provider.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .base import SandboxProvider
|
||||
|
||||
|
||||
class ProviderManager:
|
||||
"""
|
||||
Manages the currently active sandbox provider.
|
||||
|
||||
With global configuration, there's only one active provider at a time.
|
||||
This manager simply holds a reference to that provider.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty provider manager."""
|
||||
self.current_provider: Optional[SandboxProvider] = None
|
||||
self.current_provider_name: Optional[str] = None
|
||||
|
||||
def set_provider(self, name: str, provider: SandboxProvider):
|
||||
"""
|
||||
Set the active provider.
|
||||
|
||||
Args:
|
||||
name: Provider identifier (e.g., "self_managed", "e2b")
|
||||
provider: Provider instance
|
||||
"""
|
||||
self.current_provider = provider
|
||||
self.current_provider_name = name
|
||||
|
||||
def get_provider(self) -> Optional[SandboxProvider]:
|
||||
"""
|
||||
Get the active provider.
|
||||
|
||||
Returns:
|
||||
Currently active SandboxProvider instance, or None if not set
|
||||
"""
|
||||
return self.current_provider
|
||||
|
||||
def get_provider_name(self) -> Optional[str]:
|
||||
"""
|
||||
Get the active provider name.
|
||||
|
||||
Returns:
|
||||
Provider name (e.g., "self_managed"), or None if not set
|
||||
"""
|
||||
return self.current_provider_name
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
"""
|
||||
Check if a provider is configured.
|
||||
|
||||
Returns:
|
||||
True if a provider is set, False otherwise
|
||||
"""
|
||||
return self.current_provider is not None
|
||||
359
agent/sandbox/providers/self_managed.py
Normal file
359
agent/sandbox/providers/self_managed.py
Normal file
@ -0,0 +1,359 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Self-managed sandbox provider implementation.
|
||||
|
||||
This provider wraps the existing executor_manager HTTP API which manages
|
||||
a pool of Docker containers with gVisor for secure code execution.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from .base import SandboxProvider, SandboxInstance, ExecutionResult
|
||||
|
||||
|
||||
class SelfManagedProvider(SandboxProvider):
|
||||
"""
|
||||
Self-managed sandbox provider using Daytona/Docker.
|
||||
|
||||
This provider communicates with the executor_manager HTTP API
|
||||
which manages a pool of containers for code execution.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.endpoint: str = "http://localhost:9385"
|
||||
self.timeout: int = 30
|
||||
self.max_retries: int = 3
|
||||
self.pool_size: int = 10
|
||||
self._initialized: bool = False
|
||||
|
||||
def initialize(self, config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Initialize the provider with configuration.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary with keys:
|
||||
- endpoint: HTTP endpoint (default: "http://localhost:9385")
|
||||
- timeout: Request timeout in seconds (default: 30)
|
||||
- max_retries: Maximum retry attempts (default: 3)
|
||||
- pool_size: Container pool size for info (default: 10)
|
||||
|
||||
Returns:
|
||||
True if initialization successful, False otherwise
|
||||
"""
|
||||
self.endpoint = config.get("endpoint", "http://localhost:9385")
|
||||
self.timeout = config.get("timeout", 30)
|
||||
self.max_retries = config.get("max_retries", 3)
|
||||
self.pool_size = config.get("pool_size", 10)
|
||||
|
||||
# Validate endpoint is accessible
|
||||
if not self.health_check():
|
||||
# Try to fall back to SANDBOX_HOST from settings if we are using localhost
|
||||
if "localhost" in self.endpoint or "127.0.0.1" in self.endpoint:
|
||||
try:
|
||||
from api import settings
|
||||
if settings.SANDBOX_HOST and settings.SANDBOX_HOST not in self.endpoint:
|
||||
original_endpoint = self.endpoint
|
||||
self.endpoint = f"http://{settings.SANDBOX_HOST}:9385"
|
||||
if self.health_check():
|
||||
import logging
|
||||
logging.warning(f"Sandbox self_managed: Connected using settings.SANDBOX_HOST fallback: {self.endpoint} (original: {original_endpoint})")
|
||||
self._initialized = True
|
||||
return True
|
||||
else:
|
||||
self.endpoint = original_endpoint # Restore if fallback also fails
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
self._initialized = True
|
||||
return True
|
||||
|
||||
def create_instance(self, template: str = "python") -> SandboxInstance:
|
||||
"""
|
||||
Create a new sandbox instance.
|
||||
|
||||
Note: For self-managed provider, instances are managed internally
|
||||
by the executor_manager's container pool. This method returns
|
||||
a logical instance handle.
|
||||
|
||||
Args:
|
||||
template: Programming language (python, nodejs)
|
||||
|
||||
Returns:
|
||||
SandboxInstance object
|
||||
|
||||
Raises:
|
||||
RuntimeError: If instance creation fails
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Provider not initialized. Call initialize() first.")
|
||||
|
||||
# Normalize language
|
||||
language = self._normalize_language(template)
|
||||
|
||||
# The executor_manager manages instances internally via container pool
|
||||
# We create a logical instance ID for tracking
|
||||
instance_id = str(uuid.uuid4())
|
||||
|
||||
return SandboxInstance(
|
||||
instance_id=instance_id,
|
||||
provider="self_managed",
|
||||
status="running",
|
||||
metadata={
|
||||
"language": language,
|
||||
"endpoint": self.endpoint,
|
||||
"pool_size": self.pool_size,
|
||||
}
|
||||
)
|
||||
|
||||
def execute_code(
|
||||
self,
|
||||
instance_id: str,
|
||||
code: str,
|
||||
language: str,
|
||||
timeout: int = 10,
|
||||
arguments: Optional[Dict[str, Any]] = None
|
||||
) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in the sandbox.
|
||||
|
||||
Args:
|
||||
instance_id: ID of the sandbox instance (not used for self-managed)
|
||||
code: Source code to execute
|
||||
language: Programming language (python, nodejs, javascript)
|
||||
timeout: Maximum execution time in seconds
|
||||
arguments: Optional arguments dict to pass to main() function
|
||||
|
||||
Returns:
|
||||
ExecutionResult containing stdout, stderr, exit_code, and metadata
|
||||
|
||||
Raises:
|
||||
RuntimeError: If execution fails
|
||||
TimeoutError: If execution exceeds timeout
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Provider not initialized. Call initialize() first.")
|
||||
|
||||
# Normalize language
|
||||
normalized_lang = self._normalize_language(language)
|
||||
|
||||
# Prepare request
|
||||
code_b64 = base64.b64encode(code.encode("utf-8")).decode("utf-8")
|
||||
payload = {
|
||||
"code_b64": code_b64,
|
||||
"language": normalized_lang,
|
||||
"arguments": arguments or {}
|
||||
}
|
||||
|
||||
url = f"{self.endpoint}/run"
|
||||
exec_timeout = timeout or self.timeout
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
timeout=exec_timeout,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"HTTP {response.status_code}: {response.text}"
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
return ExecutionResult(
|
||||
stdout=result.get("stdout", ""),
|
||||
stderr=result.get("stderr", ""),
|
||||
exit_code=result.get("exit_code", 0),
|
||||
execution_time=execution_time,
|
||||
metadata={
|
||||
"status": result.get("status"),
|
||||
"time_used_ms": result.get("time_used_ms"),
|
||||
"memory_used_kb": result.get("memory_used_kb"),
|
||||
"detail": result.get("detail"),
|
||||
"instance_id": instance_id,
|
||||
}
|
||||
)
|
||||
|
||||
except requests.Timeout:
|
||||
execution_time = time.time() - start_time
|
||||
raise TimeoutError(
|
||||
f"Execution timed out after {exec_timeout} seconds"
|
||||
)
|
||||
|
||||
except requests.RequestException as e:
|
||||
raise RuntimeError(f"HTTP request failed: {str(e)}")
|
||||
|
||||
def destroy_instance(self, instance_id: str) -> bool:
|
||||
"""
|
||||
Destroy a sandbox instance.
|
||||
|
||||
Note: For self-managed provider, instances are returned to the
|
||||
internal pool automatically by executor_manager after execution.
|
||||
This is a no-op for tracking purposes.
|
||||
|
||||
Args:
|
||||
instance_id: ID of the instance to destroy
|
||||
|
||||
Returns:
|
||||
True (always succeeds for self-managed)
|
||||
"""
|
||||
# The executor_manager manages container lifecycle internally
|
||||
# Container is returned to pool after execution
|
||||
return True
|
||||
|
||||
def health_check(self) -> bool:
|
||||
"""
|
||||
Check if the provider is healthy and accessible.
|
||||
|
||||
Returns:
|
||||
True if provider is healthy, False otherwise
|
||||
"""
|
||||
try:
|
||||
url = f"{self.endpoint}/healthz"
|
||||
response = requests.get(url, timeout=5)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_supported_languages(self) -> List[str]:
|
||||
"""
|
||||
Get list of supported programming languages.
|
||||
|
||||
Returns:
|
||||
List of language identifiers
|
||||
"""
|
||||
return ["python", "nodejs", "javascript"]
|
||||
|
||||
@staticmethod
|
||||
def get_config_schema() -> Dict[str, Dict]:
|
||||
"""
|
||||
Return configuration schema for self-managed provider.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping field names to their schema definitions
|
||||
"""
|
||||
return {
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"label": "Executor Manager Endpoint",
|
||||
"placeholder": "http://localhost:9385",
|
||||
"default": "http://localhost:9385",
|
||||
"description": "HTTP endpoint of the executor_manager service"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"required": False,
|
||||
"label": "Request Timeout (seconds)",
|
||||
"default": 30,
|
||||
"min": 5,
|
||||
"max": 300,
|
||||
"description": "HTTP request timeout for code execution"
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"required": False,
|
||||
"label": "Max Retries",
|
||||
"default": 3,
|
||||
"min": 0,
|
||||
"max": 10,
|
||||
"description": "Maximum number of retry attempts for failed requests"
|
||||
},
|
||||
"pool_size": {
|
||||
"type": "integer",
|
||||
"required": False,
|
||||
"label": "Container Pool Size",
|
||||
"default": 10,
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"description": "Size of the container pool (configured in executor_manager)"
|
||||
}
|
||||
}
|
||||
|
||||
def _normalize_language(self, language: str) -> str:
|
||||
"""
|
||||
Normalize language identifier to executor_manager format.
|
||||
|
||||
Args:
|
||||
language: Language identifier (python, python3, nodejs, javascript)
|
||||
|
||||
Returns:
|
||||
Normalized language identifier
|
||||
"""
|
||||
if not language:
|
||||
return "python"
|
||||
|
||||
lang_lower = language.lower()
|
||||
if lang_lower in ("python", "python3"):
|
||||
return "python"
|
||||
elif lang_lower in ("javascript", "nodejs"):
|
||||
return "nodejs"
|
||||
else:
|
||||
return language
|
||||
|
||||
def validate_config(self, config: dict) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate self-managed provider configuration.
|
||||
|
||||
Performs custom validation beyond the basic schema validation,
|
||||
such as checking URL format.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
# Validate endpoint URL format
|
||||
endpoint = config.get("endpoint", "")
|
||||
if endpoint:
|
||||
# Check if it's a valid HTTP/HTTPS URL or localhost
|
||||
import re
|
||||
url_pattern = r'^(https?://|http://localhost|http://[\d\.]+:[a-z]+:[/]|http://[\w\.]+:)'
|
||||
if not re.match(url_pattern, endpoint):
|
||||
return False, f"Invalid endpoint format: {endpoint}. Must start with http:// or https://"
|
||||
|
||||
# Validate pool_size is positive
|
||||
pool_size = config.get("pool_size", 10)
|
||||
if isinstance(pool_size, int) and pool_size <= 0:
|
||||
return False, "Pool size must be greater than 0"
|
||||
|
||||
# Validate timeout is reasonable
|
||||
timeout = config.get("timeout", 30)
|
||||
if isinstance(timeout, int) and (timeout < 1 or timeout > 600):
|
||||
return False, "Timeout must be between 1 and 600 seconds"
|
||||
|
||||
# Validate max_retries
|
||||
max_retries = config.get("max_retries", 3)
|
||||
if isinstance(max_retries, int) and (max_retries < 0 or max_retries > 10):
|
||||
return False, "Max retries must be between 0 and 10"
|
||||
|
||||
return True, None
|
||||
28
agent/sandbox/pyproject.toml
Normal file
28
agent/sandbox/pyproject.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[project]
|
||||
name = "gvisor-sandbox"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.15"
|
||||
dependencies = [
|
||||
"fastapi>=0.115.12",
|
||||
"httpx>=0.28.1",
|
||||
"pydantic>=2.11.4",
|
||||
"requests>=2.32.3",
|
||||
"slowapi>=0.1.9",
|
||||
"uvicorn>=0.34.2",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"basedpyright>=1.29.1",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 200
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = ["C4", "SIM", "TCH"]
|
||||
17
agent/sandbox/sandbox_base_image/nodejs/Dockerfile
Normal file
17
agent/sandbox/sandbox_base_image/nodejs/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM node:24.13-bookworm-slim
|
||||
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# RUN grep -rl 'deb.debian.org' /etc/apt/ | xargs sed -i 's|http[s]*://deb.debian.org|https://mirrors.ustc.edu.cn|g' && \
|
||||
# apt-get update && \
|
||||
# apt-get install -y curl gcc make
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json .
|
||||
|
||||
RUN npm install
|
||||
|
||||
CMD ["sleep", "infinity"]
|
||||
|
||||
295
agent/sandbox/sandbox_base_image/nodejs/package-lock.json
generated
Normal file
295
agent/sandbox/sandbox_base_image/nodejs/package-lock.json
generated
Normal file
@ -0,0 +1,295 @@
|
||||
{
|
||||
"name": "nodejs",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nodejs",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
|
||||
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
agent/sandbox/sandbox_base_image/nodejs/package.json
Normal file
15
agent/sandbox/sandbox_base_image/nodejs/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "nodejs",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0"
|
||||
}
|
||||
}
|
||||
15
agent/sandbox/sandbox_base_image/python/Dockerfile
Normal file
15
agent/sandbox/sandbox_base_image/python/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.7.5 /uv /uvx /bin/
|
||||
ENV UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN grep -rl 'deb.debian.org' /etc/apt/ | xargs sed -i 's|http[s]*://deb.debian.org|https://mirrors.tuna.tsinghua.edu.cn|g' && \
|
||||
apt-get update && \
|
||||
apt-get install -y curl gcc && \
|
||||
uv pip install --system -r requirements.txt
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
CMD ["sleep", "infinity"]
|
||||
3
agent/sandbox/sandbox_base_image/python/requirements.txt
Normal file
3
agent/sandbox/sandbox_base_image/python/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
numpy
|
||||
pandas
|
||||
requests
|
||||
21
agent/sandbox/scripts/restart.sh
Executable file
21
agent/sandbox/scripts/restart.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
bash "$(dirname "$0")/stop.sh"
|
||||
bash "$(dirname "$0")/start.sh"
|
||||
72
agent/sandbox/scripts/start.sh
Executable file
72
agent/sandbox/scripts/start.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
BASE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$BASE_DIR"
|
||||
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
SANDBOX_EXECUTOR_MANAGER_PORT="${SANDBOX_EXECUTOR_MANAGER_PORT:-9385}" # Default to 9385 if not set in .env
|
||||
SANDBOX_EXECUTOR_MANAGER_POOL_SIZE="${SANDBOX_EXECUTOR_MANAGER_POOL_SIZE:-5}" # Default to 5 if not set in .env
|
||||
SANDBOX_BASE_PYTHON_IMAGE=${SANDBOX_BASE_PYTHON_IMAGE-"sandbox-base-python:latest"}
|
||||
SANDBOX_BASE_NODEJS_IMAGE=${SANDBOX_BASE_NODEJS_IMAGE-"sandbox-base-nodejs:latest"}
|
||||
else
|
||||
echo "⚠️ .env not found, using default ports and pool size"
|
||||
SANDBOX_EXECUTOR_MANAGER_PORT=9385
|
||||
SANDBOX_EXECUTOR_MANAGER_POOL_SIZE=5
|
||||
SANDBOX_BASE_PYTHON_IMAGE=sandbox-base-python:latest
|
||||
SANDBOX_BASE_NODEJS_IMAGE=sandbox-base-nodejs:latest
|
||||
fi
|
||||
|
||||
echo "📦 STEP 1: Build sandbox-base image ..."
|
||||
if [ -f .env ]; then
|
||||
source .env &&
|
||||
echo "🐍 Building base sandbox image for Python ($SANDBOX_BASE_PYTHON_IMAGE)..." &&
|
||||
docker build -t "$SANDBOX_BASE_PYTHON_IMAGE" ./sandbox_base_image/python &&
|
||||
echo "⬢ Building base sandbox image for Nodejs ($SANDBOX_BASE_NODEJS_IMAGE)..." &&
|
||||
docker build -t "$SANDBOX_BASE_NODEJS_IMAGE" ./sandbox_base_image/nodejs
|
||||
else
|
||||
echo "⚠️ .env file not found, skipping build."
|
||||
fi
|
||||
|
||||
echo "🧹 STEP 2: Clean up old sandbox containers (sandbox_nodejs_0~$((SANDBOX_EXECUTOR_MANAGER_POOL_SIZE - 1)) and sandbox_python_0~$((SANDBOX_EXECUTOR_MANAGER_POOL_SIZE - 1))) ..."
|
||||
for i in $(seq 0 $((SANDBOX_EXECUTOR_MANAGER_POOL_SIZE - 1))); do
|
||||
echo "🧹 Deleting sandbox_python_$i..."
|
||||
docker rm -f "sandbox_python_$i" >/dev/null 2>&1 || true
|
||||
|
||||
echo "🧹 Deleting sandbox_nodejs_$i..."
|
||||
docker rm -f "sandbox_nodejs_$i" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
echo "🔧 STEP 3: Build executor services ..."
|
||||
docker compose build
|
||||
|
||||
echo "🚀 STEP 4: Start services ..."
|
||||
docker compose up -d
|
||||
|
||||
echo "⏳ STEP 5a: Check if ports are open (basic connectivity) ..."
|
||||
bash ./scripts/wait-for-it.sh "localhost" "$SANDBOX_EXECUTOR_MANAGER_PORT" -t 30
|
||||
|
||||
echo "⏳ STEP 5b: Check if the interfaces are healthy (/healthz) ..."
|
||||
bash ./scripts/wait-for-it-http.sh "http://localhost:$SANDBOX_EXECUTOR_MANAGER_PORT/healthz" 30
|
||||
|
||||
echo "✅ STEP 6: Run security tests ..."
|
||||
python3 ./tests/sandbox_security_tests_full.py
|
||||
|
||||
echo "🎉 Service is ready: http://localhost:$SANDBOX_EXECUTOR_MANAGER_PORT/docs"
|
||||
40
agent/sandbox/scripts/stop.sh
Executable file
40
agent/sandbox/scripts/stop.sh
Executable file
@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
BASE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$BASE_DIR"
|
||||
|
||||
echo "🛑 Stopping all services..."
|
||||
docker compose down
|
||||
|
||||
echo "🧹 Deleting sandbox containers..."
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
for i in $(seq 0 $((SANDBOX_EXECUTOR_MANAGER_POOL_SIZE - 1))); do
|
||||
echo "🧹 Deleting sandbox_python_$i..."
|
||||
docker rm -f "sandbox_python_$i" >/dev/null 2>&1 || true
|
||||
|
||||
echo "🧹 Deleting sandbox_nodejs_$i..."
|
||||
docker rm -f "sandbox_nodejs_$i" >/dev/null 2>&1 || true
|
||||
done
|
||||
else
|
||||
echo "⚠️ .env not found, skipping container cleanup"
|
||||
fi
|
||||
|
||||
echo "✅ Stopping and cleanup complete"
|
||||
31
agent/sandbox/scripts/wait-for-it-http.sh
Executable file
31
agent/sandbox/scripts/wait-for-it-http.sh
Executable file
@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
url=$1
|
||||
timeout=${2:-15}
|
||||
quiet=${3:-0}
|
||||
|
||||
for i in $(seq "$timeout"); do
|
||||
if curl -fs "$url" >/dev/null; then
|
||||
[[ "$quiet" -ne 1 ]] && echo "✔ $url is healthy after $i seconds"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "✖ Timeout after $timeout seconds waiting for $url"
|
||||
exit 1
|
||||
50
agent/sandbox/scripts/wait-for-it.sh
Executable file
50
agent/sandbox/scripts/wait-for-it.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
host=$1
|
||||
port=$2
|
||||
shift 2
|
||||
|
||||
timeout=15
|
||||
quiet=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-t | --timeout)
|
||||
timeout="$2"
|
||||
shift 2
|
||||
;;
|
||||
-q | --quiet)
|
||||
quiet=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for i in $(seq "$timeout"); do
|
||||
if nc -z "$host" "$port" >/dev/null 2>&1; then
|
||||
[[ "$quiet" -ne 1 ]] && echo "✔ $host:$port is available after $i seconds"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "✖ Timeout after $timeout seconds waiting for $host:$port"
|
||||
exit 1
|
||||
261
agent/sandbox/tests/MIGRATION_GUIDE.md
Normal file
261
agent/sandbox/tests/MIGRATION_GUIDE.md
Normal file
@ -0,0 +1,261 @@
|
||||
# Aliyun Code Interpreter Provider - 使用官方 SDK
|
||||
|
||||
## 重要变更
|
||||
|
||||
### 官方资源
|
||||
- **Code Interpreter API**: https://help.aliyun.com/zh/functioncompute/fc/sandbox-sandbox-code-interepreter
|
||||
- **官方 SDK**: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
- **SDK 文档**: https://docs.agent.run
|
||||
|
||||
## 使用官方 SDK 的优势
|
||||
|
||||
从手动 HTTP 请求迁移到官方 SDK (`agentrun-sdk`) 有以下优势:
|
||||
|
||||
### 1. **自动签名认证**
|
||||
- SDK 自动处理 Aliyun API 签名(无需手动实现 `Authorization` 头)
|
||||
- 支持多种认证方式:AccessKey、STS Token
|
||||
- 自动读取环境变量
|
||||
|
||||
### 2. **简化的 API**
|
||||
```python
|
||||
# 旧实现(手动 HTTP 请求)
|
||||
response = requests.post(
|
||||
f"{DATA_ENDPOINT}/sandboxes/{sandbox_id}/execute",
|
||||
headers={"X-Acs-Parent-Id": account_id},
|
||||
json={"code": code, "language": "python"}
|
||||
)
|
||||
|
||||
# 新实现(使用 SDK)
|
||||
sandbox = CodeInterpreterSandbox(template_name="python-sandbox", config=config)
|
||||
result = sandbox.context.execute(code="print('hello')")
|
||||
```
|
||||
|
||||
### 3. **更好的错误处理**
|
||||
- 结构化的异常类型 (`ServerError`)
|
||||
- 自动重试机制
|
||||
- 详细的错误信息
|
||||
|
||||
## 主要变更
|
||||
|
||||
### 1. 文件重命名
|
||||
|
||||
| 旧文件名 | 新文件名 | 说明 |
|
||||
|---------|---------|------|
|
||||
| `aliyun_opensandbox.py` | `aliyun_codeinterpreter.py` | 提供商实现 |
|
||||
| `test_aliyun_provider.py` | `test_aliyun_codeinterpreter.py` | 单元测试 |
|
||||
| `test_aliyun_integration.py` | `test_aliyun_codeinterpreter_integration.py` | 集成测试 |
|
||||
|
||||
### 2. 配置字段变更
|
||||
|
||||
#### 旧配置(OpenSandbox)
|
||||
```json
|
||||
{
|
||||
"access_key_id": "LTAI5t...",
|
||||
"access_key_secret": "...",
|
||||
"region": "cn-hangzhou",
|
||||
"workspace_id": "ws-xxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
#### 新配置(Code Interpreter)
|
||||
```json
|
||||
{
|
||||
"access_key_id": "LTAI5t...",
|
||||
"access_key_secret": "...",
|
||||
"account_id": "1234567890...", // 新增:阿里云主账号ID(必需)
|
||||
"region": "cn-hangzhou",
|
||||
"template_name": "python-sandbox", // 新增:沙箱模板名称
|
||||
"timeout": 30 // 最大 30 秒(硬限制)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 关键差异
|
||||
|
||||
| 特性 | OpenSandbox | Code Interpreter |
|
||||
|------|-------------|-----------------|
|
||||
| **API 端点** | `opensandbox.{region}.aliyuncs.com` | `agentrun.{region}.aliyuncs.com` (控制面) |
|
||||
| **API 版本** | `2024-01-01` | `2025-09-10` |
|
||||
| **认证** | 需要 AccessKey | 需要 AccessKey + 主账号ID |
|
||||
| **请求头** | 标准签名 | 需要 `X-Acs-Parent-Id` 头 |
|
||||
| **超时限制** | 可配置 | **最大 30 秒**(硬限制) |
|
||||
| **上下文** | 不支持 | 支持上下文(Jupyter kernel) |
|
||||
|
||||
### 4. API 调用方式变更
|
||||
|
||||
#### 旧实现(假设的 OpenSandbox)
|
||||
```python
|
||||
# 单一端点
|
||||
API_ENDPOINT = "https://opensandbox.cn-hangzhou.aliyuncs.com"
|
||||
|
||||
# 简单的请求/响应
|
||||
response = requests.post(
|
||||
f"{API_ENDPOINT}/execute",
|
||||
json={"code": "print('hello')", "language": "python"}
|
||||
)
|
||||
```
|
||||
|
||||
#### 新实现(Code Interpreter)
|
||||
```python
|
||||
# 控制面 API - 管理沙箱生命周期
|
||||
CONTROL_ENDPOINT = "https://agentrun.cn-hangzhou.aliyuncs.com/2025-09-10"
|
||||
|
||||
# 数据面 API - 执行代码
|
||||
DATA_ENDPOINT = "https://{account_id}.agentrun-data.cn-hangzhou.aliyuncs.com"
|
||||
|
||||
# 创建沙箱(控制面)
|
||||
response = requests.post(
|
||||
f"{CONTROL_ENDPOINT}/sandboxes",
|
||||
headers={"X-Acs-Parent-Id": account_id},
|
||||
json={"templateName": "python-sandbox"}
|
||||
)
|
||||
|
||||
# 执行代码(数据面)
|
||||
response = requests.post(
|
||||
f"{DATA_ENDPOINT}/sandboxes/{sandbox_id}/execute",
|
||||
headers={"X-Acs-Parent-Id": account_id},
|
||||
json={"code": "print('hello')", "language": "python", "timeout": 30}
|
||||
)
|
||||
```
|
||||
|
||||
### 5. 迁移步骤
|
||||
|
||||
#### 步骤 1: 更新配置
|
||||
|
||||
如果您之前使用的是 `aliyun_opensandbox`:
|
||||
|
||||
**旧配置**:
|
||||
```json
|
||||
{
|
||||
"name": "sandbox.provider_type",
|
||||
"value": "aliyun_opensandbox"
|
||||
}
|
||||
```
|
||||
|
||||
**新配置**:
|
||||
```json
|
||||
{
|
||||
"name": "sandbox.provider_type",
|
||||
"value": "aliyun_codeinterpreter"
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 2: 添加必需的 account_id
|
||||
|
||||
在 Aliyun 控制台右上角点击头像,获取主账号 ID:
|
||||
1. 登录 [阿里云控制台](https://ram.console.aliyun.com/manage/ak)
|
||||
2. 点击右上角头像
|
||||
3. 复制主账号 ID(16 位数字)
|
||||
|
||||
#### 步骤 3: 更新环境变量
|
||||
|
||||
```bash
|
||||
# 新增必需的环境变量
|
||||
export ALIYUN_ACCOUNT_ID="1234567890123456"
|
||||
|
||||
# 其他环境变量保持不变
|
||||
export ALIYUN_ACCESS_KEY_ID="LTAI5t..."
|
||||
export ALIYUN_ACCESS_KEY_SECRET="..."
|
||||
export ALIYUN_REGION="cn-hangzhou"
|
||||
```
|
||||
|
||||
#### 步骤 4: 运行测试
|
||||
|
||||
```bash
|
||||
# 单元测试(不需要真实凭据)
|
||||
pytest agent/sandbox/tests/test_aliyun_codeinterpreter.py -v
|
||||
|
||||
# 集成测试(需要真实凭据)
|
||||
pytest agent/sandbox/tests/test_aliyun_codeinterpreter_integration.py -v -m integration
|
||||
```
|
||||
|
||||
## 文件变更清单
|
||||
|
||||
### ✅ 已完成
|
||||
|
||||
- [x] 创建 `aliyun_codeinterpreter.py` - 新的提供商实现
|
||||
- [x] 更新 `sandbox_spec.md` - 规范文档
|
||||
- [x] 更新 `admin/services.py` - 服务管理器
|
||||
- [x] 更新 `providers/__init__.py` - 包导出
|
||||
- [x] 创建 `test_aliyun_codeinterpreter.py` - 单元测试
|
||||
- [x] 创建 `test_aliyun_codeinterpreter_integration.py` - 集成测试
|
||||
|
||||
### 📝 可选清理
|
||||
|
||||
如果您想删除旧的 OpenSandbox 实现:
|
||||
|
||||
```bash
|
||||
# 删除旧文件(可选)
|
||||
rm agent/sandbox/providers/aliyun_opensandbox.py
|
||||
rm agent/sandbox/tests/test_aliyun_provider.py
|
||||
rm agent/sandbox/tests/test_aliyun_integration.py
|
||||
```
|
||||
|
||||
**注意**: 保留旧文件不会影响新功能,只是代码冗余。
|
||||
|
||||
## API 参考
|
||||
|
||||
### 控制面 API(沙箱管理)
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/sandboxes` | POST | 创建沙箱实例 |
|
||||
| `/sandboxes/{id}/stop` | POST | 停止实例 |
|
||||
| `/sandboxes/{id}` | DELETE | 删除实例 |
|
||||
| `/templates` | GET | 列出模板 |
|
||||
|
||||
### 数据面 API(代码执行)
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/sandboxes/{id}/execute` | POST | 执行代码(简化版) |
|
||||
| `/sandboxes/{id}/contexts` | POST | 创建上下文 |
|
||||
| `/sandboxes/{id}/contexts/{ctx_id}/execute` | POST | 在上下文中执行 |
|
||||
| `/sandboxes/{id}/health` | GET | 健康检查 |
|
||||
| `/sandboxes/{id}/files` | GET/POST | 文件读写 |
|
||||
| `/sandboxes/{id}/processes/cmd` | POST | 执行 Shell 命令 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么要添加 account_id?
|
||||
|
||||
**A**: Code Interpreter API 需要在请求头中提供 `X-Acs-Parent-Id`(阿里云主账号ID)进行身份验证。这是 Aliyun Code Interpreter API 的必需参数。
|
||||
|
||||
### Q: 30 秒超时限制可以绕过吗?
|
||||
|
||||
**A**: 不可以。这是 Aliyun Code Interpreter 的**硬限制**,无法通过配置或请求参数绕过。如果代码执行时间超过 30 秒,请考虑:
|
||||
1. 优化代码逻辑
|
||||
2. 分批处理数据
|
||||
3. 使用上下文保持状态
|
||||
|
||||
### Q: 旧的 OpenSandbox 配置还能用吗?
|
||||
|
||||
**A**: 不能。OpenSandbox 和 Code Interpreter 是两个不同的服务,API 不兼容。必须迁移到新的配置格式。
|
||||
|
||||
### Q: 如何获取阿里云主账号 ID?
|
||||
|
||||
**A**:
|
||||
1. 登录阿里云控制台
|
||||
2. 点击右上角的头像
|
||||
3. 在弹出的信息中可以看到"主账号ID"
|
||||
|
||||
### Q: 迁移后会影响现有功能吗?
|
||||
|
||||
**A**:
|
||||
- **自我管理提供商(self_managed)**: 不受影响
|
||||
- **E2B 提供商**: 不受影响
|
||||
- **Aliyun 提供商**: 需要更新配置并重新测试
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [官方文档](https://help.aliyun.com/zh/functioncompute/fc/sandbox-sandbox-code-interepreter)
|
||||
- [sandbox 规范](../docs/develop/sandbox_spec.md)
|
||||
- [测试指南](./README.md)
|
||||
- [快速开始](./QUICKSTART.md)
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请:
|
||||
1. 查看官方文档
|
||||
2. 检查配置是否正确
|
||||
3. 查看测试输出中的错误信息
|
||||
4. 联系 RAGFlow 团队
|
||||
178
agent/sandbox/tests/QUICKSTART.md
Normal file
178
agent/sandbox/tests/QUICKSTART.md
Normal file
@ -0,0 +1,178 @@
|
||||
# Aliyun OpenSandbox Provider - 快速测试指南
|
||||
|
||||
## 测试说明
|
||||
|
||||
### 1. 单元测试(不需要真实凭据)
|
||||
|
||||
单元测试使用 mock,**不需要**真实的 Aliyun 凭据,可以随时运行。
|
||||
|
||||
```bash
|
||||
# 运行 Aliyun 提供商的单元测试
|
||||
pytest agent/sandbox/tests/test_aliyun_provider.py -v
|
||||
|
||||
# 预期输出:
|
||||
# test_aliyun_provider.py::TestAliyunOpenSandboxProvider::test_provider_initialization PASSED
|
||||
# test_aliyun_provider.py::TestAliyunOpenSandboxProvider::test_initialize_success PASSED
|
||||
# ...
|
||||
# ========================= 48 passed in 2.34s ==========================
|
||||
```
|
||||
|
||||
### 2. 集成测试(需要真实凭据)
|
||||
|
||||
集成测试会调用真实的 Aliyun API,需要配置凭据。
|
||||
|
||||
#### 步骤 1: 配置环境变量
|
||||
|
||||
```bash
|
||||
export ALIYUN_ACCESS_KEY_ID="LTAI5t..." # 替换为真实的 Access Key ID
|
||||
export ALIYUN_ACCESS_KEY_SECRET="..." # 替换为真实的 Access Key Secret
|
||||
export ALIYUN_REGION="cn-hangzhou" # 可选,默认为 cn-hangzhou
|
||||
```
|
||||
|
||||
#### 步骤 2: 运行集成测试
|
||||
|
||||
```bash
|
||||
# 运行所有集成测试
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py -v -m integration
|
||||
|
||||
# 运行特定测试
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_health_check -v
|
||||
```
|
||||
|
||||
#### 步骤 3: 预期输出
|
||||
|
||||
```
|
||||
test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_initialize_provider PASSED
|
||||
test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_health_check PASSED
|
||||
test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_execute_python_code PASSED
|
||||
...
|
||||
========================== 10 passed in 15.67s ==========================
|
||||
```
|
||||
|
||||
### 3. 测试场景
|
||||
|
||||
#### 基础功能测试
|
||||
|
||||
```bash
|
||||
# 健康检查
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_health_check -v
|
||||
|
||||
# 创建实例
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_create_python_instance -v
|
||||
|
||||
# 执行代码
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_execute_python_code -v
|
||||
|
||||
# 销毁实例
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_destroy_instance -v
|
||||
```
|
||||
|
||||
#### 错误处理测试
|
||||
|
||||
```bash
|
||||
# 代码执行错误
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_execute_python_code_with_error -v
|
||||
|
||||
# 超时处理
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_execute_python_code_timeout -v
|
||||
```
|
||||
|
||||
#### 真实场景测试
|
||||
|
||||
```bash
|
||||
# 数据处理工作流
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunRealWorldScenarios::test_data_processing_workflow -v
|
||||
|
||||
# 字符串操作
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunRealWorldScenarios::test_string_manipulation -v
|
||||
|
||||
# 多次执行
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunRealWorldScenarios::test_multiple_executions_same_instance -v
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 没有凭据怎么办?
|
||||
|
||||
**A:** 运行单元测试即可,不需要真实凭据:
|
||||
```bash
|
||||
pytest agent/sandbox/tests/test_aliyun_provider.py -v
|
||||
```
|
||||
|
||||
### Q: 如何跳过集成测试?
|
||||
|
||||
**A:** 使用 pytest 标记跳过:
|
||||
```bash
|
||||
# 只运行单元测试,跳过集成测试
|
||||
pytest agent/sandbox/tests/ -v -m "not integration"
|
||||
```
|
||||
|
||||
### Q: 集成测试失败怎么办?
|
||||
|
||||
**A:** 检查以下几点:
|
||||
|
||||
1. **凭据是否正确**
|
||||
```bash
|
||||
echo $ALIYUN_ACCESS_KEY_ID
|
||||
echo $ALIYUN_ACCESS_KEY_SECRET
|
||||
```
|
||||
|
||||
2. **网络连接是否正常**
|
||||
```bash
|
||||
curl -I https://opensandbox.cn-hangzhou.aliyuncs.com
|
||||
```
|
||||
|
||||
3. **是否有 OpenSandbox 服务权限**
|
||||
- 登录阿里云控制台
|
||||
- 检查是否已开通 OpenSandbox 服务
|
||||
- 检查 AccessKey 权限
|
||||
|
||||
4. **查看详细错误信息**
|
||||
```bash
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py -v -s
|
||||
```
|
||||
|
||||
### Q: 测试超时怎么办?
|
||||
|
||||
**A:** 增加超时时间或检查网络:
|
||||
```bash
|
||||
# 使用更长的超时
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py -v --timeout=60
|
||||
```
|
||||
|
||||
## 测试命令速查表
|
||||
|
||||
| 命令 | 说明 | 需要凭据 |
|
||||
|------|------|---------|
|
||||
| `pytest agent/sandbox/tests/test_aliyun_provider.py -v` | 单元测试 | ❌ |
|
||||
| `pytest agent/sandbox/tests/test_aliyun_integration.py -v` | 集成测试 | ✅ |
|
||||
| `pytest agent/sandbox/tests/ -v -m "not integration"` | 仅单元测试 | ❌ |
|
||||
| `pytest agent/sandbox/tests/ -v -m integration` | 仅集成测试 | ✅ |
|
||||
| `pytest agent/sandbox/tests/ -v` | 所有测试 | 部分需要 |
|
||||
|
||||
## 获取 Aliyun 凭据
|
||||
|
||||
1. 访问 [阿里云控制台](https://ram.console.aliyun.com/manage/ak)
|
||||
2. 创建 AccessKey
|
||||
3. 保存 AccessKey ID 和 AccessKey Secret
|
||||
4. 设置环境变量
|
||||
|
||||
⚠️ **安全提示:**
|
||||
- 不要在代码中硬编码凭据
|
||||
- 使用环境变量或配置文件
|
||||
- 定期轮换 AccessKey
|
||||
- 限制 AccessKey 权限
|
||||
|
||||
## 下一步
|
||||
|
||||
1. ✅ **运行单元测试** - 验证代码逻辑
|
||||
2. 🔧 **配置凭据** - 设置环境变量
|
||||
3. 🚀 **运行集成测试** - 测试真实 API
|
||||
4. 📊 **查看结果** - 确保所有测试通过
|
||||
5. 🎯 **集成到系统** - 使用 admin API 配置提供商
|
||||
|
||||
## 需要帮助?
|
||||
|
||||
- 查看 [完整文档](README.md)
|
||||
- 检查 [sandbox 规范](../../../../../docs/develop/sandbox_spec.md)
|
||||
- 联系 RAGFlow 团队
|
||||
213
agent/sandbox/tests/README.md
Normal file
213
agent/sandbox/tests/README.md
Normal file
@ -0,0 +1,213 @@
|
||||
# Sandbox Provider Tests
|
||||
|
||||
This directory contains tests for the RAGFlow sandbox provider system.
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── pytest.ini # Pytest configuration
|
||||
├── test_providers.py # Unit tests for all providers (mocked)
|
||||
├── test_aliyun_provider.py # Unit tests for Aliyun provider (mocked)
|
||||
├── test_aliyun_integration.py # Integration tests for Aliyun (real API)
|
||||
└── sandbox_security_tests_full.py # Security tests for self-managed provider
|
||||
```
|
||||
|
||||
## Test Types
|
||||
|
||||
### 1. Unit Tests (No Credentials Required)
|
||||
|
||||
Unit tests use mocks and don't require any external services or credentials.
|
||||
|
||||
**Files:**
|
||||
- `test_providers.py` - Tests for base provider interface and manager
|
||||
- `test_aliyun_provider.py` - Tests for Aliyun provider with mocked API calls
|
||||
|
||||
**Run unit tests:**
|
||||
```bash
|
||||
# Run all unit tests
|
||||
pytest agent/sandbox/tests/test_providers.py -v
|
||||
pytest agent/sandbox/tests/test_aliyun_provider.py -v
|
||||
|
||||
# Run specific test
|
||||
pytest agent/sandbox/tests/test_aliyun_provider.py::TestAliyunOpenSandboxProvider::test_initialize_success -v
|
||||
|
||||
# Run all unit tests (skip integration)
|
||||
pytest agent/sandbox/tests/ -v -m "not integration"
|
||||
```
|
||||
|
||||
### 2. Integration Tests (Real Credentials Required)
|
||||
|
||||
Integration tests make real API calls to Aliyun OpenSandbox service.
|
||||
|
||||
**Files:**
|
||||
- `test_aliyun_integration.py` - Tests with real Aliyun API calls
|
||||
|
||||
**Setup environment variables:**
|
||||
```bash
|
||||
export ALIYUN_ACCESS_KEY_ID="LTAI5t..."
|
||||
export ALIYUN_ACCESS_KEY_SECRET="..."
|
||||
export ALIYUN_REGION="cn-hangzhou" # Optional, defaults to cn-hangzhou
|
||||
export ALIYUN_WORKSPACE_ID="ws-..." # Optional
|
||||
```
|
||||
|
||||
**Run integration tests:**
|
||||
```bash
|
||||
# Run only integration tests
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py -v -m integration
|
||||
|
||||
# Run all tests including integration
|
||||
pytest agent/sandbox/tests/ -v
|
||||
|
||||
# Run specific integration test
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_health_check -v
|
||||
```
|
||||
|
||||
### 3. Security Tests
|
||||
|
||||
Security tests validate the security features of the self-managed sandbox provider.
|
||||
|
||||
**Files:**
|
||||
- `sandbox_security_tests_full.py` - Comprehensive security tests
|
||||
|
||||
**Run security tests:**
|
||||
```bash
|
||||
# Run all security tests
|
||||
pytest agent/sandbox/tests/sandbox_security_tests_full.py -v
|
||||
|
||||
# Run specific security test
|
||||
pytest agent/sandbox/tests/sandbox_security_tests_full.py -k "test_dangerous_imports" -v
|
||||
```
|
||||
|
||||
## Test Commands
|
||||
|
||||
### Quick Test Commands
|
||||
|
||||
```bash
|
||||
# Run all sandbox tests (unit only, fast)
|
||||
pytest agent/sandbox/tests/ -v -m "not integration" --tb=short
|
||||
|
||||
# Run tests with coverage
|
||||
pytest agent/sandbox/tests/ -v --cov=agent.sandbox --cov-report=term-missing -m "not integration"
|
||||
|
||||
# Run tests and stop on first failure
|
||||
pytest agent/sandbox/tests/ -v -x -m "not integration"
|
||||
|
||||
# Run tests in parallel (requires pytest-xdist)
|
||||
pytest agent/sandbox/tests/ -v -n auto -m "not integration"
|
||||
```
|
||||
|
||||
### Aliyun Provider Testing
|
||||
|
||||
```bash
|
||||
# 1. Run unit tests (no credentials needed)
|
||||
pytest agent/sandbox/tests/test_aliyun_provider.py -v
|
||||
|
||||
# 2. Set up credentials for integration tests
|
||||
export ALIYUN_ACCESS_KEY_ID="your-key-id"
|
||||
export ALIYUN_ACCESS_KEY_SECRET="your-secret"
|
||||
export ALIYUN_REGION="cn-hangzhou"
|
||||
|
||||
# 3. Run integration tests (makes real API calls)
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py -v
|
||||
|
||||
# 4. Test specific scenarios
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_execute_python_code -v
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py::TestAliyunRealWorldScenarios -v
|
||||
```
|
||||
|
||||
## Understanding Test Results
|
||||
|
||||
### Unit Test Output
|
||||
|
||||
```
|
||||
agent/sandbox/tests/test_aliyun_provider.py::TestAliyunOpenSandboxProvider::test_initialize_success PASSED
|
||||
agent/sandbox/tests/test_aliyun_provider.py::TestAliyunOpenSandboxProvider::test_create_instance_python PASSED
|
||||
...
|
||||
========================== 48 passed in 2.34s ===========================
|
||||
```
|
||||
|
||||
### Integration Test Output
|
||||
|
||||
```
|
||||
agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_health_check PASSED
|
||||
agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_create_python_instance PASSED
|
||||
agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_execute_python_code PASSED
|
||||
...
|
||||
========================== 10 passed in 15.67s ===========================
|
||||
```
|
||||
|
||||
**Note:** Integration tests will be skipped if credentials are not set:
|
||||
```
|
||||
agent/sandbox/tests/test_aliyun_integration.py::TestAliyunOpenSandboxIntegration::test_health_check SKIPPED
|
||||
...
|
||||
========================== 48 skipped, 10 passed in 0.12s ===========================
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Integration Tests Fail
|
||||
|
||||
1. **Check credentials:**
|
||||
```bash
|
||||
echo $ALIYUN_ACCESS_KEY_ID
|
||||
echo $ALIYUN_ACCESS_KEY_SECRET
|
||||
```
|
||||
|
||||
2. **Check network connectivity:**
|
||||
```bash
|
||||
curl -I https://opensandbox.cn-hangzhou.aliyuncs.com
|
||||
```
|
||||
|
||||
3. **Verify permissions:**
|
||||
- Make sure your Aliyun account has OpenSandbox service enabled
|
||||
- Check that your AccessKey has the required permissions
|
||||
|
||||
4. **Check region:**
|
||||
- Verify the region is correct for your account
|
||||
- Try different regions: cn-hangzhou, cn-beijing, cn-shanghai, etc.
|
||||
|
||||
### Tests Timeout
|
||||
|
||||
If tests timeout, increase the timeout in the test configuration or run with a longer timeout:
|
||||
```bash
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py -v --timeout=60
|
||||
```
|
||||
|
||||
### Mock Tests Fail
|
||||
|
||||
If unit tests fail, it's likely a code issue, not a credentials issue:
|
||||
1. Check the test error message
|
||||
2. Review the code changes
|
||||
3. Run with verbose output: `pytest -vv`
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new providers:
|
||||
|
||||
1. **Create unit tests** in `test_{provider}_provider.py` with mocks
|
||||
2. **Create integration tests** in `test_{provider}_integration.py` with real API calls
|
||||
3. **Add markers** to distinguish test types
|
||||
4. **Update this README** with provider-specific testing instructions
|
||||
|
||||
Example:
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
def test_new_provider_real_api():
|
||||
"""Test with real API calls."""
|
||||
# Your test here
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
In CI/CD pipelines:
|
||||
|
||||
```yaml
|
||||
# Run unit tests only (fast, no credentials)
|
||||
pytest agent/sandbox/tests/ -v -m "not integration"
|
||||
|
||||
# Run integration tests if credentials available
|
||||
if [ -n "$ALIYUN_ACCESS_KEY_ID" ]; then
|
||||
pytest agent/sandbox/tests/test_aliyun_integration.py -v -m integration
|
||||
fi
|
||||
```
|
||||
19
agent/sandbox/tests/__init__.py
Normal file
19
agent/sandbox/tests/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Sandbox provider tests package.
|
||||
"""
|
||||
33
agent/sandbox/tests/pytest.ini
Normal file
33
agent/sandbox/tests/pytest.ini
Normal file
@ -0,0 +1,33 @@
|
||||
[pytest]
|
||||
# Pytest configuration for sandbox tests
|
||||
|
||||
# Test discovery patterns
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
|
||||
# Markers for different test types
|
||||
markers =
|
||||
integration: Tests that require external services (Aliyun API, etc.)
|
||||
unit: Fast tests that don't require external services
|
||||
slow: Tests that take a long time to run
|
||||
|
||||
# Test paths
|
||||
testpaths = .
|
||||
|
||||
# Minimum version
|
||||
minversion = 7.0
|
||||
|
||||
# Output options
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--disable-warnings
|
||||
|
||||
# Log options
|
||||
log_cli = false
|
||||
log_cli_level = INFO
|
||||
|
||||
# Coverage options (if using pytest-cov)
|
||||
# addopts = --cov=agent.sandbox --cov-report=html --cov-report=term
|
||||
436
agent/sandbox/tests/sandbox_security_tests_full.py
Normal file
436
agent/sandbox/tests/sandbox_security_tests_full.py
Normal file
@ -0,0 +1,436 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import base64
|
||||
import os
|
||||
import textwrap
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional
|
||||
|
||||
import requests
|
||||
from pydantic import BaseModel
|
||||
|
||||
API_URL = os.getenv("SANDBOX_API_URL", "http://localhost:9385/run")
|
||||
TIMEOUT = 15
|
||||
MAX_WORKERS = 5
|
||||
|
||||
|
||||
class ResultStatus(str, Enum):
|
||||
SUCCESS = "success"
|
||||
PROGRAM_ERROR = "program_error"
|
||||
RESOURCE_LIMIT_EXCEEDED = "resource_limit_exceeded"
|
||||
UNAUTHORIZED_ACCESS = "unauthorized_access"
|
||||
RUNTIME_ERROR = "runtime_error"
|
||||
PROGRAM_RUNNER_ERROR = "program_runner_error"
|
||||
|
||||
|
||||
class ResourceLimitType(str, Enum):
|
||||
TIME = "time"
|
||||
MEMORY = "memory"
|
||||
OUTPUT = "output"
|
||||
|
||||
|
||||
class UnauthorizedAccessType(str, Enum):
|
||||
DISALLOWED_SYSCALL = "disallowed_syscall"
|
||||
FILE_ACCESS = "file_access"
|
||||
NETWORK_ACCESS = "network_access"
|
||||
|
||||
|
||||
class RuntimeErrorType(str, Enum):
|
||||
SIGNALLED = "signalled"
|
||||
NONZERO_EXIT = "nonzero_exit"
|
||||
|
||||
|
||||
class ExecutionResult(BaseModel):
|
||||
status: ResultStatus
|
||||
stdout: str
|
||||
stderr: str
|
||||
exit_code: int
|
||||
detail: Optional[str] = None
|
||||
resource_limit_type: Optional[ResourceLimitType] = None
|
||||
unauthorized_access_type: Optional[UnauthorizedAccessType] = None
|
||||
runtime_error_type: Optional[RuntimeErrorType] = None
|
||||
|
||||
|
||||
class TestResult(BaseModel):
|
||||
name: str
|
||||
passed: bool
|
||||
duration: float
|
||||
expected_failure: bool = False
|
||||
result: Optional[ExecutionResult] = None
|
||||
error: Optional[str] = None
|
||||
validation_error: Optional[str] = None
|
||||
|
||||
|
||||
def encode_code(code: str) -> str:
|
||||
return base64.b64encode(code.encode("utf-8")).decode("utf-8")
|
||||
|
||||
|
||||
def execute_single_test(name: str, code: str, language: str, arguments: dict, expect_fail: bool = False) -> TestResult:
|
||||
"""Execute a single test case"""
|
||||
payload = {
|
||||
"code_b64": encode_code(textwrap.dedent(code)),
|
||||
"language": language,
|
||||
"arguments": arguments,
|
||||
}
|
||||
|
||||
test_result = TestResult(name=name, passed=False, duration=0, expected_failure=expect_fail)
|
||||
|
||||
really_processed = False
|
||||
try:
|
||||
while not really_processed:
|
||||
start_time = time.perf_counter()
|
||||
|
||||
resp = requests.post(API_URL, json=payload, timeout=TIMEOUT)
|
||||
resp.raise_for_status()
|
||||
response_data = resp.json()
|
||||
if response_data["exit_code"] == -429: # too many request
|
||||
print(f"[{name}] Reached request limit, retring...")
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
really_processed = True
|
||||
|
||||
print("-------------------")
|
||||
print(f"{name}:\n{response_data}")
|
||||
print("-------------------")
|
||||
|
||||
test_result.duration = time.perf_counter() - start_time
|
||||
test_result.result = ExecutionResult(**response_data)
|
||||
|
||||
# Validate test result expectations
|
||||
validate_test_result(name, expect_fail, test_result)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
test_result.duration = time.perf_counter() - start_time
|
||||
test_result.error = f"Request failed: {str(e)}"
|
||||
test_result.result = ExecutionResult(
|
||||
status=ResultStatus.PROGRAM_RUNNER_ERROR,
|
||||
stdout="",
|
||||
stderr=str(e),
|
||||
exit_code=-999,
|
||||
detail="request_failed",
|
||||
)
|
||||
|
||||
return test_result
|
||||
|
||||
|
||||
def validate_test_result(name: str, expect_fail: bool, test_result: TestResult):
|
||||
"""Validate if the test result meets expectations"""
|
||||
if not test_result.result:
|
||||
test_result.passed = False
|
||||
test_result.validation_error = "No result returned"
|
||||
return
|
||||
|
||||
test_result.passed = test_result.result.status == ResultStatus.SUCCESS
|
||||
# General validation logic
|
||||
if expect_fail:
|
||||
# Tests expected to fail should return a non-success status
|
||||
if test_result.passed:
|
||||
test_result.validation_error = "Expected failure but actually succeeded"
|
||||
else:
|
||||
# Tests expected to succeed should return a success status
|
||||
if not test_result.passed:
|
||||
test_result.validation_error = f"Unexpected failure (status={test_result.result.status})"
|
||||
|
||||
|
||||
def get_test_cases() -> Dict[str, dict]:
|
||||
"""Return test cases (code, whether expected to fail)"""
|
||||
return {
|
||||
"1 Infinite loop: Should be forcibly terminated": {
|
||||
"code": """
|
||||
def main():
|
||||
while True:
|
||||
pass
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"2 Infinite loop: Should be forcibly terminated": {
|
||||
"code": """
|
||||
def main():
|
||||
while True:
|
||||
pass
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"3 Infinite loop: Should be forcibly terminated": {
|
||||
"code": """
|
||||
def main():
|
||||
while True:
|
||||
pass
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"4 Infinite loop: Should be forcibly terminated": {
|
||||
"code": """
|
||||
def main():
|
||||
while True:
|
||||
pass
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"5 Infinite loop: Should be forcibly terminated": {
|
||||
"code": """
|
||||
def main():
|
||||
while True:
|
||||
pass
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"6 Infinite loop: Should be forcibly terminated": {
|
||||
"code": """
|
||||
def main():
|
||||
while True:
|
||||
pass
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"7 Normal test: Python without dependencies": {
|
||||
"code": """
|
||||
def main():
|
||||
return {"data": "hello, world"}
|
||||
""",
|
||||
"should_fail": False,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"8 Normal test: Python with pandas, should pass without any error": {
|
||||
"code": """
|
||||
import pandas as pd
|
||||
|
||||
def main():
|
||||
data = {'Name': ['Alice', 'Bob', 'Charlie'],
|
||||
'Age': [25, 30, 35]}
|
||||
df = pd.DataFrame(data)
|
||||
""",
|
||||
"should_fail": False,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"9 Normal test: Nodejs without dependencies, should pass without any error": {
|
||||
"code": """
|
||||
const https = require('https');
|
||||
|
||||
async function main(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get('https://example.com/', (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
clearTimeout(timeout);
|
||||
console.log('Body:', data);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
req.destroy(new Error('Request timeout after 10s'));
|
||||
}, 10000);
|
||||
|
||||
req.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
console.error('Error:', err.message);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
""",
|
||||
"should_fail": False,
|
||||
"arguments": {},
|
||||
"language": "nodejs",
|
||||
},
|
||||
"10 Normal test: Nodejs with axios, should pass without any error": {
|
||||
"code": """
|
||||
const axios = require('axios');
|
||||
|
||||
async function main(args) {
|
||||
try {
|
||||
const response = await axios.get('https://example.com/', {
|
||||
timeout: 10000
|
||||
});
|
||||
console.log('Body:', response.data);
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
""",
|
||||
"should_fail": False,
|
||||
"arguments": {},
|
||||
"language": "nodejs",
|
||||
},
|
||||
"11 Dangerous import: Should fail due to os module import": {
|
||||
"code": """
|
||||
import os
|
||||
|
||||
def main():
|
||||
pass
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"12 Dangerous import from subprocess: Should fail due to subprocess import": {
|
||||
"code": """
|
||||
from subprocess import Popen
|
||||
|
||||
def main():
|
||||
pass
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"13 Dangerous call: Should fail due to eval function call": {
|
||||
"code": """
|
||||
def main():
|
||||
eval('os.system("echo hello")')
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"14 Dangerous attribute access: Should fail due to shutil.rmtree": {
|
||||
"code": """
|
||||
import shutil
|
||||
|
||||
def main():
|
||||
shutil.rmtree('/some/path')
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"15 Dangerous binary operation: Should fail due to unsafe concatenation leading to eval": {
|
||||
"code": """
|
||||
def main():
|
||||
dangerous_string = "os." + "system"
|
||||
eval(dangerous_string + '("echo hello")')
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"16 Dangerous function definition: Should fail due to user-defined eval function": {
|
||||
"code": """
|
||||
def eval_function():
|
||||
eval('os.system("echo hello")')
|
||||
|
||||
def main():
|
||||
eval_function()
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
"17 Memory exhaustion(256m): Should fail due to exceeding memory limit(try to allocate 300m)": {
|
||||
"code": """
|
||||
def main():
|
||||
x = ['a' * 1024 * 1024] * 300 # 300MB
|
||||
""",
|
||||
"should_fail": True,
|
||||
"arguments": {},
|
||||
"language": "python",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def print_test_report(results: Dict[str, TestResult]):
|
||||
print("\n=== 🔍 Test Report ===")
|
||||
|
||||
max_name_len = max(len(name) for name in results)
|
||||
|
||||
for name, result in results.items():
|
||||
status = "✅" if result.passed else "❌"
|
||||
if result.expected_failure:
|
||||
status = "⚠️" if result.passed else "✓" # Expected failure case
|
||||
|
||||
print(f"{status} {name.ljust(max_name_len)} {result.duration:.2f}s")
|
||||
|
||||
if result.error:
|
||||
print(f" REQUEST ERROR: {result.error}")
|
||||
if result.validation_error:
|
||||
print(f" VALIDATION ERROR: {result.validation_error}")
|
||||
|
||||
if result.result and not result.passed:
|
||||
print(f" STATUS: {result.result.status}")
|
||||
if result.result.stderr:
|
||||
print(f" STDERR: {result.result.stderr[:200]}...")
|
||||
if result.result.detail:
|
||||
print(f" DETAIL: {result.result.detail}")
|
||||
|
||||
passed = sum(1 for r in results.values() if ((not r.expected_failure and r.passed) or (r.expected_failure and not r.passed)))
|
||||
failed = len(results) - passed
|
||||
|
||||
print("\n=== 📊 Statistics ===")
|
||||
print(f"✅ Passed: {passed}")
|
||||
print(f"❌ Failed: {failed}")
|
||||
print(f"📌 Total: {len(results)}")
|
||||
|
||||
|
||||
def main():
|
||||
print(f"🔐 Starting sandbox security tests (API: {API_URL})")
|
||||
print(f"🚀 Concurrent threads: {MAX_WORKERS}")
|
||||
|
||||
test_cases = get_test_cases()
|
||||
results = {}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||
futures = {}
|
||||
for name, detail in test_cases.items():
|
||||
# ✅ Log when a task is submitted
|
||||
print(f"✅ Task submitted: {name}")
|
||||
time.sleep(0.4)
|
||||
future = executor.submit(execute_single_test, name, detail["code"], detail["language"], detail["arguments"], detail["should_fail"])
|
||||
futures[future] = name
|
||||
|
||||
print("\n=== 🚦 Test Progress ===")
|
||||
for i, future in enumerate(as_completed(futures)):
|
||||
name = futures[future]
|
||||
print(f" {i + 1}/{len(test_cases)} completed: {name}")
|
||||
try:
|
||||
results[name] = future.result()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Test {name} execution exception: {str(e)}")
|
||||
results[name] = TestResult(name=name, passed=False, duration=0, error=f"Execution exception: {str(e)}")
|
||||
|
||||
print_test_report(results)
|
||||
|
||||
if any(not r.passed and not r.expected_failure for r in results.values()):
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
329
agent/sandbox/tests/test_aliyun_codeinterpreter.py
Normal file
329
agent/sandbox/tests/test_aliyun_codeinterpreter.py
Normal file
@ -0,0 +1,329 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Unit tests for Aliyun Code Interpreter provider.
|
||||
|
||||
These tests use mocks and don't require real Aliyun credentials.
|
||||
|
||||
Official Documentation: https://help.aliyun.com/zh/functioncompute/fc/sandbox-sandbox-code-interepreter
|
||||
Official SDK: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from agent.sandbox.providers.base import SandboxProvider
|
||||
from agent.sandbox.providers.aliyun_codeinterpreter import AliyunCodeInterpreterProvider
|
||||
|
||||
|
||||
class TestAliyunCodeInterpreterProvider:
|
||||
"""Test AliyunCodeInterpreterProvider implementation."""
|
||||
|
||||
def test_provider_initialization(self):
|
||||
"""Test provider initialization."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
assert provider.access_key_id == ""
|
||||
assert provider.access_key_secret == ""
|
||||
assert provider.account_id == ""
|
||||
assert provider.region == "cn-hangzhou"
|
||||
assert provider.template_name == ""
|
||||
assert provider.timeout == 30
|
||||
assert not provider._initialized
|
||||
|
||||
@patch("agent.sandbox.providers.aliyun_codeinterpreter.Template")
|
||||
def test_initialize_success(self, mock_template):
|
||||
"""Test successful initialization."""
|
||||
# Mock health check response
|
||||
mock_template.list.return_value = []
|
||||
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
result = provider.initialize(
|
||||
{
|
||||
"access_key_id": "LTAI5tXXXXXXXXXX",
|
||||
"access_key_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"account_id": "1234567890123456",
|
||||
"region": "cn-hangzhou",
|
||||
"template_name": "python-sandbox",
|
||||
"timeout": 20,
|
||||
}
|
||||
)
|
||||
|
||||
assert result is True
|
||||
assert provider.access_key_id == "LTAI5tXXXXXXXXXX"
|
||||
assert provider.access_key_secret == "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
assert provider.account_id == "1234567890123456"
|
||||
assert provider.region == "cn-hangzhou"
|
||||
assert provider.template_name == "python-sandbox"
|
||||
assert provider.timeout == 20
|
||||
assert provider._initialized
|
||||
|
||||
def test_initialize_missing_credentials(self):
|
||||
"""Test initialization with missing credentials."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
# Missing access_key_id
|
||||
result = provider.initialize({"access_key_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"})
|
||||
assert result is False
|
||||
|
||||
# Missing access_key_secret
|
||||
result = provider.initialize({"access_key_id": "LTAI5tXXXXXXXXXX"})
|
||||
assert result is False
|
||||
|
||||
# Missing account_id
|
||||
provider2 = AliyunCodeInterpreterProvider()
|
||||
result = provider2.initialize({"access_key_id": "LTAI5tXXXXXXXXXX", "access_key_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"})
|
||||
assert result is False
|
||||
|
||||
@patch("agent.sandbox.providers.aliyun_codeinterpreter.Template")
|
||||
def test_initialize_default_config(self, mock_template):
|
||||
"""Test initialization with default config."""
|
||||
mock_template.list.return_value = []
|
||||
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
result = provider.initialize({"access_key_id": "LTAI5tXXXXXXXXXX", "access_key_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "account_id": "1234567890123456"})
|
||||
|
||||
assert result is True
|
||||
assert provider.region == "cn-hangzhou"
|
||||
assert provider.template_name == ""
|
||||
|
||||
@patch("agent.sandbox.providers.aliyun_codeinterpreter.CodeInterpreterSandbox")
|
||||
def test_create_instance_python(self, mock_sandbox_class):
|
||||
"""Test creating a Python instance."""
|
||||
# Mock successful instance creation
|
||||
mock_sandbox = MagicMock()
|
||||
mock_sandbox.sandbox_id = "01JCED8Z9Y6XQVK8M2NRST5WXY"
|
||||
mock_sandbox_class.return_value = mock_sandbox
|
||||
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
provider._initialized = True
|
||||
provider._config = MagicMock()
|
||||
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
assert instance.provider == "aliyun_codeinterpreter"
|
||||
assert instance.status == "READY"
|
||||
assert instance.metadata["language"] == "python"
|
||||
|
||||
@patch("agent.sandbox.providers.aliyun_codeinterpreter.CodeInterpreterSandbox")
|
||||
def test_create_instance_javascript(self, mock_sandbox_class):
|
||||
"""Test creating a JavaScript instance."""
|
||||
mock_sandbox = MagicMock()
|
||||
mock_sandbox.sandbox_id = "01JCED8Z9Y6XQVK8M2NRST5WXY"
|
||||
mock_sandbox_class.return_value = mock_sandbox
|
||||
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
provider._initialized = True
|
||||
provider._config = MagicMock()
|
||||
|
||||
instance = provider.create_instance("javascript")
|
||||
|
||||
assert instance.metadata["language"] == "javascript"
|
||||
|
||||
def test_create_instance_not_initialized(self):
|
||||
"""Test creating instance when provider not initialized."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
with pytest.raises(RuntimeError, match="Provider not initialized"):
|
||||
provider.create_instance("python")
|
||||
|
||||
@patch("agent.sandbox.providers.aliyun_codeinterpreter.CodeInterpreterSandbox")
|
||||
def test_execute_code_success(self, mock_sandbox_class):
|
||||
"""Test successful code execution."""
|
||||
# Mock sandbox instance
|
||||
mock_sandbox = MagicMock()
|
||||
mock_sandbox.context.execute.return_value = {
|
||||
"results": [{"type": "stdout", "text": "Hello, World!"}, {"type": "result", "text": "None"}, {"type": "endOfExecution", "status": "ok"}],
|
||||
"contextId": "kernel-12345-67890",
|
||||
}
|
||||
mock_sandbox_class.return_value = mock_sandbox
|
||||
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
provider._initialized = True
|
||||
provider._config = MagicMock()
|
||||
|
||||
result = provider.execute_code(instance_id="01JCED8Z9Y6XQVK8M2NRST5WXY", code="print('Hello, World!')", language="python", timeout=10)
|
||||
|
||||
assert result.stdout == "Hello, World!"
|
||||
assert result.stderr == ""
|
||||
assert result.exit_code == 0
|
||||
assert result.execution_time > 0
|
||||
|
||||
@patch("agent.sandbox.providers.aliyun_codeinterpreter.CodeInterpreterSandbox")
|
||||
def test_execute_code_timeout(self, mock_sandbox_class):
|
||||
"""Test code execution timeout."""
|
||||
from agentrun.utils.exception import ServerError
|
||||
|
||||
mock_sandbox = MagicMock()
|
||||
mock_sandbox.context.execute.side_effect = ServerError(408, "Request timeout")
|
||||
mock_sandbox_class.return_value = mock_sandbox
|
||||
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
provider._initialized = True
|
||||
provider._config = MagicMock()
|
||||
|
||||
with pytest.raises(TimeoutError, match="Execution timed out"):
|
||||
provider.execute_code(instance_id="01JCED8Z9Y6XQVK8M2NRST5WXY", code="while True: pass", language="python", timeout=5)
|
||||
|
||||
@patch("agent.sandbox.providers.aliyun_codeinterpreter.CodeInterpreterSandbox")
|
||||
def test_execute_code_with_error(self, mock_sandbox_class):
|
||||
"""Test code execution with error."""
|
||||
mock_sandbox = MagicMock()
|
||||
mock_sandbox.context.execute.return_value = {
|
||||
"results": [{"type": "stderr", "text": "Traceback..."}, {"type": "error", "text": "NameError: name 'x' is not defined"}, {"type": "endOfExecution", "status": "error"}]
|
||||
}
|
||||
mock_sandbox_class.return_value = mock_sandbox
|
||||
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
provider._initialized = True
|
||||
provider._config = MagicMock()
|
||||
|
||||
result = provider.execute_code(instance_id="01JCED8Z9Y6XQVK8M2NRST5WXY", code="print(x)", language="python")
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert len(result.stderr) > 0
|
||||
|
||||
def test_get_supported_languages(self):
|
||||
"""Test getting supported languages."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
languages = provider.get_supported_languages()
|
||||
|
||||
assert "python" in languages
|
||||
assert "javascript" in languages
|
||||
|
||||
def test_get_config_schema(self):
|
||||
"""Test getting configuration schema."""
|
||||
schema = AliyunCodeInterpreterProvider.get_config_schema()
|
||||
|
||||
assert "access_key_id" in schema
|
||||
assert schema["access_key_id"]["required"] is True
|
||||
|
||||
assert "access_key_secret" in schema
|
||||
assert schema["access_key_secret"]["required"] is True
|
||||
|
||||
assert "account_id" in schema
|
||||
assert schema["account_id"]["required"] is True
|
||||
|
||||
assert "region" in schema
|
||||
assert "template_name" in schema
|
||||
assert "timeout" in schema
|
||||
|
||||
def test_validate_config_success(self):
|
||||
"""Test successful configuration validation."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
is_valid, error_msg = provider.validate_config({"access_key_id": "LTAI5tXXXXXXXXXX", "account_id": "1234567890123456", "region": "cn-hangzhou"})
|
||||
|
||||
assert is_valid is True
|
||||
assert error_msg is None
|
||||
|
||||
def test_validate_config_invalid_access_key(self):
|
||||
"""Test validation with invalid access key format."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
is_valid, error_msg = provider.validate_config({"access_key_id": "INVALID_KEY"})
|
||||
|
||||
assert is_valid is False
|
||||
assert "AccessKey ID format" in error_msg
|
||||
|
||||
def test_validate_config_missing_account_id(self):
|
||||
"""Test validation with missing account ID."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
is_valid, error_msg = provider.validate_config({})
|
||||
|
||||
assert is_valid is False
|
||||
assert "Account ID" in error_msg
|
||||
|
||||
def test_validate_config_invalid_region(self):
|
||||
"""Test validation with invalid region."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
is_valid, error_msg = provider.validate_config(
|
||||
{
|
||||
"access_key_id": "LTAI5tXXXXXXXXXX",
|
||||
"account_id": "1234567890123456", # Provide required field
|
||||
"region": "us-west-1",
|
||||
}
|
||||
)
|
||||
|
||||
assert is_valid is False
|
||||
assert "Invalid region" in error_msg
|
||||
|
||||
def test_validate_config_invalid_timeout(self):
|
||||
"""Test validation with invalid timeout (> 30 seconds)."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
is_valid, error_msg = provider.validate_config(
|
||||
{
|
||||
"access_key_id": "LTAI5tXXXXXXXXXX",
|
||||
"account_id": "1234567890123456", # Provide required field
|
||||
"timeout": 60,
|
||||
}
|
||||
)
|
||||
|
||||
assert is_valid is False
|
||||
assert "Timeout must be between 1 and 30 seconds" in error_msg
|
||||
|
||||
def test_normalize_language_python(self):
|
||||
"""Test normalizing Python language identifier."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
assert provider._normalize_language("python") == "python"
|
||||
assert provider._normalize_language("python3") == "python"
|
||||
assert provider._normalize_language("PYTHON") == "python"
|
||||
|
||||
def test_normalize_language_javascript(self):
|
||||
"""Test normalizing JavaScript language identifier."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
assert provider._normalize_language("javascript") == "javascript"
|
||||
assert provider._normalize_language("nodejs") == "javascript"
|
||||
assert provider._normalize_language("JavaScript") == "javascript"
|
||||
|
||||
|
||||
class TestAliyunCodeInterpreterInterface:
|
||||
"""Test that Aliyun provider correctly implements the interface."""
|
||||
|
||||
def test_aliyun_provider_is_abstract(self):
|
||||
"""Test that AliyunCodeInterpreterProvider is a SandboxProvider."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
assert isinstance(provider, SandboxProvider)
|
||||
|
||||
def test_aliyun_provider_has_abstract_methods(self):
|
||||
"""Test that AliyunCodeInterpreterProvider implements all abstract methods."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
|
||||
assert hasattr(provider, "initialize")
|
||||
assert callable(provider.initialize)
|
||||
|
||||
assert hasattr(provider, "create_instance")
|
||||
assert callable(provider.create_instance)
|
||||
|
||||
assert hasattr(provider, "execute_code")
|
||||
assert callable(provider.execute_code)
|
||||
|
||||
assert hasattr(provider, "destroy_instance")
|
||||
assert callable(provider.destroy_instance)
|
||||
|
||||
assert hasattr(provider, "health_check")
|
||||
assert callable(provider.health_check)
|
||||
|
||||
assert hasattr(provider, "get_supported_languages")
|
||||
assert callable(provider.get_supported_languages)
|
||||
353
agent/sandbox/tests/test_aliyun_codeinterpreter_integration.py
Normal file
353
agent/sandbox/tests/test_aliyun_codeinterpreter_integration.py
Normal file
@ -0,0 +1,353 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Integration tests for Aliyun Code Interpreter provider.
|
||||
|
||||
These tests require real Aliyun credentials and will make actual API calls.
|
||||
To run these tests, set the following environment variables:
|
||||
|
||||
export AGENTRUN_ACCESS_KEY_ID="LTAI5t..."
|
||||
export AGENTRUN_ACCESS_KEY_SECRET="..."
|
||||
export AGENTRUN_ACCOUNT_ID="1234567890..." # Aliyun primary account ID (主账号ID)
|
||||
export AGENTRUN_REGION="cn-hangzhou" # Note: AGENTRUN_REGION (SDK will read this)
|
||||
|
||||
Then run:
|
||||
pytest agent/sandbox/tests/test_aliyun_codeinterpreter_integration.py -v
|
||||
|
||||
Official Documentation: https://help.aliyun.com/zh/functioncompute/fc/sandbox-sandbox-code-interepreter
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from agent.sandbox.providers.aliyun_codeinterpreter import AliyunCodeInterpreterProvider
|
||||
|
||||
|
||||
# Skip all tests if credentials are not provided
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not all(
|
||||
[
|
||||
os.getenv("AGENTRUN_ACCESS_KEY_ID"),
|
||||
os.getenv("AGENTRUN_ACCESS_KEY_SECRET"),
|
||||
os.getenv("AGENTRUN_ACCOUNT_ID"),
|
||||
]
|
||||
),
|
||||
reason="Aliyun credentials not set. Set AGENTRUN_ACCESS_KEY_ID, AGENTRUN_ACCESS_KEY_SECRET, and AGENTRUN_ACCOUNT_ID.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aliyun_config():
|
||||
"""Get Aliyun configuration from environment variables."""
|
||||
return {
|
||||
"access_key_id": os.getenv("AGENTRUN_ACCESS_KEY_ID"),
|
||||
"access_key_secret": os.getenv("AGENTRUN_ACCESS_KEY_SECRET"),
|
||||
"account_id": os.getenv("AGENTRUN_ACCOUNT_ID"),
|
||||
"region": os.getenv("AGENTRUN_REGION", "cn-hangzhou"),
|
||||
"template_name": os.getenv("AGENTRUN_TEMPLATE_NAME", ""),
|
||||
"timeout": 30,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider(aliyun_config):
|
||||
"""Create an initialized Aliyun provider."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
initialized = provider.initialize(aliyun_config)
|
||||
if not initialized:
|
||||
pytest.skip("Failed to initialize Aliyun provider. Check credentials, account ID, and network.")
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAliyunCodeInterpreterIntegration:
|
||||
"""Integration tests for Aliyun Code Interpreter provider."""
|
||||
|
||||
def test_initialize_provider(self, aliyun_config):
|
||||
"""Test provider initialization with real credentials."""
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
result = provider.initialize(aliyun_config)
|
||||
|
||||
assert result is True
|
||||
assert provider._initialized is True
|
||||
|
||||
def test_health_check(self, provider):
|
||||
"""Test health check with real API."""
|
||||
result = provider.health_check()
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_get_supported_languages(self, provider):
|
||||
"""Test getting supported languages."""
|
||||
languages = provider.get_supported_languages()
|
||||
|
||||
assert "python" in languages
|
||||
assert "javascript" in languages
|
||||
assert isinstance(languages, list)
|
||||
|
||||
def test_create_python_instance(self, provider):
|
||||
"""Test creating a Python sandbox instance."""
|
||||
try:
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
assert instance.provider == "aliyun_codeinterpreter"
|
||||
assert instance.status in ["READY", "CREATING"]
|
||||
assert instance.metadata["language"] == "python"
|
||||
assert len(instance.instance_id) > 0
|
||||
|
||||
# Clean up
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"Instance creation failed: {str(e)}. API might not be available yet.")
|
||||
|
||||
def test_execute_python_code(self, provider):
|
||||
"""Test executing Python code in the sandbox."""
|
||||
try:
|
||||
# Create instance
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
# Execute simple code
|
||||
result = provider.execute_code(
|
||||
instance_id=instance.instance_id,
|
||||
code="print('Hello from Aliyun Code Interpreter!')\nprint(42)",
|
||||
language="python",
|
||||
timeout=30, # Max 30 seconds
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Hello from Aliyun Code Interpreter!" in result.stdout
|
||||
assert "42" in result.stdout
|
||||
assert result.execution_time > 0
|
||||
|
||||
# Clean up
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"Code execution test failed: {str(e)}. API might not be available yet.")
|
||||
|
||||
def test_execute_python_code_with_arguments(self, provider):
|
||||
"""Test executing Python code with arguments parameter."""
|
||||
try:
|
||||
# Create instance
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
# Execute code with arguments
|
||||
result = provider.execute_code(
|
||||
instance_id=instance.instance_id,
|
||||
code="""def main(name: str, count: int) -> dict:
|
||||
return {"message": f"Hello {name}!" * count}
|
||||
""",
|
||||
language="python",
|
||||
timeout=30,
|
||||
arguments={"name": "World", "count": 2}
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Hello World!Hello World!" in result.stdout
|
||||
|
||||
# Clean up
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"Arguments test failed: {str(e)}. API might not be available yet.")
|
||||
|
||||
def test_execute_python_code_with_error(self, provider):
|
||||
"""Test executing Python code that produces an error."""
|
||||
try:
|
||||
# Create instance
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
# Execute code with error
|
||||
result = provider.execute_code(instance_id=instance.instance_id, code="raise ValueError('Test error')", language="python", timeout=30)
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert len(result.stderr) > 0 or "ValueError" in result.stdout
|
||||
|
||||
# Clean up
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"Error handling test failed: {str(e)}. API might not be available yet.")
|
||||
|
||||
def test_execute_javascript_code(self, provider):
|
||||
"""Test executing JavaScript code in the sandbox."""
|
||||
try:
|
||||
# Create instance
|
||||
instance = provider.create_instance("javascript")
|
||||
|
||||
# Execute simple code
|
||||
result = provider.execute_code(instance_id=instance.instance_id, code="console.log('Hello from JavaScript!');", language="javascript", timeout=30)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Hello from JavaScript!" in result.stdout
|
||||
|
||||
# Clean up
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"JavaScript execution test failed: {str(e)}. API might not be available yet.")
|
||||
|
||||
def test_execute_javascript_code_with_arguments(self, provider):
|
||||
"""Test executing JavaScript code with arguments parameter."""
|
||||
try:
|
||||
# Create instance
|
||||
instance = provider.create_instance("javascript")
|
||||
|
||||
# Execute code with arguments
|
||||
result = provider.execute_code(
|
||||
instance_id=instance.instance_id,
|
||||
code="""function main(args) {
|
||||
const { name, count } = args;
|
||||
return `Hello ${name}!`.repeat(count);
|
||||
}""",
|
||||
language="javascript",
|
||||
timeout=30,
|
||||
arguments={"name": "World", "count": 2}
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Hello World!Hello World!" in result.stdout
|
||||
|
||||
# Clean up
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"JavaScript arguments test failed: {str(e)}. API might not be available yet.")
|
||||
|
||||
def test_destroy_instance(self, provider):
|
||||
"""Test destroying a sandbox instance."""
|
||||
try:
|
||||
# Create instance
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
# Destroy instance
|
||||
result = provider.destroy_instance(instance.instance_id)
|
||||
|
||||
# Note: The API might return True immediately or async
|
||||
assert result is True or result is False
|
||||
except Exception as e:
|
||||
pytest.skip(f"Destroy instance test failed: {str(e)}. API might not be available yet.")
|
||||
|
||||
def test_config_validation(self, provider):
|
||||
"""Test configuration validation."""
|
||||
# Valid config
|
||||
is_valid, error = provider.validate_config({"access_key_id": "LTAI5tXXXXXXXXXX", "account_id": "1234567890123456", "region": "cn-hangzhou", "timeout": 30})
|
||||
assert is_valid is True
|
||||
assert error is None
|
||||
|
||||
# Invalid access key
|
||||
is_valid, error = provider.validate_config({"access_key_id": "INVALID_KEY"})
|
||||
assert is_valid is False
|
||||
|
||||
# Missing account ID
|
||||
is_valid, error = provider.validate_config({})
|
||||
assert is_valid is False
|
||||
assert "Account ID" in error
|
||||
|
||||
def test_timeout_limit(self, provider):
|
||||
"""Test that timeout is limited to 30 seconds."""
|
||||
# Timeout > 30 should be clamped to 30
|
||||
provider2 = AliyunCodeInterpreterProvider()
|
||||
provider2.initialize(
|
||||
{
|
||||
"access_key_id": os.getenv("AGENTRUN_ACCESS_KEY_ID"),
|
||||
"access_key_secret": os.getenv("AGENTRUN_ACCESS_KEY_SECRET"),
|
||||
"account_id": os.getenv("AGENTRUN_ACCOUNT_ID"),
|
||||
"timeout": 60, # Request 60 seconds
|
||||
}
|
||||
)
|
||||
|
||||
# Should be clamped to 30
|
||||
assert provider2.timeout == 30
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAliyunCodeInterpreterScenarios:
|
||||
"""Test real-world usage scenarios."""
|
||||
|
||||
def test_data_processing_workflow(self, provider):
|
||||
"""Test a simple data processing workflow."""
|
||||
try:
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
# Execute data processing code
|
||||
code = """
|
||||
import json
|
||||
data = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
|
||||
result = json.dumps(data, indent=2)
|
||||
print(result)
|
||||
"""
|
||||
result = provider.execute_code(instance_id=instance.instance_id, code=code, language="python", timeout=30)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Alice" in result.stdout
|
||||
assert "Bob" in result.stdout
|
||||
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"Data processing test failed: {str(e)}")
|
||||
|
||||
def test_string_manipulation(self, provider):
|
||||
"""Test string manipulation operations."""
|
||||
try:
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
code = """
|
||||
text = "Hello, World!"
|
||||
print(text.upper())
|
||||
print(text.lower())
|
||||
print(text.replace("World", "Aliyun"))
|
||||
"""
|
||||
result = provider.execute_code(instance_id=instance.instance_id, code=code, language="python", timeout=30)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "HELLO, WORLD!" in result.stdout
|
||||
assert "hello, world!" in result.stdout
|
||||
assert "Hello, Aliyun!" in result.stdout
|
||||
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"String manipulation test failed: {str(e)}")
|
||||
|
||||
def test_context_persistence(self, provider):
|
||||
"""Test code execution with context persistence."""
|
||||
try:
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
# First execution - define variable
|
||||
result1 = provider.execute_code(instance_id=instance.instance_id, code="x = 42\nprint(x)", language="python", timeout=30)
|
||||
assert result1.exit_code == 0
|
||||
|
||||
# Second execution - use variable
|
||||
# Note: Context persistence depends on whether the contextId is reused
|
||||
result2 = provider.execute_code(instance_id=instance.instance_id, code="print(f'x is {x}')", language="python", timeout=30)
|
||||
|
||||
# Context might or might not persist depending on API implementation
|
||||
assert result2.exit_code == 0
|
||||
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
except Exception as e:
|
||||
pytest.skip(f"Context persistence test failed: {str(e)}")
|
||||
|
||||
|
||||
def test_without_credentials():
|
||||
"""Test that tests are skipped without credentials."""
|
||||
# This test should always run (not skipped)
|
||||
if all(
|
||||
[
|
||||
os.getenv("AGENTRUN_ACCESS_KEY_ID"),
|
||||
os.getenv("AGENTRUN_ACCESS_KEY_SECRET"),
|
||||
os.getenv("AGENTRUN_ACCOUNT_ID"),
|
||||
]
|
||||
):
|
||||
assert True # Credentials are set
|
||||
else:
|
||||
assert True # Credentials not set, test still passes
|
||||
423
agent/sandbox/tests/test_providers.py
Normal file
423
agent/sandbox/tests/test_providers.py
Normal file
@ -0,0 +1,423 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Unit tests for sandbox provider abstraction layer.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
import requests
|
||||
|
||||
from agent.sandbox.providers.base import SandboxProvider, SandboxInstance, ExecutionResult
|
||||
from agent.sandbox.providers.manager import ProviderManager
|
||||
from agent.sandbox.providers.self_managed import SelfManagedProvider
|
||||
|
||||
|
||||
class TestSandboxDataclasses:
|
||||
"""Test sandbox dataclasses."""
|
||||
|
||||
def test_sandbox_instance_creation(self):
|
||||
"""Test SandboxInstance dataclass creation."""
|
||||
instance = SandboxInstance(
|
||||
instance_id="test-123",
|
||||
provider="self_managed",
|
||||
status="running",
|
||||
metadata={"language": "python"}
|
||||
)
|
||||
|
||||
assert instance.instance_id == "test-123"
|
||||
assert instance.provider == "self_managed"
|
||||
assert instance.status == "running"
|
||||
assert instance.metadata == {"language": "python"}
|
||||
|
||||
def test_sandbox_instance_default_metadata(self):
|
||||
"""Test SandboxInstance with None metadata."""
|
||||
instance = SandboxInstance(
|
||||
instance_id="test-123",
|
||||
provider="self_managed",
|
||||
status="running",
|
||||
metadata=None
|
||||
)
|
||||
|
||||
assert instance.metadata == {}
|
||||
|
||||
def test_execution_result_creation(self):
|
||||
"""Test ExecutionResult dataclass creation."""
|
||||
result = ExecutionResult(
|
||||
stdout="Hello, World!",
|
||||
stderr="",
|
||||
exit_code=0,
|
||||
execution_time=1.5,
|
||||
metadata={"status": "success"}
|
||||
)
|
||||
|
||||
assert result.stdout == "Hello, World!"
|
||||
assert result.stderr == ""
|
||||
assert result.exit_code == 0
|
||||
assert result.execution_time == 1.5
|
||||
assert result.metadata == {"status": "success"}
|
||||
|
||||
def test_execution_result_default_metadata(self):
|
||||
"""Test ExecutionResult with None metadata."""
|
||||
result = ExecutionResult(
|
||||
stdout="output",
|
||||
stderr="error",
|
||||
exit_code=1,
|
||||
execution_time=0.5,
|
||||
metadata=None
|
||||
)
|
||||
|
||||
assert result.metadata == {}
|
||||
|
||||
|
||||
class TestProviderManager:
|
||||
"""Test ProviderManager functionality."""
|
||||
|
||||
def test_manager_initialization(self):
|
||||
"""Test ProviderManager initialization."""
|
||||
manager = ProviderManager()
|
||||
|
||||
assert manager.current_provider is None
|
||||
assert manager.current_provider_name is None
|
||||
assert not manager.is_configured()
|
||||
|
||||
def test_set_provider(self):
|
||||
"""Test setting a provider."""
|
||||
manager = ProviderManager()
|
||||
mock_provider = Mock(spec=SandboxProvider)
|
||||
|
||||
manager.set_provider("self_managed", mock_provider)
|
||||
|
||||
assert manager.current_provider == mock_provider
|
||||
assert manager.current_provider_name == "self_managed"
|
||||
assert manager.is_configured()
|
||||
|
||||
def test_get_provider(self):
|
||||
"""Test getting the current provider."""
|
||||
manager = ProviderManager()
|
||||
mock_provider = Mock(spec=SandboxProvider)
|
||||
|
||||
manager.set_provider("self_managed", mock_provider)
|
||||
|
||||
assert manager.get_provider() == mock_provider
|
||||
|
||||
def test_get_provider_name(self):
|
||||
"""Test getting the current provider name."""
|
||||
manager = ProviderManager()
|
||||
mock_provider = Mock(spec=SandboxProvider)
|
||||
|
||||
manager.set_provider("self_managed", mock_provider)
|
||||
|
||||
assert manager.get_provider_name() == "self_managed"
|
||||
|
||||
def test_get_provider_when_not_set(self):
|
||||
"""Test getting provider when none is set."""
|
||||
manager = ProviderManager()
|
||||
|
||||
assert manager.get_provider() is None
|
||||
assert manager.get_provider_name() is None
|
||||
|
||||
|
||||
class TestSelfManagedProvider:
|
||||
"""Test SelfManagedProvider implementation."""
|
||||
|
||||
def test_provider_initialization(self):
|
||||
"""Test provider initialization."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
assert provider.endpoint == "http://localhost:9385"
|
||||
assert provider.timeout == 30
|
||||
assert provider.max_retries == 3
|
||||
assert provider.pool_size == 10
|
||||
assert not provider._initialized
|
||||
|
||||
@patch('requests.get')
|
||||
def test_initialize_success(self, mock_get):
|
||||
"""Test successful initialization."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
result = provider.initialize({
|
||||
"endpoint": "http://test-endpoint:9385",
|
||||
"timeout": 60,
|
||||
"max_retries": 5,
|
||||
"pool_size": 20
|
||||
})
|
||||
|
||||
assert result is True
|
||||
assert provider.endpoint == "http://test-endpoint:9385"
|
||||
assert provider.timeout == 60
|
||||
assert provider.max_retries == 5
|
||||
assert provider.pool_size == 20
|
||||
assert provider._initialized
|
||||
mock_get.assert_called_once_with("http://test-endpoint:9385/healthz", timeout=5)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_initialize_failure(self, mock_get):
|
||||
"""Test initialization failure."""
|
||||
mock_get.side_effect = Exception("Connection error")
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
result = provider.initialize({"endpoint": "http://invalid:9385"})
|
||||
|
||||
assert result is False
|
||||
assert not provider._initialized
|
||||
|
||||
def test_initialize_default_config(self):
|
||||
"""Test initialization with default config."""
|
||||
with patch('requests.get') as mock_get:
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
result = provider.initialize({})
|
||||
|
||||
assert result is True
|
||||
assert provider.endpoint == "http://localhost:9385"
|
||||
assert provider.timeout == 30
|
||||
|
||||
def test_create_instance_python(self):
|
||||
"""Test creating a Python instance."""
|
||||
provider = SelfManagedProvider()
|
||||
provider._initialized = True
|
||||
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
assert instance.provider == "self_managed"
|
||||
assert instance.status == "running"
|
||||
assert instance.metadata["language"] == "python"
|
||||
assert instance.metadata["endpoint"] == "http://localhost:9385"
|
||||
assert len(instance.instance_id) > 0 # Verify instance_id exists
|
||||
|
||||
def test_create_instance_nodejs(self):
|
||||
"""Test creating a Node.js instance."""
|
||||
provider = SelfManagedProvider()
|
||||
provider._initialized = True
|
||||
|
||||
instance = provider.create_instance("nodejs")
|
||||
|
||||
assert instance.metadata["language"] == "nodejs"
|
||||
|
||||
def test_create_instance_not_initialized(self):
|
||||
"""Test creating instance when provider not initialized."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
with pytest.raises(RuntimeError, match="Provider not initialized"):
|
||||
provider.create_instance("python")
|
||||
|
||||
@patch('requests.post')
|
||||
def test_execute_code_success(self, mock_post):
|
||||
"""Test successful code execution."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"status": "success",
|
||||
"stdout": '{"result": 42}',
|
||||
"stderr": "",
|
||||
"exit_code": 0,
|
||||
"time_used_ms": 100.0,
|
||||
"memory_used_kb": 1024.0
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
provider._initialized = True
|
||||
|
||||
result = provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="def main(): return {'result': 42}",
|
||||
language="python",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
assert result.stdout == '{"result": 42}'
|
||||
assert result.stderr == ""
|
||||
assert result.exit_code == 0
|
||||
assert result.execution_time > 0
|
||||
assert result.metadata["status"] == "success"
|
||||
assert result.metadata["instance_id"] == "test-123"
|
||||
|
||||
@patch('requests.post')
|
||||
def test_execute_code_timeout(self, mock_post):
|
||||
"""Test code execution timeout."""
|
||||
mock_post.side_effect = requests.Timeout()
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
provider._initialized = True
|
||||
|
||||
with pytest.raises(TimeoutError, match="Execution timed out"):
|
||||
provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="while True: pass",
|
||||
language="python",
|
||||
timeout=5
|
||||
)
|
||||
|
||||
@patch('requests.post')
|
||||
def test_execute_code_http_error(self, mock_post):
|
||||
"""Test code execution with HTTP error."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 500
|
||||
mock_response.text = "Internal Server Error"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
provider._initialized = True
|
||||
|
||||
with pytest.raises(RuntimeError, match="HTTP 500"):
|
||||
provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="invalid code",
|
||||
language="python"
|
||||
)
|
||||
|
||||
def test_execute_code_not_initialized(self):
|
||||
"""Test executing code when provider not initialized."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
with pytest.raises(RuntimeError, match="Provider not initialized"):
|
||||
provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="print('hello')",
|
||||
language="python"
|
||||
)
|
||||
|
||||
def test_destroy_instance(self):
|
||||
"""Test destroying an instance (no-op for self-managed)."""
|
||||
provider = SelfManagedProvider()
|
||||
provider._initialized = True
|
||||
|
||||
# For self-managed, destroy_instance is a no-op
|
||||
result = provider.destroy_instance("test-123")
|
||||
|
||||
assert result is True
|
||||
|
||||
@patch('requests.get')
|
||||
def test_health_check_success(self, mock_get):
|
||||
"""Test successful health check."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
result = provider.health_check()
|
||||
|
||||
assert result is True
|
||||
mock_get.assert_called_once_with("http://localhost:9385/healthz", timeout=5)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_health_check_failure(self, mock_get):
|
||||
"""Test health check failure."""
|
||||
mock_get.side_effect = Exception("Connection error")
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
result = provider.health_check()
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_get_supported_languages(self):
|
||||
"""Test getting supported languages."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
languages = provider.get_supported_languages()
|
||||
|
||||
assert "python" in languages
|
||||
assert "nodejs" in languages
|
||||
assert "javascript" in languages
|
||||
|
||||
def test_get_config_schema(self):
|
||||
"""Test getting configuration schema."""
|
||||
schema = SelfManagedProvider.get_config_schema()
|
||||
|
||||
assert "endpoint" in schema
|
||||
assert schema["endpoint"]["type"] == "string"
|
||||
assert schema["endpoint"]["required"] is True
|
||||
assert schema["endpoint"]["default"] == "http://localhost:9385"
|
||||
|
||||
assert "timeout" in schema
|
||||
assert schema["timeout"]["type"] == "integer"
|
||||
assert schema["timeout"]["default"] == 30
|
||||
|
||||
assert "max_retries" in schema
|
||||
assert schema["max_retries"]["type"] == "integer"
|
||||
|
||||
assert "pool_size" in schema
|
||||
assert schema["pool_size"]["type"] == "integer"
|
||||
|
||||
def test_normalize_language_python(self):
|
||||
"""Test normalizing Python language identifier."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
assert provider._normalize_language("python") == "python"
|
||||
assert provider._normalize_language("python3") == "python"
|
||||
assert provider._normalize_language("PYTHON") == "python"
|
||||
assert provider._normalize_language("Python3") == "python"
|
||||
|
||||
def test_normalize_language_javascript(self):
|
||||
"""Test normalizing JavaScript language identifier."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
assert provider._normalize_language("javascript") == "nodejs"
|
||||
assert provider._normalize_language("nodejs") == "nodejs"
|
||||
assert provider._normalize_language("JavaScript") == "nodejs"
|
||||
assert provider._normalize_language("NodeJS") == "nodejs"
|
||||
|
||||
def test_normalize_language_default(self):
|
||||
"""Test language normalization with empty/unknown input."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
assert provider._normalize_language("") == "python"
|
||||
assert provider._normalize_language(None) == "python"
|
||||
assert provider._normalize_language("unknown") == "unknown"
|
||||
|
||||
|
||||
class TestProviderInterface:
|
||||
"""Test that providers correctly implement the interface."""
|
||||
|
||||
def test_self_managed_provider_is_abstract(self):
|
||||
"""Test that SelfManagedProvider is a SandboxProvider."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
assert isinstance(provider, SandboxProvider)
|
||||
|
||||
def test_self_managed_provider_has_abstract_methods(self):
|
||||
"""Test that SelfManagedProvider implements all abstract methods."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
# Check all abstract methods are implemented
|
||||
assert hasattr(provider, 'initialize')
|
||||
assert callable(provider.initialize)
|
||||
|
||||
assert hasattr(provider, 'create_instance')
|
||||
assert callable(provider.create_instance)
|
||||
|
||||
assert hasattr(provider, 'execute_code')
|
||||
assert callable(provider.execute_code)
|
||||
|
||||
assert hasattr(provider, 'destroy_instance')
|
||||
assert callable(provider.destroy_instance)
|
||||
|
||||
assert hasattr(provider, 'health_check')
|
||||
assert callable(provider.health_check)
|
||||
|
||||
assert hasattr(provider, 'get_supported_languages')
|
||||
assert callable(provider.get_supported_languages)
|
||||
78
agent/sandbox/tests/verify_sdk.py
Normal file
78
agent/sandbox/tests/verify_sdk.py
Normal file
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick verification script for Aliyun Code Interpreter provider using official SDK.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
print("=" * 60)
|
||||
print("Aliyun Code Interpreter Provider - SDK Verification")
|
||||
print("=" * 60)
|
||||
|
||||
# Test 1: Import provider
|
||||
print("\n[1/5] Testing provider import...")
|
||||
try:
|
||||
from agent.sandbox.providers.aliyun_codeinterpreter import AliyunCodeInterpreterProvider
|
||||
|
||||
print("✓ Provider imported successfully")
|
||||
except ImportError as e:
|
||||
print(f"✗ Import failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 2: Check provider class
|
||||
print("\n[2/5] Testing provider class...")
|
||||
provider = AliyunCodeInterpreterProvider()
|
||||
assert hasattr(provider, "initialize")
|
||||
assert hasattr(provider, "create_instance")
|
||||
assert hasattr(provider, "execute_code")
|
||||
assert hasattr(provider, "destroy_instance")
|
||||
assert hasattr(provider, "health_check")
|
||||
print("✓ Provider has all required methods")
|
||||
|
||||
# Test 3: Check SDK imports
|
||||
print("\n[3/5] Testing SDK imports...")
|
||||
try:
|
||||
# Check if agentrun SDK is available using importlib
|
||||
if (
|
||||
importlib.util.find_spec("agentrun.sandbox") is None
|
||||
or importlib.util.find_spec("agentrun.utils.config") is None
|
||||
or importlib.util.find_spec("agentrun.utils.exception") is None
|
||||
):
|
||||
raise ImportError("agentrun SDK not found")
|
||||
|
||||
# Verify imports work (assign to _ to indicate they're intentionally unused)
|
||||
from agentrun.sandbox import CodeInterpreterSandbox, TemplateType, CodeLanguage
|
||||
from agentrun.utils.config import Config
|
||||
from agentrun.utils.exception import ServerError
|
||||
_ = (CodeInterpreterSandbox, TemplateType, CodeLanguage, Config, ServerError)
|
||||
|
||||
print("✓ SDK modules imported successfully")
|
||||
except ImportError as e:
|
||||
print(f"✗ SDK import failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 4: Check config schema
|
||||
print("\n[4/5] Testing configuration schema...")
|
||||
schema = AliyunCodeInterpreterProvider.get_config_schema()
|
||||
required_fields = ["access_key_id", "access_key_secret", "account_id"]
|
||||
for field in required_fields:
|
||||
assert field in schema
|
||||
assert schema[field]["required"] is True
|
||||
print(f"✓ All required fields present: {', '.join(required_fields)}")
|
||||
|
||||
# Test 5: Check supported languages
|
||||
print("\n[5/5] Testing supported languages...")
|
||||
languages = provider.get_supported_languages()
|
||||
assert "python" in languages
|
||||
assert "javascript" in languages
|
||||
print(f"✓ Supported languages: {', '.join(languages)}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All verification tests passed! ✓")
|
||||
print("=" * 60)
|
||||
print("\nNote: This provider now uses the official agentrun-sdk.")
|
||||
print("SDK Documentation: https://github.com/Serverless-Devs/agentrun-sdk-python")
|
||||
print("API Documentation: https://help.aliyun.com/zh/functioncompute/fc/sandbox-sandbox-code-interepreter")
|
||||
539
agent/sandbox/uv.lock
generated
Normal file
539
agent/sandbox/uv.lock
generated
Normal file
@ -0,0 +1,539 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basedpyright"
|
||||
version = "1.29.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodejs-wheel-binaries" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/18/f5e488eac4960ad9a2e71b95f0d91cf93a982c7f68aa90e4e0554f0bc37e/basedpyright-1.29.1.tar.gz", hash = "sha256:06bbe6c3b50ab4af20f80e154049477a50d8b81d2522eadbc9f472f2f92cd44b", size = 21773469, upload-time = "2025-04-23T13:29:42.47Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/1b/1bb837bbb7e259928f33d3c105dfef4f5349ef08b3ef45576801256e3234/basedpyright-1.29.1-py3-none-any.whl", hash = "sha256:b7eb65b9d4aaeeea29a349ac494252032a75a364942d0ac466d7f07ddeacc786", size = 11397959, upload-time = "2025-04-23T13:29:38.106Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deprecated"
|
||||
version = "1.2.18"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.12"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gvisor-sandbox"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "requests" },
|
||||
{ name = "slowapi" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "basedpyright" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "fastapi", specifier = ">=0.115.12" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "pydantic", specifier = ">=2.11.4" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "slowapi", specifier = ">=0.1.9" },
|
||||
{ name = "uvicorn", specifier = ">=0.34.2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "basedpyright", specifier = ">=1.29.1" }]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "limits"
|
||||
version = "5.1.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "deprecated" },
|
||||
{ name = "packaging" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/94/a04e64f487a56f97aff67c53df609cc19d5c3f3e7e5697ec8a1ff8413829/limits-5.1.0.tar.gz", hash = "sha256:b298e4af0b47997da03cbeee9df027ddc2328f8630546125e81083bb56311827", size = 94655, upload-time = "2025-04-23T18:59:44.457Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/00/876a5ec60addda62ee13ac4b588a5afc0d1a86a431645a91711ceae834cf/limits-5.1.0-py3-none-any.whl", hash = "sha256:f368d4572ac3ef8190cb8b9911ed481175a0b4189894a63cac95cae39ebeb147", size = 60472, upload-time = "2025-04-23T18:59:42.266Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodejs-wheel-binaries"
|
||||
version = "22.15.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/5b/6c5f973765b96793d4e4d03684bcbd273b17e471ecc7e9bec4c32b595ebd/nodejs_wheel_binaries-22.15.0.tar.gz", hash = "sha256:ff81aa2a79db279c2266686ebcb829b6634d049a5a49fc7dc6921e4f18af9703", size = 8054, upload-time = "2025-04-23T16:57:40.338Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/a8/a32e5bb99e95c536e7dac781cffab1e7e9f8661b8ee296b93df77e4df7f9/nodejs_wheel_binaries-22.15.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:aa16366d48487fff89446fb237693e777aa2ecd987208db7d4e35acc40c3e1b1", size = 50514526, upload-time = "2025-04-23T16:57:07.478Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/e8/eb024dbb3a7d3b98c8922d1c306be989befad4d2132292954cb902f43b07/nodejs_wheel_binaries-22.15.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:a54bb3fee9170003fa8abc69572d819b2b1540344eff78505fcc2129a9175596", size = 51409179, upload-time = "2025-04-23T16:57:11.599Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/0f/baa968456c3577e45c7d0e3715258bd175dcecc67b683a41a5044d5dae40/nodejs_wheel_binaries-22.15.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:867121ccf99d10523f6878a26db86e162c4939690e24cfb5bea56d01ea696c93", size = 57364460, upload-time = "2025-04-23T16:57:15.725Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/a2/977f63cd07ed8fc27bc0d0cd72e801fc3691ffc8cd40a51496ff18a6d0a2/nodejs_wheel_binaries-22.15.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab0fbcda2ddc8aab7db1505d72cb958f99324b3834c4543541a305e02bfe860", size = 57889101, upload-time = "2025-04-23T16:57:19.643Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/7f/57b9c24a4f0d25490527b043146aa0fdff2d8fdc82f90667cdaf6f00cfc9/nodejs_wheel_binaries-22.15.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2bde1d8e00cd955b9ce9ee9ac08309923e2778a790ee791b715e93e487e74bfd", size = 59190817, upload-time = "2025-04-23T16:57:23.875Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/7f/970acbe33b81c22b3c7928f52e32347030aa46d23d779cf781cf9a9cf557/nodejs_wheel_binaries-22.15.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:acdd4ef73b6701aab9fbe02ac5e104f208a5e3c300402fa41ad7bc7f49499fbf", size = 60220316, upload-time = "2025-04-23T16:57:28.276Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/4c/030243c04bb60f0de66c2d7ee3be289c6d28ef09113c06ffa417bdfedf8f/nodejs_wheel_binaries-22.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:51deaf13ee474e39684ce8c066dfe86240edb94e7241950ca789befbbbcbd23d", size = 40718853, upload-time = "2025-04-23T16:57:32.651Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/49/011d472814af4fabeaab7d7ce3d5a1a635a3dadc23ae404d1f546839ecb3/nodejs_wheel_binaries-22.15.0-py2.py3-none-win_arm64.whl", hash = "sha256:01a3fe4d60477f93bf21a44219db33548c75d7fed6dc6e6f4c05cf0adf015609", size = 36436645, upload-time = "2025-04-23T16:57:36.326Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.4"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slowapi"
|
||||
version = "0.1.9"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "limits" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.46.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "1.17.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" },
|
||||
]
|
||||
@ -149,6 +149,39 @@ class CodeExec(ToolBase, ABC):
|
||||
return
|
||||
|
||||
try:
|
||||
# Try using the new sandbox provider system first
|
||||
try:
|
||||
from agent.sandbox.client import execute_code as sandbox_execute_code
|
||||
|
||||
if self.check_if_canceled("CodeExec execution"):
|
||||
return
|
||||
|
||||
# Execute code using the provider system
|
||||
result = sandbox_execute_code(
|
||||
code=code,
|
||||
language=language,
|
||||
timeout=int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)),
|
||||
arguments=arguments
|
||||
)
|
||||
|
||||
if self.check_if_canceled("CodeExec execution"):
|
||||
return
|
||||
|
||||
# Process the result
|
||||
if result.stderr:
|
||||
self.set_output("_ERROR", result.stderr)
|
||||
return
|
||||
|
||||
parsed_stdout = self._deserialize_stdout(result.stdout)
|
||||
logging.info(f"[CodeExec]: Provider system -> {parsed_stdout}")
|
||||
self._populate_outputs(parsed_stdout, result.stdout)
|
||||
return
|
||||
|
||||
except (ImportError, RuntimeError) as provider_error:
|
||||
# Provider system not available or not configured, fall back to HTTP
|
||||
logging.info(f"[CodeExec]: Provider system not available, using HTTP fallback: {provider_error}")
|
||||
|
||||
# Fallback to direct HTTP request
|
||||
code_b64 = self._encode_code(code)
|
||||
code_req = CodeExecutionRequest(code_b64=code_b64, language=language, arguments=arguments).model_dump()
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user