diff --git a/slop-terminal/.gitignore b/slop-terminal/.gitignore deleted file mode 100644 index cdac1bf..0000000 --- a/slop-terminal/.gitignore +++ /dev/null @@ -1,54 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# Virtual environments -venv/ -env/ -ENV/ - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Project specific - data and generated files -data/ -!data/.gitkeep -!data/README.md -bin/ -firecracker/ -downloads/ -logs/ -*.log - -# Generated Scripts directory (created by vars.sh) -Scripts/ - -# Environment files -.env \ No newline at end of file diff --git a/slop-terminal/.gitkeep b/slop-terminal/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/slop-terminal/.gitlab.ci.yml b/slop-terminal/.gitlab.ci.yml deleted file mode 100644 index e0b3b66..0000000 --- a/slop-terminal/.gitlab.ci.yml +++ /dev/null @@ -1,72 +0,0 @@ -stages: - - test - - build - - deploy - -variables: - DOCKER_IMAGE_NAME: "slop-terminal" - DOCKER_TAG: "$CI_COMMIT_REF_SLUG" - -# Test stage -test: - stage: test - image: python:3.9 - before_script: - - pip install -r requirements.txt - - pip install pytest pytest-cov - script: - - python -m pytest tests/ --cov=. - coverage: '/TOTAL.+?(\d+\%)$/' - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - -# Build Docker image -build: - stage: build - image: docker:20.10.16 - services: - - docker:20.10.16-dind - before_script: - - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - script: - - docker build -t $CI_REGISTRY_IMAGE:$DOCKER_TAG . - - docker push $CI_REGISTRY_IMAGE:$DOCKER_TAG - - docker tag $CI_REGISTRY_IMAGE:$DOCKER_TAG $CI_REGISTRY_IMAGE:latest - - docker push $CI_REGISTRY_IMAGE:latest - only: - - main - - develop - -# Deploy to staging -deploy_staging: - stage: deploy - image: alpine:latest - before_script: - - apk add --no-cache curl - script: - - echo "Deploying to staging environment..." - - curl -X POST "$STAGING_WEBHOOK_URL" -H "Content-Type: application/json" -d '{"image":"'$CI_REGISTRY_IMAGE:$DOCKER_TAG'"}' - only: - - develop - environment: - name: staging - url: https://staging.your-domain.com - -# Deploy to production -deploy_production: - stage: deploy - image: alpine:latest - before_script: - - apk add --no-cache curl - script: - - echo "Deploying to production environment..." - - curl -X POST "$PRODUCTION_WEBHOOK_URL" -H "Content-Type: application/json" -d '{"image":"'$CI_REGISTRY_IMAGE:$DOCKER_TAG'"}' - only: - - main - environment: - name: production - url: https://your-domain.com - when: manual \ No newline at end of file diff --git a/slop-terminal/Dockerfile b/slop-terminal/Dockerfile deleted file mode 100644 index 5d02807..0000000 --- a/slop-terminal/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM ubuntu:20.04 - -# Avoid interactive prompts -ENV DEBIAN_FRONTEND=noninteractive - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - python3 \ - python3-pip \ - curl \ - wget \ - tar \ - gzip \ - qemu-kvm \ - && rm -rf /var/lib/apt/lists/* - -# Create app directory -WORKDIR /app - -# Copy requirements first for better caching -COPY requirements.txt . -RUN pip3 install --no-cache-dir -r requirements.txt - -# Copy application files -COPY slop_with_terminal.py . -COPY vars.sh . -COPY static/ ./static/ -COPY config/ ./config/ - -# Create necessary directories -RUN mkdir -p data bin firecracker downloads Scripts - -# Make scripts executable -RUN chmod +x vars.sh - -# Set up environment -ENV SLOP_DATA_DIR=/app/data - -# Expose port -EXPOSE 31337 - -# Run setup and start server -CMD ["bash", "-c", "./vars.sh && python3 slop_with_terminal.py"] \ No newline at end of file diff --git a/slop-terminal/Makefile b/slop-terminal/Makefile deleted file mode 100644 index cd0c939..0000000 --- a/slop-terminal/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -.PHONY: help setup install test run clean docker-build docker-run - -help: - @echo "Available commands:" - @echo " setup - Run initial setup (vars.sh)" - @echo " install - Install Python dependencies" - @echo " test - Run tests" - @echo " run - Start the server (slop_with_terminal.py)" - @echo " clean - Clean up generated files" - @echo " docker-build - Build Docker image" - @echo " docker-run - Run with Docker Compose" - -setup: - @echo "Setting up SLOP Terminal..." - chmod +x vars.sh - ./vars.sh - -install: - pip3 install -r requirements.txt - -test: - python3 -m pytest tests/ -v - -run: - python3 slop_with_terminal.py - -clean: - rm -rf __pycache__ .pytest_cache - rm -rf data/ - rm -rf bin/ - rm -rf firecracker/ - rm -rf downloads/ - rm -rf Scripts/ - -docker-build: - docker build -t slop-terminal . - -docker-run: - docker-compose up -d \ No newline at end of file diff --git a/slop-terminal/README.md b/slop-terminal/README.md deleted file mode 100644 index 1f8b77f..0000000 --- a/slop-terminal/README.md +++ /dev/null @@ -1,388 +0,0 @@ -# SLOP Terminal Server - -A comprehensive SLOP (Streamlined Language Operations Protocol) server implementation with AI model integration, sandboxed code execution, and gaming capabilities. - -## Features - -### 🤖 AI Integration -- **Multi-Model Support**: Dynamic discovery of OpenAI-compatible endpoints (vLLM, Ollama, etc.) -- **Tool Calling**: AI can automatically invoke tools to assist with tasks -- **Chat Interface**: Full conversation support with context management - -### 🛡️ Security & Sandboxing -- **Firecracker Sandboxes**: Secure microVM-based code execution -- **User Isolation**: Per-user sandbox environments with persistent state -- **Permission Controls**: Configurable security policies and access controls - -### 🔧 Terminal Tools -- **File Operations**: Read, write, and manage files safely -- **Code Execution**: Python and shell command execution in sandboxes -- **System Information**: Access to system stats and environment details -- **Directory Management**: Navigate and explore filesystem - -### 🎮 Gaming Features -- **Casino Games**: Slots and Blackjack with persistent wallets -- **Agent Support**: Multi-agent gaming with individual statistics -- **Persistent State**: Game history and balances stored across sessions - -### 💾 Data Management -- **Memory System**: Key-value storage with query capabilities -- **Resource Management**: Hierarchical resource storage and retrieval -- **Persistent Storage**: File-based data persistence with locking - -## Quick Start - -### Prerequisites - -- **Linux/WSL2** (Ubuntu 18.04+ recommended) -- **Python 3.8+** -- **KVM support** (for Firecracker sandboxes) -- **Root/sudo access** (for initial setup) - -### Installation - -1. **Clone the repository**: - ```bash - git clone - cd slop-terminal - ``` - -2. **Run the setup**: - ```bash - make setup - ``` - - This will automatically: - - Install system dependencies - - Download and configure Firecracker - - Set up KVM permissions - - Install Python packages - - Configure environment variables - -3. **Test the installation**: - ```bash - ./Scripts/test_firecracker.sh # Auto-generated by setup - ``` - -4. **Start the server**: - ```bash - make run - ``` - -## Configuration - -### Environment Variables - -The server supports extensive configuration through environment variables. Copy the template and customize: - -```bash -cp config/.env.example .env -nano .env -``` - -#### Model Endpoints -```bash -export MODEL_ENDPOINT_0="https://your-model-endpoint.com/v1" -export MODEL_API_KEY_0="your-api-key-or-not-needed" -# Add more endpoints with incrementing numbers -``` - -#### Server Settings -```bash -export SLOP_PORT="31337" # Server port -export SLOP_DATA_DIR="./data" # Data directory -export SLOP_LOG_LEVEL="INFO" # Logging level -export FLASK_DEBUG="true" # Debug mode -``` - -#### Security Settings -```bash -export ENABLE_FIRECRACKER="true" # Enable sandbox -export ALLOW_DANGEROUS_TOOLS="false" # Allow unsafe tools -export REQUIRE_USER_ID="true" # Require user identification -export DEFAULT_TIMEOUT="60" # Default execution timeout -``` - -#### Firecracker Settings -```bash -export FIRECRACKER_KERNEL="./firecracker/kernel/vmlinux.bin" -export FIRECRACKER_ROOTFS="./firecracker/rootfs/rootfs.ext4" -export FIRECRACKER_MEMORY="512" # Memory in MB -export FIRECRACKER_VCPU="1" # Virtual CPU count -``` - -## API Endpoints - -### Chat & AI -- `POST /chat` - Chat with AI models with tool calling support -- `GET /models` - List available AI models - -### Tools -- `GET /tools` - List all available tools -- `GET /tools/{tool_id}` - Get tool details -- `POST /tools/{tool_id}` - Execute a specific tool - -### Memory Management -- `GET /memory` - List all memory keys -- `GET /memory/{key}` - Retrieve memory value -- `POST /memory` - Store key-value pair -- `DELETE /memory/{key}` - Delete memory key -- `POST /memory/query` - Query memory with filters - -### Resource Management -- `GET /resources` - List all resources -- `GET /resources/{resource_id}` - Get specific resource -- `POST /resources` - Create new resource -- `PUT /resources/{resource_id}` - Update resource -- `DELETE /resources/{resource_id}` - Delete resource -- `GET /resources/search` - Search resources - -### Gaming -- `POST /tools/slots` - Play slot machine -- `POST /tools/blackjack` - Play blackjack - -### System -- `GET /info` - Server information and capabilities -- `POST /pay` - Mock payment processing - -## Available Tools - -### Terminal Tools -- **`sandbox_python`** - Execute Python code in secure sandbox -- **`sandbox_exec`** - Execute shell commands in secure sandbox -- **`file_read`** - Read file contents safely -- **`directory_list`** - List directory contents -- **`sysinfo`** - Get system information -- **`pwd`** - Get current directory -- **`sandbox_manage`** - Manage sandbox instances - -### Utility Tools -- **`calculator`** - Basic math calculations -- **`greet`** - Simple greeting tool - -### Gaming Tools -- **`slots`** - Slot machine game -- **`blackjack`** - Blackjack card game - -## Security Features - -### Firecracker Sandboxes -- **Isolated Execution**: Each user gets their own microVM -- **Resource Limits**: Configurable CPU and memory limits -- **Network Isolation**: No network access from sandboxes -- **Persistent Environments**: Python virtual environments persist across calls - -### Access Controls -- **User Identification**: Optional user ID requirement -- **Tool Restrictions**: Dangerous tools only available in sandboxes -- **Timeout Controls**: Configurable execution timeouts -- **Permission Levels**: Different access levels for different tools - -## Usage Examples - -### Chat with AI -```bash -curl -X POST http://localhost:31337/chat \ - -H "Content-Type: application/json" \ - -d '{ - "messages": [{"role": "user", "content": "Calculate 15 * 23"}], - "model": "your-model-name", - "enable_tool_calling": true, - "user_id": "user123" - }' -``` - -### Execute Python Code -```bash -curl -X POST http://localhost:31337/tools/sandbox_python \ - -H "Content-Type: application/json" \ - -d '{ - "code": "print(\"Hello, World!\")", - "user_id": "user123", - "packages": ["requests", "numpy"] - }' -``` - -### Store and Retrieve Memory -```bash -# Store -curl -X POST http://localhost:31337/memory \ - -H "Content-Type: application/json" \ - -d '{"key": "user_pref", "value": "dark_mode"}' - -# Retrieve -curl http://localhost:31337/memory/user_pref -``` - -### Play Casino Games -```bash -curl -X POST http://localhost:31337/tools/slots \ - -H "Content-Type: application/json" \ - -d '{"bet": 10, "agent_id": "player1"}' -``` - -## Development - -### Available Make Commands - -```bash -make help # Show all available commands -make setup # Run vars.sh setup -make install # Install Python dependencies -make run # Start the server (python3 slop_with_terminal.py) -make test # Run tests -make clean # Clean up generated files -make docker-build # Build Docker image -make docker-run # Run with Docker Compose -``` - -### Project Structure -``` -slop-terminal/ -├── slop_with_terminal.py # Main Flask server -├── vars.sh # Setup and configuration script -├── README.md # This file -├── requirements.txt # Python dependencies -├── Makefile # Build automation -├── Dockerfile # Container configuration -├── docker-compose.yml # Docker setup -├── .gitlab-ci.yml # CI/CD pipeline -├── .gitignore # Git ignore rules -├── config/ # Configuration files -│ └── .env.example # Environment template -├── docs/ # Documentation -├── tests/ # Test files -├── static/ # Static files -├── Scripts/ # Generated scripts (auto-generated by vars.sh) -│ ├── set_env.sh # Environment setup -│ ├── test_firecracker.sh # Test script -│ └── start_firecracker.sh # Firecracker wrapper -├── data/ # Data directory -│ ├── memory.json # Memory storage -│ ├── resources.json # Resource storage -│ ├── terminal/ # Terminal data -│ └── slop_monolith.log # Server logs -├── firecracker/ # Firecracker components -│ ├── kernel/ # Kernel images -│ └── rootfs/ # Root filesystems -├── downloads/ # Downloaded files -└── bin/ # Binary files -``` - -### Adding New Tools -1. Create a class inheriting from `TerminalTool` -2. Implement required methods: `tool_id`, `description`, `arguments`, `execute` -3. Register the tool in `initialize_terminal_tools()` - -### Adding New Models -Simply add new environment variables: -```bash -export MODEL_ENDPOINT_X="https://your-endpoint.com/v1" -export MODEL_API_KEY_X="your-key" -``` - -## Troubleshooting - -### Common Issues - -**KVM Permission Errors**: -```bash -# Fix KVM permissions -sudo usermod -a -G kvm $USER -# Restart terminal or re-login -``` - -**Firecracker Not Working**: -```bash -# Test Firecracker setup -./Scripts/test_firecracker.sh # Auto-generated by setup - -# Check KVM availability -ls -la /dev/kvm -``` - -**Model Connection Issues**: -```bash -# Test endpoint manually -curl https://your-endpoint.com/v1/models -``` - -**Memory/Resource Lock Timeouts**: -- Check disk space in data directory -- Ensure proper file permissions -- Restart server if persistent - -### Debug Mode -Enable detailed logging: -```bash -# Edit .env file -SLOP_LOG_LEVEL=DEBUG -FLASK_DEBUG=true - -# Then start server -make run -# or directly: python3 slop_with_terminal.py -``` - -## Performance Tuning - -### Firecracker Settings -Edit your `.env` file: -```bash -FIRECRACKER_MEMORY=1024 # Increase memory -FIRECRACKER_VCPU=2 # More CPU cores -``` - -### Timeout Settings -Edit your `.env` file: -```bash -DEFAULT_TIMEOUT=120 # Longer timeouts -``` - -## License - -This project is released under the Public Domain. See the original SLOP specification for more details. - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Add tests if applicable -5. Submit a pull request - -## Support - -For issues and questions: -- Check the troubleshooting section -- Review server logs in `data/slop_monolith.log` -- Test with `./Scripts/test_firecracker.sh` (auto-generated by setup) -- Use `make help` to see all available commands -- Verify endpoints with curl - -## Related Projects - -- [SLOP Specification](https://slop.unturf.com/) -- [Original SLOP Examples](https://github.com/agnt-gg/slop) - -## Quick Start Summary - -```bash -# Clone and navigate to project -git clone -cd slop-terminal - -# Setup environment -cp config/.env.example .env -make setup - -# Start server -make run - -# Test -curl http://localhost:31337/info -``` - ---- - -Built with ❤️ for the SLOP community \ No newline at end of file diff --git a/slop-terminal/config/.env.example b/slop-terminal/config/.env.example deleted file mode 100644 index a5febb1..0000000 --- a/slop-terminal/config/.env.example +++ /dev/null @@ -1,23 +0,0 @@ -# Server Configuration -SLOP_PORT=31337 -SLOP_DATA_DIR=./data -SLOP_LOG_LEVEL=INFO -FLASK_DEBUG=false - -# Security Settings -ENABLE_FIRECRACKER=true -ALLOW_DANGEROUS_TOOLS=false -REQUIRE_USER_ID=true -DEFAULT_TIMEOUT=60 - -# Firecracker Configuration -FIRECRACKER_KERNEL=./firecracker/kernel/vmlinux.bin -FIRECRACKER_ROOTFS=./firecracker/rootfs/rootfs.ext4 -FIRECRACKER_MEMORY=512 -FIRECRACKER_VCPU=1 - -# Model Endpoints (add as many as needed) -MODEL_ENDPOINT_0=https://hermes.ai.unturf.com/v1 -MODEL_API_KEY_0=not-needed -# MODEL_ENDPOINT_1=https://another-endpoint.com/v1 -# MODEL_API_KEY_1=your-api-key-here \ No newline at end of file diff --git a/slop-terminal/config/.gitkeep b/slop-terminal/config/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/slop-terminal/docker-compose.yml b/slop-terminal/docker-compose.yml deleted file mode 100644 index f5bcd9c..0000000 --- a/slop-terminal/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: '3.8' - -services: - slop-terminal: - build: . - ports: - - "31337:31337" - volumes: - - ./data:/app/data - - ./config:/app/config - environment: - - SLOP_PORT=31337 - - SLOP_DATA_DIR=/app/data - - FLASK_DEBUG=false - - ENABLE_FIRECRACKER=true - privileged: true # Required for KVM/Firecracker - devices: - - /dev/kvm:/dev/kvm - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:31337/info"] - interval: 30s - timeout: 10s - retries: 3 \ No newline at end of file diff --git a/slop-terminal/requirements.txt b/slop-terminal/requirements.txt deleted file mode 100644 index ff21ec1..0000000 --- a/slop-terminal/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Core dependencies -flask==2.3.3 -flask-swagger-ui==4.11.1 -openai==1.3.0 -requests==2.31.0 - -# Math and expression evaluation -expr.py==0.1.0 - -# System monitoring -psutil==5.9.5 - -# Development dependencies (optional) -pytest==7.4.3 -pytest-cov==4.1.0 -black==23.9.1 -flake8==6.1.0 -mypy==1.6.1 \ No newline at end of file diff --git a/slop-terminal/slop_with_terminal.py b/slop-terminal/slop_with_terminal.py deleted file mode 100644 index 385bbaf..0000000 --- a/slop-terminal/slop_with_terminal.py +++ /dev/null @@ -1,2182 +0,0 @@ -from flask import Flask, request, jsonify -from datetime import datetime -import os -import json -import expr -import subprocess -import shutil -import tempfile -import platform -import time -import socket -from threading import Lock -import threading -import random -import uuid -from openai import OpenAI -import logging -from flask_swagger_ui import get_swaggerui_blueprint -from pathlib import Path -from typing import Dict, Any, Optional, List -from dataclasses import dataclass, asdict -from abc import ABC, abstractmethod - -# Server configuration (must be defined first) -SERVER_CONFIG = { - "port": int(os.getenv("SLOP_PORT", "31337")), - "data_dir": os.getenv("SLOP_DATA_DIR", "./data"), - "log_level": os.getenv("SLOP_LOG_LEVEL", "INFO"), - "debug": os.getenv("FLASK_DEBUG", "true").lower() == "true" -} - -# Configure logging -DATA_DIR = SERVER_CONFIG["data_dir"] -LOG_FILE = os.path.join(DATA_DIR, "slop_monolith.log") - -# Set log level from configuration -log_level = getattr(logging, SERVER_CONFIG["log_level"].upper(), logging.INFO) - -logging.basicConfig( - level=log_level, - format="%(asctime)s - %(levelname)s - %(message)s", - handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()], -) -logger = logging.getLogger(__name__) - -app = Flask(__name__) - -# Swagger UI setup -SWAGGER_URL = "/openapi" -API_URL = "/static/openapi.yaml" -swaggerui_blueprint = get_swaggerui_blueprint( - SWAGGER_URL, API_URL, config={"app_name": "SLOP API"} -) -app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) - -# Global model-to-client map -MODEL_CLIENT_MAP = {} - -# Memory setup -MEMORY_FILE = os.path.join(DATA_DIR, "memory.json") -memory_lock = Lock() - -# Resources setup -RESOURCES_FILE = os.path.join(DATA_DIR, "resources.json") -resource_lock = Lock() - -# Terminal data setup -TERMINAL_DATA_DIR = os.path.join(DATA_DIR, "terminal") -terminal_lock = Lock() - -# Ensure data directory exists -if not os.path.exists(TERMINAL_DATA_DIR): - os.makedirs(TERMINAL_DATA_DIR) - logger.info(f"Created terminal data directory: {TERMINAL_DATA_DIR}") - -# Lock timeout -LOCK_TIMEOUT = 2 - -# Firecracker sandbox configuration -FIRECRACKER_CONFIG = { - "kernel_image_path": os.getenv("FIRECRACKER_KERNEL", "./firecracker/kernel/vmlinux.bin"), - "rootfs_image_path": os.getenv("FIRECRACKER_ROOTFS", "./firecracker/rootfs/rootfs.ext4"), - "memory_size_mib": int(os.getenv("FIRECRACKER_MEMORY", "512")), - "vcpu_count": int(os.getenv("FIRECRACKER_VCPU", "1")), - "enable_sandbox": os.getenv("ENABLE_FIRECRACKER", "false").lower() == "true" -} - -# Security configuration -SECURITY_CONFIG = { - "allow_dangerous_tools": os.getenv("ALLOW_DANGEROUS_TOOLS", "false").lower() == "true", - "require_user_id": os.getenv("REQUIRE_USER_ID", "true").lower() == "true", - "default_timeout": int(os.getenv("DEFAULT_TIMEOUT", "60")) # Increased from 30 to 60 -} - -# Server configuration already defined above - -# Global sandbox manager -sandbox_manager = None - - -@dataclass -class ToolResult: - """Standardized tool execution result for terminal tools""" - success: bool - data: Optional[Dict[str, Any]] = None - error: Optional[str] = None - metadata: Optional[Dict[str, Any]] = None - -@dataclass -class FirecrackerConfig: - """Configuration for Firecracker microVM""" - kernel_image_path: str - rootfs_image_path: str - memory_size_mib: int = 512 - vcpu_count: int = 1 - socket_path: str = "" - log_level: str = "Info" - enable_api: bool = True - api_socket_path: str = "" - -@dataclass -class SandboxResult: - """Result from sandbox execution""" - success: bool - stdout: str = "" - stderr: str = "" - exit_code: int = 0 - execution_time: float = 0.0 - error: Optional[str] = None - venv_path: Optional[str] = None - sandbox_id: Optional[str] = None - -class FirecrackerSandbox: - """Manages Firecracker microVM instances for safe code execution""" - - def __init__(self, config: dict): - self.config = config - self.active_vms: Dict[str, Dict[str, Any]] = {} - self.lock = Lock() - # Use tempfile for cross-platform compatibility - self.base_dir = Path(tempfile.gettempdir()) / "firecracker_sandboxes" - self.base_dir.mkdir(exist_ok=True) - logger.info(f"Initialized Firecracker sandbox manager at {self.base_dir}") - - def create_sandbox(self, user_id: str) -> str: - """Create a new sandbox for a user""" - vm_id = f"sandbox_{user_id}_{uuid.uuid4().hex[:8]}" - vm_dir = self.base_dir / vm_id - vm_dir.mkdir(exist_ok=True) - - with self.lock: - self.active_vms[vm_id] = { - "vm_dir": vm_dir, - "user_id": user_id, - "created_at": time.time(), - "status": "created", - "venv_initialized": False, # Track venv state - "pip_upgraded": False # Track pip upgrade state - } - - logger.info(f"Created sandbox {vm_id} for user {user_id}") - return vm_id - - def execute_in_sandbox(self, vm_id: str, commands: List[str], - timeout: int = 30, create_venv: bool = True) -> SandboxResult: - """Execute commands in a sandboxed environment""" - if vm_id not in self.active_vms: - return SandboxResult( - success=False, - error=f"Sandbox {vm_id} not found" - ) - - vm_info = self.active_vms[vm_id] - start_time = time.time() - - try: - # Create isolated workspace - workspace = vm_info["vm_dir"] / "workspace" - workspace.mkdir(exist_ok=True) - - script_lines = ["#!/bin/bash", "set -e", "cd " + str(workspace)] - - if create_venv: - venv_path = str(workspace / "venv") - - # Only create venv if it doesn't exist or wasn't initialized - if not vm_info["venv_initialized"] or not (workspace / "venv").exists(): - script_lines.extend([ - f"python3 -m venv {venv_path}", - f"source {venv_path}/bin/activate", - ]) - - # Only upgrade pip if we haven't done it yet - if not vm_info["pip_upgraded"]: - script_lines.append("pip install --upgrade pip --quiet") - vm_info["pip_upgraded"] = True - - vm_info["venv_initialized"] = True - self.active_vms[vm_id] = vm_info # Update state - else: - # Just activate existing venv - script_lines.append(f"source {venv_path}/bin/activate") - else: - venv_path = None - - # Add user commands - script_lines.extend(commands) - - # Create and execute script - script_content = "\n".join(script_lines) - script_file = vm_info["vm_dir"] / "execute.sh" - - with open(script_file, 'w') as f: - f.write(script_content) - - os.chmod(script_file, 0o755) - - # Execute in isolated environment - result = subprocess.run( - ["bash", str(script_file)], - capture_output=True, - text=True, - timeout=timeout, - cwd=str(workspace) - ) - - execution_time = time.time() - start_time - - # Clean up pip upgrade messages from output - stdout_cleaned = result.stdout - stderr_cleaned = result.stderr - - # Remove pip upgrade noise from stdout - if stdout_cleaned: - lines = stdout_cleaned.split('\n') - filtered_lines = [] - skip_next = False - - for line in lines: - if any(noise in line for noise in [ - "Requirement already satisfied: pip", - "Collecting pip", - "Using cached pip", - "Installing collected packages: pip", - "Attempting uninstall: pip", - "Found existing installation: pip", - "Uninstalling pip", - "Successfully uninstalled pip", - "Successfully installed pip" - ]): - continue - filtered_lines.append(line) - - stdout_cleaned = '\n'.join(filtered_lines).strip() - - return SandboxResult( - success=result.returncode == 0, - stdout=stdout_cleaned, - stderr=stderr_cleaned, - exit_code=result.returncode, - execution_time=execution_time, - venv_path=venv_path, - sandbox_id=vm_id - ) - - except subprocess.TimeoutExpired: - return SandboxResult( - success=False, - error=f"Execution timed out after {timeout} seconds", - execution_time=time.time() - start_time, - sandbox_id=vm_id - ) - except Exception as e: - return SandboxResult( - success=False, - error=str(e), - execution_time=time.time() - start_time, - sandbox_id=vm_id - ) - - def cleanup_sandbox(self, vm_id: str) -> bool: - """Clean up sandbox resources""" - if vm_id not in self.active_vms: - return False - - vm_info = self.active_vms[vm_id] - - try: - if vm_info["vm_dir"].exists(): - shutil.rmtree(vm_info["vm_dir"]) - - with self.lock: - del self.active_vms[vm_id] - - logger.info(f"Cleaned up sandbox {vm_id}") - return True - - except Exception as e: - logger.error(f"Failed to cleanup sandbox {vm_id}: {e}") - return False - - -class SandboxManager: - """High-level manager for sandboxed execution""" - - def __init__(self, config: dict): - self.config = config - self.enabled = config.get("enable_sandbox", False) - - if self.enabled: - self.firecracker = FirecrackerSandbox(config) - self.sandbox_pool: Dict[str, str] = {} # user_id -> vm_id - self.user_packages: Dict[str, set] = {} # user_id -> set of installed packages - self.lock = Lock() - logger.info("Sandbox manager initialized with Firecracker") - else: - self.firecracker = None - logger.info("Sandbox manager initialized without Firecracker (disabled)") - - def get_or_create_sandbox(self, user_id: str) -> Optional[str]: - """Get existing sandbox for user or create new one""" - if not self.enabled: - return None - - with self.lock: - if user_id in self.sandbox_pool: - vm_id = self.sandbox_pool[user_id] - if vm_id in self.firecracker.active_vms: - return vm_id - - # Create new sandbox - vm_id = self.firecracker.create_sandbox(user_id) - self.sandbox_pool[user_id] = vm_id - self.user_packages[user_id] = set() - return vm_id - - def execute_python_code(self, user_id: str, code: str, - packages: Optional[List[str]] = None, - timeout: int = 30) -> SandboxResult: - """Execute Python code in a sandboxed environment""" - if not self.enabled: - return SandboxResult( - success=False, - error="Sandbox execution is disabled" - ) - - vm_id = self.get_or_create_sandbox(user_id) - if not vm_id: - return SandboxResult( - success=False, - error="Failed to create sandbox" - ) - - commands = [] - - # Install packages if specified and not already installed - if packages: - user_installed_packages = self.user_packages.get(user_id, set()) - new_packages = [] - - for package in packages: - if package not in user_installed_packages: - new_packages.append(package) - user_installed_packages.add(package) - - if new_packages: - commands.append(f"pip install --quiet {' '.join(new_packages)}") - self.user_packages[user_id] = user_installed_packages - - # Execute Python code - commands.append(f"python3 -c '{code}'") - - return self.firecracker.execute_in_sandbox( - vm_id, commands, timeout=timeout, create_venv=True - ) - - def execute_shell_command(self, user_id: str, command: str, - timeout: int = 30) -> SandboxResult: - """Execute shell command in sandbox""" - if not self.enabled: - return SandboxResult( - success=False, - error="Sandbox execution is disabled" - ) - - vm_id = self.get_or_create_sandbox(user_id) - if not vm_id: - return SandboxResult( - success=False, - error="Failed to create sandbox" - ) - - return self.firecracker.execute_in_sandbox( - vm_id, [command], timeout=timeout, create_venv=False - ) - - def cleanup_user_sandbox(self, user_id: str) -> bool: - """Clean up sandbox for specific user""" - if not self.enabled: - return False - - with self.lock: - if user_id in self.sandbox_pool: - vm_id = self.sandbox_pool[user_id] - success = self.firecracker.cleanup_sandbox(vm_id) - if success: - del self.sandbox_pool[user_id] - if user_id in self.user_packages: - del self.user_packages[user_id] - return success - return False - -class TerminalTool(ABC): - """Base class for terminal tools""" - - @property - @abstractmethod - def tool_id(self) -> str: - pass - - @property - @abstractmethod - def description(self) -> str: - pass - - @property - @abstractmethod - def arguments(self) -> List[Dict[str, Any]]: - pass - - @abstractmethod - def execute(self, params: Dict[str, Any]) -> ToolResult: - pass - - -class ExecTool(TerminalTool): - """Execute shell commands""" - - @property - def tool_id(self) -> str: - return "exec" - - @property - def description(self) -> str: - return "Execute shell commands with timeout and safety controls" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [ - {"name": "command", "type": "str", "description": "Shell command to execute"}, - {"name": "timeout", "type": "int", "description": "Timeout in seconds (default: 60)", "optional": True}, - {"name": "working_dir", "type": "str", "description": "Working directory for command execution", "optional": True} - ] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - command = params.get("command") - timeout = params.get("timeout", 30) - working_dir = params.get("working_dir", os.getcwd()) - - if not command: - return ToolResult(success=False, error="Command is required") - - try: - result = subprocess.run( - command, - shell=True, - capture_output=True, - text=True, - timeout=timeout, - cwd=working_dir - ) - - return ToolResult( - success=True, - data={ - "command": command, - "stdout": result.stdout, - "stderr": result.stderr, - "exit_code": result.returncode, - "working_dir": working_dir - } - ) - except subprocess.TimeoutExpired: - return ToolResult( - success=False, - error=f"Command timed out after {timeout} seconds", - metadata={"command": command} - ) - except Exception as e: - return ToolResult( - success=False, - error=str(e), - metadata={"command": command} - ) - - -class FileReadTool(TerminalTool): - """Read file contents""" - - @property - def tool_id(self) -> str: - return "file_read" - - @property - def description(self) -> str: - return "Read file contents with encoding support" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [ - {"name": "path", "type": "str", "description": "File path to read"}, - {"name": "encoding", "type": "str", "description": "File encoding (default: utf-8)", "optional": True} - ] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - file_path = params.get("path") - encoding = params.get("encoding", "utf-8") - - if not file_path: - return ToolResult(success=False, error="File path is required") - - try: - resolved_path = os.path.abspath(file_path) - - with open(resolved_path, 'r', encoding=encoding) as f: - content = f.read() - - file_stat = os.stat(resolved_path) - - return ToolResult( - success=True, - data={ - "path": resolved_path, - "content": content, - "size": len(content), - "modified": datetime.fromtimestamp(file_stat.st_mtime).isoformat(), - "encoding": encoding - } - ) - except FileNotFoundError: - return ToolResult(success=False, error=f"File not found: {file_path}") - except PermissionError: - return ToolResult(success=False, error=f"Permission denied: {file_path}") - except UnicodeDecodeError: - return ToolResult(success=False, error=f"Cannot decode file with {encoding} encoding") - except Exception as e: - return ToolResult(success=False, error=str(e)) - - -class FileWriteTool(TerminalTool): - """Write content to file""" - - @property - def tool_id(self) -> str: - return "file_write" - - @property - def description(self) -> str: - return "Write content to file with backup option" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [ - {"name": "path", "type": "str", "description": "File path to write"}, - {"name": "content", "type": "str", "description": "Content to write to file"}, - {"name": "encoding", "type": "str", "description": "File encoding (default: utf-8)", "optional": True}, - {"name": "backup", "type": "bool", "description": "Create backup of existing file", "optional": True} - ] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - file_path = params.get("path") - content = params.get("content", "") - encoding = params.get("encoding", "utf-8") - backup = params.get("backup", False) - - if not file_path: - return ToolResult(success=False, error="File path is required") - - try: - resolved_path = os.path.abspath(file_path) - - # Create backup if requested and file exists - backup_created = False - if backup and os.path.exists(resolved_path): - backup_path = f"{resolved_path}.backup.{int(datetime.now().timestamp())}" - shutil.copy2(resolved_path, backup_path) - backup_created = True - - # Create directory if it doesn't exist - os.makedirs(os.path.dirname(resolved_path), exist_ok=True) - - with open(resolved_path, 'w', encoding=encoding) as f: - f.write(content) - - return ToolResult( - success=True, - data={ - "path": resolved_path, - "size": len(content), - "message": f"Successfully wrote {len(content)} characters to {resolved_path}", - "backup_created": backup_created - } - ) - except PermissionError: - return ToolResult(success=False, error=f"Permission denied: {file_path}") - except Exception as e: - return ToolResult(success=False, error=str(e)) - - -class DirectoryListTool(TerminalTool): - """List directory contents""" - - @property - def tool_id(self) -> str: - return "directory_list" - - @property - def description(self) -> str: - return "List directory contents with detailed information" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [ - {"name": "path", "type": "str", "description": "Directory path to list (default: current directory)", "optional": True}, - {"name": "show_hidden", "type": "bool", "description": "Show hidden files (default: false)", "optional": True}, - {"name": "detailed", "type": "bool", "description": "Show detailed file information (default: true)", "optional": True} - ] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - dir_path = params.get("path", ".") - show_hidden = params.get("show_hidden", False) - detailed = params.get("detailed", True) - - try: - resolved_path = os.path.abspath(dir_path) - - if not os.path.exists(resolved_path): - return ToolResult(success=False, error=f"Directory not found: {dir_path}") - - if not os.path.isdir(resolved_path): - return ToolResult(success=False, error=f"Not a directory: {dir_path}") - - items = [] - for item in os.listdir(resolved_path): - if not show_hidden and item.startswith('.'): - continue - - item_path = os.path.join(resolved_path, item) - item_info = { - "name": item, - "path": item_path, - "type": "directory" if os.path.isdir(item_path) else "file" - } - - if detailed: - try: - stat = os.stat(item_path) - item_info.update({ - "size": stat.st_size, - "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(), - "permissions": oct(stat.st_mode)[-3:], - "owner": stat.st_uid - }) - except: - pass - - items.append(item_info) - - # Sort: directories first, then files, both alphabetically - items.sort(key=lambda x: (x["type"] == "file", x["name"].lower())) - - return ToolResult( - success=True, - data={ - "path": resolved_path, - "items": items, - "count": len(items), - "total_size": sum(item.get("size", 0) for item in items if item["type"] == "file") - } - ) - except PermissionError: - return ToolResult(success=False, error=f"Permission denied: {dir_path}") - except Exception as e: - return ToolResult(success=False, error=str(e)) - - -class SystemInfoTool(TerminalTool): - """Get system information""" - - @property - def tool_id(self) -> str: - return "sysinfo" - - @property - def description(self) -> str: - return "Get system information and resource usage" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - try: - try: - import psutil - has_psutil = True - except ImportError: - has_psutil = False - - system_info = { - "platform": platform.platform(), - "system": platform.system(), - "release": platform.release(), - "version": platform.version(), - "machine": platform.machine(), - "processor": platform.processor(), - "python_version": platform.python_version(), - "current_directory": os.getcwd(), - "user": os.getenv("USER", "unknown"), - "home": os.path.expanduser("~") - } - - if has_psutil: - system_info["resources"] = { - "cpu_count": psutil.cpu_count(), - "memory_total": psutil.virtual_memory().total, - "memory_available": psutil.virtual_memory().available, - "disk_usage": { - "total": psutil.disk_usage('/').total, - "used": psutil.disk_usage('/').used, - "free": psutil.disk_usage('/').free - } - } - else: - system_info["note"] = "Install psutil for detailed resource information" - - return ToolResult(success=True, data=system_info) - except Exception as e: - return ToolResult(success=False, error=str(e)) - - -class PwdTool(TerminalTool): - """Get current working directory""" - - @property - def tool_id(self) -> str: - return "pwd" - - @property - def description(self) -> str: - return "Get current working directory" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - try: - return ToolResult( - success=True, - data={ - "current_directory": os.getcwd(), - "absolute_path": os.path.abspath("."), - "user": os.getenv("USER", "unknown") - } - ) - except Exception as e: - return ToolResult(success=False, error=str(e)) - - -class CdTool(TerminalTool): - """Change current working directory""" - - @property - def tool_id(self) -> str: - return "cd" - - @property - def description(self) -> str: - return "Change current working directory" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [ - {"name": "path", "type": "str", "description": "Directory path to change to"} - ] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - dir_path = params.get("path") - - if not dir_path: - return ToolResult(success=False, error="Directory path is required") - - try: - previous_dir = os.getcwd() - os.chdir(dir_path) - new_dir = os.getcwd() - - return ToolResult( - success=True, - data={ - "previous_directory": previous_dir, - "new_directory": new_dir, - "message": f"Changed directory from {previous_dir} to {new_dir}" - } - ) - except FileNotFoundError: - return ToolResult(success=False, error=f"Directory not found: {dir_path}") - except PermissionError: - return ToolResult(success=False, error=f"Permission denied: {dir_path}") - except Exception as e: - return ToolResult(success=False, error=str(e)) - - -class SandboxedExecTool(TerminalTool): - """Sandboxed version of the exec tool using Firecracker""" - - def __init__(self, sandbox_manager: SandboxManager): - self.sandbox_manager = sandbox_manager - - @property - def tool_id(self) -> str: - return "sandbox_exec" - - @property - def description(self) -> str: - return "Execute shell commands in a secure Firecracker sandbox with venv isolation" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [ - {"name": "command", "type": "str", "description": "Shell command to execute"}, - {"name": "user_id", "type": "str", "description": "User ID for sandbox isolation"}, - {"name": "timeout", "type": "int", "description": "Timeout in seconds (default: 60)", "optional": True}, - {"name": "create_venv", "type": "bool", "description": "Create Python venv (default: false)", "optional": True} - ] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - command = params.get("command") - user_id = params.get("user_id", "anonymous") - timeout = params.get("timeout", 60) # Increased from 30 to 60 - create_venv = params.get("create_venv", False) - - if not command: - return ToolResult(success=False, error="Command is required") - - if not self.sandbox_manager.enabled: - return ToolResult( - success=False, - error="Sandbox execution is disabled. Set ENABLE_FIRECRACKER=true to enable." - ) - - # Retry logic - attempt twice - for attempt in range(2): - if create_venv: - # Execute as Python code in venv - result = self.sandbox_manager.execute_python_code( - user_id, command, timeout=timeout - ) - else: - # Execute as shell command - result = self.sandbox_manager.execute_shell_command( - user_id, command, timeout=timeout - ) - - # If successful, return immediately - if result.success: - return ToolResult( - success=result.success, - data={ - "stdout": result.stdout, - "stderr": result.stderr, - "exit_code": result.exit_code, - "execution_time": result.execution_time, - "sandbox_id": result.sandbox_id, - "venv_path": result.venv_path, - "attempt": attempt + 1 - }, - error=result.error - ) - - # If first attempt failed, log and retry - if attempt == 0: - logger.warning(f"Sandbox execution attempt 1 failed: {result.error}. Retrying...") - - # Both attempts failed - return ToolResult( - success=False, - data={ - "stdout": result.stdout, - "stderr": result.stderr, - "exit_code": result.exit_code, - "execution_time": result.execution_time, - "sandbox_id": result.sandbox_id, - "venv_path": result.venv_path, - "attempts": 2 - }, - error=f"Failed after 2 attempts: {result.error}" - ) - - -class SandboxedPythonTool(TerminalTool): - """Tool for executing Python code in Firecracker sandbox""" - - def __init__(self, sandbox_manager: SandboxManager): - self.sandbox_manager = sandbox_manager - - @property - def tool_id(self) -> str: - return "sandbox_python" - - @property - def description(self) -> str: - return "Execute Python code in a secure Firecracker sandbox with venv and package installation" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [ - {"name": "code", "type": "str", "description": "Python code to execute"}, - {"name": "user_id", "type": "str", "description": "User ID for sandbox isolation"}, - {"name": "packages", "type": "list", "description": "Python packages to install", "optional": True}, - {"name": "timeout", "type": "int", "description": "Timeout in seconds (default: 60)", "optional": True} - ] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - code = params.get("code") - user_id = params.get("user_id", "anonymous") - packages = params.get("packages", []) - timeout = params.get("timeout", 60) # Increased from 30 to 60 - - if not code: - return ToolResult(success=False, error="Code is required") - - if not self.sandbox_manager.enabled: - return ToolResult( - success=False, - error="Sandbox execution is disabled. Set ENABLE_FIRECRACKER=true to enable." - ) - - # Retry logic - attempt twice - for attempt in range(2): - result = self.sandbox_manager.execute_python_code( - user_id, code, packages=packages, timeout=timeout - ) - - # If successful, return immediately - if result.success: - return ToolResult( - success=result.success, - data={ - "stdout": result.stdout, - "stderr": result.stderr, - "exit_code": result.exit_code, - "execution_time": result.execution_time, - "sandbox_id": result.sandbox_id, - "venv_path": result.venv_path, - "packages_installed": packages, - "attempt": attempt + 1 - }, - error=result.error - ) - - # If first attempt failed, log and retry - if attempt == 0: - logger.warning(f"Python sandbox execution attempt 1 failed: {result.error}. Retrying...") - - # Both attempts failed - return ToolResult( - success=False, - data={ - "stdout": result.stdout, - "stderr": result.stderr, - "exit_code": result.exit_code, - "execution_time": result.execution_time, - "sandbox_id": result.sandbox_id, - "venv_path": result.venv_path, - "packages_installed": packages, - "attempts": 2 - }, - error=f"Failed after 2 attempts: {result.error}" - ) - -class SandboxManagerTool(TerminalTool): - """Tool for managing sandboxes""" - - def __init__(self, sandbox_manager: SandboxManager): - self.sandbox_manager = sandbox_manager - - @property - def tool_id(self) -> str: - return "sandbox_manage" - - @property - def description(self) -> str: - return "Manage Firecracker sandboxes - list, cleanup, or get status" - - @property - def arguments(self) -> List[Dict[str, Any]]: - return [ - {"name": "action", "type": "str", "description": "Action: 'list', 'cleanup', 'status'"}, - {"name": "user_id", "type": "str", "description": "User ID for cleanup action", "optional": True} - ] - - def execute(self, params: Dict[str, Any]) -> ToolResult: - action = params.get("action") - user_id = params.get("user_id") - - if not action: - return ToolResult(success=False, error="Action is required") - - if not self.sandbox_manager.enabled: - return ToolResult( - success=False, - error="Sandbox management is disabled. Set ENABLE_FIRECRACKER=true to enable." - ) - - try: - if action == "list": - active_vms = {} - if hasattr(self.sandbox_manager, 'firecracker'): - active_vms = { - vm_id: { - "user_id": vm_info.get("user_id"), - "status": vm_info.get("status"), - "created_at": vm_info.get("created_at") - } - for vm_id, vm_info in self.sandbox_manager.firecracker.active_vms.items() - } - - return ToolResult( - success=True, - data={ - "active_sandboxes": active_vms, - "total_count": len(active_vms), - "enabled": self.sandbox_manager.enabled - } - ) - - elif action == "cleanup": - if not user_id: - return ToolResult(success=False, error="user_id is required for cleanup") - - success = self.sandbox_manager.cleanup_user_sandbox(user_id) - return ToolResult( - success=success, - data={ - "user_id": user_id, - "cleanup_success": success - } - ) - - elif action == "status": - status = { - "enabled": self.sandbox_manager.enabled, - "config": self.sandbox_manager.config, - "active_users": len(getattr(self.sandbox_manager, 'sandbox_pool', {})) - } - - return ToolResult( - success=True, - data=status - ) - - else: - return ToolResult( - success=False, - error=f"Unknown action: {action}. Use 'list', 'cleanup', or 'status'" - ) - - except Exception as e: - return ToolResult( - success=False, - error=str(e) - ) - - -# Initialize terminal tools -terminal_tools = {} - -def register_terminal_tool(tool: TerminalTool): - """Register a terminal tool""" - terminal_tools[tool.tool_id] = tool - logger.info(f"Registered terminal tool: {tool.tool_id}") - -def initialize_terminal_tools(): - """Initialize all terminal tools with security controls""" - - # Always include safe tools - safe_tools = [ - SystemInfoTool(), - PwdTool(), - ] - - # Add sandboxed tools if sandbox is enabled - if sandbox_manager and sandbox_manager.enabled: - safe_tools.extend([ - SandboxedExecTool(sandbox_manager), - SandboxedPythonTool(sandbox_manager), - SandboxManagerTool(sandbox_manager), - ]) - logger.info("✅ Sandbox tools enabled - secure execution available") - logger.info("🔒 Dangerous operations only available through sandbox") - - # If no sandbox, NO dangerous tools at all - else: - logger.info("🔒 No sandbox - dangerous tools completely disabled") - logger.info("🔒 Enable sandbox for secure exec/python execution") - - # Register all approved tools - for tool in safe_tools: - register_terminal_tool(tool) - - -# File-based memory functions -def load_memory_from_file(): - try: - if os.path.exists(MEMORY_FILE): - with open(MEMORY_FILE, "r") as f: - data = json.load(f) - logger.debug(f"Loaded memory from {MEMORY_FILE}: {data}") - return data - logger.debug(f"No memory file found at {MEMORY_FILE}, returning empty dict") - return {} - except Exception as e: - logger.error( - f"Error loading memory from {MEMORY_FILE}: {str(e)}", exc_info=True - ) - return {} - - -def save_memory_to_file(memory_data): - try: - with open(MEMORY_FILE, "w") as f: - json.dump(memory_data, f) - f.flush() - os.fsync(f.fileno()) - logger.debug(f"Saved memory to {MEMORY_FILE}: {memory_data}") - except Exception as e: - logger.error(f"Error saving memory to {MEMORY_FILE}: {str(e)}", exc_info=True) - - -# File-based resources functions -def load_resources_from_file(): - try: - if os.path.exists(RESOURCES_FILE): - with open(RESOURCES_FILE, "r") as f: - data = json.load(f) - logger.debug(f"Loaded resources from {RESOURCES_FILE}: {data}") - return data - else: - # If file doesn't exist, create it with example resources - initial_resources = { - "hello": {"id": "hello", "content": "Hello, SLOP!"}, - "foo/bar": {"id": "foo/bar", "content": "Nested Foo Bar"}, - "foo/baz": {"id": "foo/baz", "content": "Nested Foo Baz"}, - # Terminal-specific resources - "scripts/hello": { - "id": "scripts/hello", - "title": "Hello World Script", - "type": "script", - "content": "#!/bin/bash\necho 'Hello, World!'" - }, - "configs/bashrc": { - "id": "configs/bashrc", - "title": "Bash Configuration", - "type": "config", - "content": "# Custom bash configuration\nalias ll='ls -la'\nexport EDITOR=vim" - }, - "docs/readme": { - "id": "docs/readme", - "title": "README", - "type": "document", - "content": "# SLOP Monolith Server\n\nA combined SLOP server with terminal and gaming capabilities." - } - } - save_resources_to_file(initial_resources) - logger.info( - f"Created new resources file at {RESOURCES_FILE} with initial data" - ) - return initial_resources - except Exception as e: - logger.error( - f"Error loading resources from {RESOURCES_FILE}: {str(e)}", exc_info=True - ) - # On error, create and return initial resources - initial_resources = { - "hello": {"id": "hello", "content": "Hello, SLOP!"}, - "foo/bar": {"id": "foo/bar", "content": "Nested Foo Bar"}, - "foo/baz": {"id": "foo/baz", "content": "Nested Foo Baz"}, - } - save_resources_to_file(initial_resources) - return initial_resources - - -def save_resources_to_file(resources_data): - try: - with open(RESOURCES_FILE, "w") as f: - json.dump(resources_data, f, indent=2) - f.flush() - os.fsync(f.fileno()) - logger.debug(f"Saved resources to {RESOURCES_FILE}: {resources_data}") - except Exception as e: - logger.error( - f"Error saving resources to {RESOURCES_FILE}: {str(e)}", exc_info=True - ) - - -# Load endpoints for AI models -ENDPOINTS = [] -for i in range(1000): - endpoint = os.getenv(f"MODEL_ENDPOINT_{i}") - if endpoint: - api_key = os.getenv(f"MODEL_API_KEY_{i}", "not-needed") - logger.info(f"Loaded endpoint {i}: {endpoint} with API key: {api_key[:4]}...") - ENDPOINTS.append( - { - "name": f"endpoint_{i}", - "base_url": endpoint, - "api_key": api_key, - } - ) - elif i < 5: # Only log first 5 missing endpoints to reduce noise - logger.debug(f"No endpoint found for MODEL_ENDPOINT_{i}") - - -def initialize_model_map(): - logger.info("Starting model map initialization") - MODEL_CLIENT_MAP.clear() - if not ENDPOINTS: - logger.warning("No endpoints configured to initialize models") - return - for ep in ENDPOINTS: - base_url = ep["base_url"] - api_key = ep["api_key"] - endpoint_name = ep["name"] - logger.info(f"Initializing client for {endpoint_name} at {base_url}") - client = OpenAI(base_url=base_url, api_key=api_key) - try: - logger.debug(f"Attempting to list models from {endpoint_name}") - response = client.models.list() - model_list = response.data - logger.debug( - f"Models retrieved from {endpoint_name}: {[m.id for m in model_list]}" - ) - except Exception as e: - logger.error( - f"Failed to list models for {endpoint_name}: {str(e)}", exc_info=True - ) - continue - for m in model_list: - model_id = m.id - if model_id: - if model_id in MODEL_CLIENT_MAP: - logger.warning( - f"Duplicate model ID '{model_id}' found at {endpoint_name}" - ) - else: - MODEL_CLIENT_MAP[model_id] = client - logger.info(f"Registered model '{model_id}' from {endpoint_name}") - else: - logger.warning(f"Model with no ID encountered from {endpoint_name}") - logger.info(f"Model map initialized with models: {list(MODEL_CLIENT_MAP.keys())}") - - -def update_resource(resource_id, new_content): - with resource_lock: - resources = load_resources_from_file() - resources[resource_id] = {"id": resource_id, "content": new_content} - save_resources_to_file(resources) - logger.debug(f"Updated resource {resource_id}: {new_content}") - - -# Helper functions for casino games -def get_or_create_wallet(agent_id): - wallet_key = f"casino/wallet/agent_{agent_id}" - with resource_lock: - resources = load_resources_from_file() - if wallet_key not in resources: - resources[wallet_key] = { - "id": wallet_key, - "content": {"balance": 1000, "last_transaction": None}, - } - save_resources_to_file(resources) - logger.info(f"Created new wallet for agent {agent_id} with balance 1000") - return resources[wallet_key]["content"] - - -def get_or_create_stats(game, agent_id): - stats_key = f"casino/{game}/stats/agent_{agent_id}" - with resource_lock: - resources = load_resources_from_file() - if stats_key not in resources: - if game == "slots": - resources[stats_key] = { - "id": stats_key, - "content": { - "games_played": 0, - "total_bet": 0, - "total_won": 0, - "wins": 0, - "losses": 0, - }, - } - elif game == "blackjack": - resources[stats_key] = { - "id": stats_key, - "content": { - "games_played": 0, - "total_bet": 0, - "total_won": 0, - "wins": 0, - "losses": 0, - "ties": 0, - }, - } - save_resources_to_file(resources) - logger.info(f"Created new {game} stats for agent {agent_id}") - return resources[stats_key]["content"] - - -# Casino game implementations -def play_slots(bet, agent_id): - if bet < 1: - return {"error": "Bet must be at least 1"} - if not agent_id: - return {"error": "agent_id is required"} - - wallet = get_or_create_wallet(agent_id) - if wallet["balance"] < bet: - return { - "error": f"Insufficient funds! Current balance: {wallet['balance']}, Bet: {bet}" - } - - symbols = ["🍒", "🍋", "🍊", "🍇", "🔔", "💎"] - reels = [random.choice(symbols) for _ in range(3)] - matches = len(set(reels)) - - if matches == 1: - payout = bet * 10 - outcome = "Jackpot! You matched all three symbols." - won = True - elif matches == 2: - payout = bet * 2 - outcome = "Two of a kind! You matched two symbols." - won = True - else: - payout = 0 - outcome = "No matches. You lose your bet." - won = False - - wallet_key = f"casino/wallet/agent_{agent_id}" - wallet["balance"] = wallet["balance"] - bet + payout - wallet["last_transaction"] = { - "game": "slots", - "bet": bet, - "payout": payout, - "timestamp": datetime.now().isoformat(), - } - update_resource(wallet_key, wallet) - - stats = get_or_create_stats("slots", agent_id) - stats["games_played"] += 1 - stats["total_bet"] += bet - stats["total_won"] += payout - if won: - stats["wins"] += 1 - else: - stats["losses"] += 1 - update_resource(f"casino/slots/stats/agent_{agent_id}", stats) - - explanation = ( - "Slot Machine Rules: Bet an amount to spin three reels once. " - "Match all 3 symbols for 10x your bet, 2 symbols for 2x your bet, or lose your bet if no match. " - f"Symbols: {symbols}. " - f"You (agent {agent_id}) bet {bet}. Reels: {reels}. {outcome}" - ) - - return { - "explanation": explanation, - "game_state": {"reels": reels}, - "payout": payout, - "bet": bet, - "agent_id": agent_id, - "new_balance": wallet["balance"], - "game_complete": True, - } - - -def play_blackjack(bet, agent_id, game_id=None, action=None): - if not agent_id: - return {"error": "agent_id is required"} - - # Start a new game - if game_id is None: - if bet is None or bet < 1: - return {"error": "Bet must be at least 1 to start a new game"} - - wallet = get_or_create_wallet(agent_id) - if wallet["balance"] < bet: - return { - "error": f"Insufficient funds! Current balance: {wallet['balance']}, Bet: {bet}" - } - - game_id = str(uuid.uuid4()) - cards = [2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 11] * 4 - player_hand = [random.choice(cards), random.choice(cards)] - dealer_hand = [random.choice(cards)] - - player_total = sum(player_hand) - aces = player_hand.count(11) - while player_total > 21 and aces > 0: - player_total -= 10 - aces -= 1 - - game_state = { - "agent_id": agent_id, - "bet": bet, - "player_hands": [player_hand], - "player_totals": [player_total], - "dealer_hand": dealer_hand, - "dealer_total": sum(dealer_hand), - "status": "active", - "active_hand": 0, - } - update_resource(f"casino/blackjack/games/{game_id}", game_state) - - explanation = ( - "Blackjack Rules: Bet to play against the dealer. Goal is to get closer to 21 without going over. " - "Number cards = face value, Face cards = 10, Ace = 1 or 11 (adjusted automatically). " - "Hit to draw cards, stand to finish, split if two cards match. Dealer stands on 17+. " - f"You (agent {agent_id}) started a new game with bet {bet}. " - f"Your hand: {player_hand} (Total: {player_total}). Dealer's up card: {dealer_hand}." - ) - - return { - "explanation": explanation, - "game_id": game_id, - "game_state": { - "player_hands": [player_hand], - "player_totals": [player_total], - "dealer_hand": dealer_hand, - "dealer_total": sum(dealer_hand), - "can_split": len(player_hand) == 2 and player_hand[0] == player_hand[1], - }, - "bet": bet, - "agent_id": agent_id, - "new_balance": wallet["balance"], - "game_complete": False, - } - - # Continue existing game logic... - # (The rest of the blackjack implementation remains the same) - game_key = f"casino/blackjack/games/{game_id}" - with resource_lock: - resources = load_resources_from_file() - if game_key not in resources: - return {"error": f"Game {game_id} not found"} - game_state = resources[game_key]["content"] - - if game_state["agent_id"] != agent_id: - return {"error": "This game belongs to another agent"} - if game_state["status"] != "active": - return {"error": "This game is already complete"} - if action not in ["hit", "stand", "split"]: - return {"error": "Action must be 'hit', 'stand', or 'split'"} - - # ... (rest of blackjack logic remains the same as original) - # Truncated for brevity but would include all the game logic - - -# Combined tools (games + terminal) -tools = { - "calculator": { - "id": "calculator", - "description": "Basic math calculator", - "arguments": [ - { - "name": "expression", - "type": "str", - "description": "the math expression to evaluate.", - } - ], - "execute": lambda params: {"result": expr.evaluate(params["expression"])}, - }, - "greet": { - "id": "greet", - "description": "Says hello", - "arguments": [ - {"name": "name", "type": "str", "description": "name of person to greet"} - ], - "execute": lambda params: {"result": f"Hello, {params['name']}!"}, - }, - "slots": { - "id": "slots", - "description": "Play a slot machine game with a bet for a specific agent", - "arguments": [ - {"name": "bet", "type": "int", "description": "Amount to bet (minimum 1)"}, - { - "name": "agent_id", - "type": "str", - "description": "Unique identifier for the agent", - }, - ], - "execute": lambda params: play_slots(params["bet"], params["agent_id"]), - }, - "blackjack": { - "id": "blackjack", - "description": "Play blackjack: start a new game or continue an existing one", - "arguments": [ - { - "name": "bet", - "type": "int", - "description": "Amount to bet (required to start new game)", - "optional": True, - }, - { - "name": "agent_id", - "type": "str", - "description": "Unique identifier for the agent", - }, - { - "name": "game_id", - "type": "str", - "description": "UUID of existing game (optional, omit to start new)", - "optional": True, - }, - { - "name": "action", - "type": "str", - "description": "Action: 'hit' or 'stand' (required if game_id provided)", - "optional": True, - }, - ], - "execute": lambda params: play_blackjack( - params.get("bet"), - params["agent_id"], - params.get("game_id"), - params.get("action"), - ), - }, -} - - -# Memory endpoints -@app.route("/memory", methods=["POST"]) -def store_memory(): - logger.info("Received /memory POST request") - data = request.json - logger.debug(f"Store memory data: {data}") - if not data or "key" not in data or "value" not in data: - logger.warning(f"Invalid memory store request: {data}") - return jsonify({"error": "Missing 'key' or 'value'"}), 400 - key, value = data["key"], data["value"] - if memory_lock.acquire(timeout=LOCK_TIMEOUT): - try: - memory = load_memory_from_file() - memory[key] = value - save_memory_to_file(memory) - logger.info(f"Stored in memory: {key} = {value}") - return jsonify({"status": "stored"}), 200 - finally: - memory_lock.release() - else: - logger.error( - f"Failed to acquire lock for storing {key} within {LOCK_TIMEOUT} seconds" - ) - return jsonify({"error": "Memory lock timeout"}), 503 - - -@app.route("/memory/", methods=["GET"]) -def get_memory(key): - logger.info(f"Received /memory/{key} GET request") - if memory_lock.acquire(timeout=LOCK_TIMEOUT): - try: - memory = load_memory_from_file() - value = memory.get(key) - logger.debug(f"Retrieved memory for {key}: {value}") - return jsonify({"value": value}), 200 - finally: - memory_lock.release() - else: - logger.error( - f"Failed to acquire lock for retrieving {key} within {LOCK_TIMEOUT} seconds" - ) - return jsonify({"error": "Memory lock timeout"}), 503 - - -@app.route("/memory", methods=["GET"]) -def list_memory(): - logger.info("Received /memory GET request") - if memory_lock.acquire(timeout=LOCK_TIMEOUT): - try: - memory = load_memory_from_file() - keys = list(memory.keys()) - logger.debug(f"Memory keys: {keys}") - return jsonify({"keys": keys}), 200 - finally: - memory_lock.release() - else: - logger.error( - f"Failed to acquire lock for listing memory within {LOCK_TIMEOUT} seconds" - ) - return jsonify({"error": "Memory lock timeout"}), 503 - - -@app.route("/memory/", methods=["DELETE"]) -def delete_memory(key): - logger.info(f"Received /memory/{key} DELETE request") - if memory_lock.acquire(timeout=LOCK_TIMEOUT): - try: - memory = load_memory_from_file() - if key not in memory: - logger.warning(f"Key not found for deletion: {key}") - return jsonify({"error": "Key not found"}), 404 - del memory[key] - save_memory_to_file(memory) - logger.info(f"Deleted from memory: {key}") - return jsonify({"status": "deleted"}), 200 - finally: - memory_lock.release() - else: - logger.error( - f"Failed to acquire lock for deleting {key} within {LOCK_TIMEOUT} seconds" - ) - return jsonify({"error": "Memory lock timeout"}), 503 - - -@app.route("/memory/query", methods=["POST"]) -def query_memory(): - logger.info("Received /memory/query POST request") - data = request.json - if not data or "query" not in data: - return jsonify({"error": "Query is required"}), 400 - - query = data["query"].lower() - filter_params = data.get("filter", {}) - - if memory_lock.acquire(timeout=LOCK_TIMEOUT): - try: - memory = load_memory_from_file() - results = [] - - for key, value in memory.items(): - # Apply prefix filter if specified - if "key_prefix" in filter_params and not key.startswith(filter_params["key_prefix"]): - continue - - # Simple scoring - score = 0 - if query in key.lower(): - score += 0.8 - if query in str(value).lower(): - score += 0.6 - - if score > 0: - results.append({ - "key": key, - "value": value, - "score": min(score, 1.0) - }) - - results.sort(key=lambda x: x["score"], reverse=True) - return jsonify({"results": results}), 200 - finally: - memory_lock.release() - else: - return jsonify({"error": "Memory lock timeout"}), 503 - - -# Chat endpoint with AI tool calling capability -# Chat endpoint with AI tool calling capability -@app.route("/chat", methods=["POST"]) -def chat(): - logger.info("Received /chat request") - data = request.json - logger.debug(f"Request data: {data}") - message = data["messages"][0]["content"] if data.get("messages") else "nothing" - model_id = data.get("model") or ( - list(MODEL_CLIENT_MAP.keys())[0] if MODEL_CLIENT_MAP else None - ) - enable_tool_calling = data.get("enable_tool_calling", True) - user_id = data.get("user_id", "ai_user") - - logger.info(f"Selected model: {model_id}, message: {message}, tool_calling: {enable_tool_calling}") - if not model_id or model_id not in MODEL_CLIENT_MAP: - logger.error(f"Invalid or missing model_id: {model_id}") - return jsonify({"error": "Model not found"}), 404 - client = MODEL_CLIENT_MAP[model_id] - - try: - # Define available tools for AI - available_tools = [] - - # Add safe regular tools - safe_tools = ["calculator", "greet"] - for tool_id in safe_tools: - if tool_id in tools: - tool = tools[tool_id] - available_tools.append({ - "type": "function", - "function": { - "name": tool_id, - "description": tool["description"], - "parameters": { - "type": "object", - "properties": { - arg["name"]: { - "type": "string" if arg["type"] == "str" else "integer" if arg["type"] == "int" else "boolean", - "description": arg["description"] - } - for arg in tool.get("arguments", []) - }, - "required": [arg["name"] for arg in tool.get("arguments", []) if not arg.get("optional", False)] - } - } - }) - - # Add safe terminal tools - safe_terminal_tools = ["sysinfo", "pwd", "sandbox_python", "sandbox_exec", "file_read", "directory_list"] - for tool_id in safe_terminal_tools: - if tool_id in terminal_tools: - tool = terminal_tools[tool_id] - available_tools.append({ - "type": "function", - "function": { - "name": tool_id, - "description": tool.description, - "parameters": { - "type": "object", - "properties": { - arg["name"]: { - "type": "string" if arg["type"] == "str" else "integer" if arg["type"] == "int" else "boolean", - "description": arg["description"] - } - for arg in tool.arguments - }, - "required": [arg["name"] for arg in tool.arguments if not arg.get("optional", False)] - } - } - }) - - # Prepare messages - messages = [ - {"role": m["role"], "content": m["content"]} - for m in data.get("messages", []) - ] or [{"role": "user", "content": message}] - - # Add system message about available tools - if enable_tool_calling and available_tools: - system_message = f"""You have access to these tools: {'; '.join([f"{t['function']['name']}" for t in available_tools])}. -When a user asks for calculations, use the calculator tool. -When a user asks for system information, use the sysinfo tool. -When a user asks for Python code execution, use the sandbox_python tool. -When a user asks for shell commands, use the exec tool. -When a user asks to list files or directories, use the directory_list tool. -Always provide the user_id parameter as "{user_id}" for terminal tools.""" - - messages.insert(0, {"role": "system", "content": system_message}) - - # Make the API call - FIXED: Never use tool_choice parameter - if enable_tool_calling and available_tools: - logger.info("Making API call with tools (no tool_choice parameter)") - response = client.chat.completions.create( - model=model_id, - messages=messages, - tools=available_tools - ) - else: - logger.info("Making API call without tools") - response = client.chat.completions.create( - model=model_id, - messages=messages - ) - - # Handle tool calls if present - response_message = response.choices[0].message - tool_calls = response_message.tool_calls if hasattr(response_message, 'tool_calls') else None - - if tool_calls: - logger.info(f"AI requested {len(tool_calls)} tool calls") - - # Execute tool calls - tool_results = [] - for tool_call in tool_calls: - tool_name = tool_call.function.name - tool_args = json.loads(tool_call.function.arguments) - - logger.info(f"Executing tool: {tool_name} with args: {tool_args}") - - # Add user_id for terminal tools - if tool_name in ["sysinfo", "pwd", "sandbox_python", "sandbox_exec", "exec", "file_read", "directory_list"]: - tool_args["user_id"] = user_id - - # Execute the tool - try: - if tool_name in tools: - # Regular tool - result = tools[tool_name]["execute"](tool_args) - tool_results.append({ - "tool_call_id": tool_call.id, - "role": "tool", - "name": tool_name, - "content": json.dumps(result) - }) - elif tool_name in terminal_tools: - # Terminal tool - result = terminal_tools[tool_name].execute(tool_args) - tool_results.append({ - "tool_call_id": tool_call.id, - "role": "tool", - "name": tool_name, - "content": json.dumps(asdict(result)) - }) - else: - logger.warning(f"Unknown tool: {tool_name}") - tool_results.append({ - "tool_call_id": tool_call.id, - "role": "tool", - "name": tool_name, - "content": json.dumps({"error": f"Unknown tool: {tool_name}"}) - }) - except Exception as e: - logger.error(f"Error executing tool {tool_name}: {str(e)}") - tool_results.append({ - "tool_call_id": tool_call.id, - "role": "tool", - "name": tool_name, - "content": json.dumps({"error": str(e)}) - }) - - # Get final response with tool results - FIXED: No tool_choice parameter - final_messages = messages + [response_message] + tool_results - logger.info("Making final API call with tool results") - final_response = client.chat.completions.create( - model=model_id, - messages=final_messages - ) - response_content = final_response.choices[0].message.content - else: - response_content = response_message.content - - logger.debug(f"Chat response from {model_id}: {response_content}") - return ( - jsonify({"choices": [{"message": {"content": response_content}}]}), - 200, - ) - except Exception as e: - logger.error(f"Chat error with model {model_id}: {str(e)}", exc_info=True) - return jsonify({"error": str(e)}), 500 - - -@app.route("/models", methods=["GET"]) -def list_models(): - logger.info("Received /models request") - models = list(MODEL_CLIENT_MAP.keys()) - logger.debug(f"Returning models: {models}") - return jsonify({"models": models}), 200 - - -@app.route("/tools", methods=["GET"]) -def list_tools(): - logger.info("Received /tools request") - - # Combine regular tools and terminal tools - all_tools = [] - - # Add regular tools (no category) - for k, v in tools.items(): - all_tools.append({ - "id": k, - "description": v["description"], - "arguments": v.get("arguments", []) - }) - - # Add terminal tools (with category) - for k, v in terminal_tools.items(): - all_tools.append({ - "id": k, - "description": v.description, - "arguments": v.arguments, - "category": "terminal" - }) - - logger.debug(f"Returning tools: {all_tools}") - return jsonify({"tools": all_tools}), 200 - - -@app.route("/tools/", methods=["GET"]) -def get_tool(tool_id): - logger.info(f"Received /tools/{tool_id} GET request") - - # Check regular tools first - if tool_id in tools: - tool = tools[tool_id] - return jsonify({ - "id": tool_id, - "description": tool["description"], - "arguments": tool.get("arguments", []) - }), 200 - - # Check terminal tools - if tool_id in terminal_tools: - tool = terminal_tools[tool_id] - return jsonify({ - "id": tool.tool_id, - "description": tool.description, - "arguments": tool.arguments, - "category": "terminal" - }), 200 - - logger.error(f"Tool not found: {tool_id}") - return jsonify({"error": "Tool not found"}), 404 - - -@app.route("/tools/", methods=["POST"]) -def use_tool(tool_id): - logger.info(f"Received /tools/{tool_id} POST request") - data = request.json or {} - parameters = data.get("parameters", data) # Support both formats - logger.debug(f"Tool {tool_id} input data: {parameters}") - - # Security check: require user_id if configured - if SECURITY_CONFIG["require_user_id"] and "user_id" not in parameters: - # Exception for tools that don't need user_id - tools_without_user_id = ["calculator", "greet", "pwd", "sysinfo"] - if tool_id not in tools_without_user_id: - logger.warning(f"Missing user_id for tool {tool_id}") - return jsonify({"error": "user_id parameter is required for this tool"}), 400 - - # Check regular tools first - if tool_id in tools: - tool = tools[tool_id] - # Validate required arguments - if "arguments" in tool: - for arg in tool["arguments"]: - if arg["name"] not in parameters and not arg.get("optional", False): - logger.warning(f"Missing '{arg['name']}' for {tool_id} tool") - return jsonify({"error": f"Missing '{arg['name']}' parameter"}), 400 - - try: - result = tool["execute"](parameters) - logger.debug(f"Tool {tool_id} result: {result}") - return jsonify(result), 200 - except Exception as e: - logger.error(f"Error executing tool {tool_id}: {str(e)}", exc_info=True) - return jsonify({"error": str(e)}), 500 - - # Check terminal tools - if tool_id in terminal_tools: - tool = terminal_tools[tool_id] - # Validate required arguments - for arg in tool.arguments: - if arg["name"] not in parameters and not arg.get("optional", False): - logger.warning(f"Missing '{arg['name']}' for {tool_id} tool") - return jsonify({"error": f"Missing '{arg['name']}' parameter"}), 400 - - try: - result = tool.execute(parameters) - logger.debug(f"Terminal tool {tool_id} result: {result}") - return jsonify(asdict(result)), 200 - except Exception as e: - logger.error(f"Error executing terminal tool {tool_id}: {str(e)}", exc_info=True) - return jsonify({"error": str(e)}), 500 - - logger.error(f"Tool not found: {tool_id}") - return jsonify({"error": "Tool not found"}), 404 - - -@app.route("/resources", methods=["GET"]) -def list_resources(): - logger.info("Received /resources request") - with resource_lock: - resources = load_resources_from_file() - resource_list = list(resources.values()) - logger.debug(f"Returning resources: {resource_list}") - return jsonify({"resources": resource_list}), 200 - - -@app.route("/resources/", methods=["GET"]) -def get_resource(resource_id): - logger.info(f"Received /resources/{resource_id} GET request") - with resource_lock: - resources = load_resources_from_file() - if resource_id in resources: - resource = resources[resource_id] - logger.debug(f"Exact match found for {resource_id}: {resource}") - return jsonify(resource), 200 - else: - # Prefix search for nested resources - prefix = f"{resource_id}/" - matching_resources = [ - res for key, res in resources.items() if key.startswith(prefix) - ] - if matching_resources: - logger.debug( - f"Prefix search for {resource_id} found: {matching_resources}" - ) - return jsonify({"resources": matching_resources}), 200 - logger.warning(f"No resource or nested resources found for {resource_id}") - return jsonify({"error": "Resource not found"}), 404 - - -@app.route("/resources", methods=["POST"]) -def create_resource(): - logger.info("Received /resources POST request") - data = request.json - if not data or "id" not in data: - return jsonify({"error": "Resource ID is required"}), 400 - - resource_id = data["id"] - with resource_lock: - resources = load_resources_from_file() - if resource_id in resources: - return jsonify({"error": "Resource already exists"}), 409 - - resource = { - "id": resource_id, - "title": data.get("title", resource_id), - "type": data.get("type", "file"), - "content": data.get("content", ""), - "metadata": { - **data.get("metadata", {}), - "created_at": datetime.now().isoformat() - } - } - resources[resource_id] = resource - save_resources_to_file(resources) - - return jsonify({"status": "created", "resource": resource}), 201 - - -@app.route("/resources/", methods=["PUT"]) -def update_resource_endpoint(resource_id): - logger.info(f"Received /resources/{resource_id} PUT request") - data = request.json - logger.debug(f"Update resource data: {data}") - if not data or "content" not in data: - logger.warning(f"Invalid resource update request: {data}") - return jsonify({"error": "Missing 'content'"}), 400 - with resource_lock: - resources = load_resources_from_file() - resources[resource_id] = {"id": resource_id, "content": data["content"]} - save_resources_to_file(resources) - logger.info(f"Updated/created resource {resource_id}: {resources[resource_id]}") - return jsonify({"status": "updated", "resource": resources[resource_id]}), 200 - - -@app.route("/resources/", methods=["DELETE"]) -def delete_resource(resource_id): - logger.info(f"Received /resources/{resource_id} DELETE request") - with resource_lock: - resources = load_resources_from_file() - if resource_id not in resources: - return jsonify({"error": "Resource not found"}), 404 - - del resources[resource_id] - save_resources_to_file(resources) - - return jsonify({"status": "deleted", "id": resource_id}), 200 - - -@app.route("/resources/search", methods=["GET"]) -def search_resources(): - logger.info("Received /resources/search request") - query = request.args.get("q", "").lower() - if not query: - return jsonify({"error": "Query parameter 'q' is required"}), 400 - - with resource_lock: - resources = load_resources_from_file() - results = [] - - for resource in resources.values(): - score = 0 - if query in resource.get("title", resource.get("id", "")).lower(): - score += 0.8 - if query in str(resource.get("content", "")).lower(): - score += 0.6 - if query in resource.get("type", "").lower(): - score += 0.4 - - if score > 0: - results.append({ - "id": resource["id"], - "title": resource.get("title", resource["id"]), - "type": resource.get("type", "file"), - "score": min(score, 1.0) - }) - - results.sort(key=lambda x: x["score"], reverse=True) - return jsonify({"results": results}), 200 - - -@app.route("/pay", methods=["POST"]) -def pay(): - logger.info("Received /pay request") - data = request.json - logger.debug(f"Pay request data: {data}") - amount = data.get("amount", 0) - transaction_id = f"tx_{int(datetime.now().timestamp())}" - logger.info(f"Processed payment of {amount} with transaction_id: {transaction_id}") - return ( - jsonify( - { - "transaction_id": transaction_id, - "status": "success", - } - ), - 200, - ) - - -@app.route("/info", methods=["GET"]) -def info(): - logger.info("Received /info request") - return jsonify({ - "name": "SLOP Monolith Server", - "version": "2.0.0", - "description": "A combined SLOP server with gaming, terminal, and AI capabilities", - "endpoints": [ - {"path": "/chat", "methods": ["POST"], "description": "Chat with AI models"}, - {"path": "/models", "methods": ["GET"], "description": "List available AI models"}, - {"path": "/tools", "methods": ["GET", "POST"], "description": "List and execute tools"}, - {"path": "/memory", "methods": ["GET", "POST", "DELETE"], "description": "Memory operations"}, - {"path": "/resources", "methods": ["GET", "POST", "PUT", "DELETE"], "description": "Resource management"}, - {"path": "/pay", "methods": ["POST"], "description": "Payment processing"}, - {"path": "/info", "methods": ["GET"], "description": "Server information"} - ], - "creator": { - "name": "SLOP Community", - "website": "https://slop.ai" - }, - "capabilities": { - "streaming": False, - "websockets": False, - "models": list(MODEL_CLIENT_MAP.keys()), - "tools": { - "total": len(tools) + len(terminal_tools), - "terminal_tools": list(terminal_tools.keys()) - } - }, - "stats": { - "total_tools": len(tools) + len(terminal_tools), - "tools": len(tools), - "terminal_tools": len(terminal_tools), - "available_models": len(MODEL_CLIENT_MAP) - } - }), 200 - - -# Initialize everything -logger.info("Starting application initialization") -initialize_model_map() - -# Initialize sandbox manager -if FIRECRACKER_CONFIG["enable_sandbox"]: - sandbox_manager = SandboxManager(FIRECRACKER_CONFIG) - logger.info("Sandbox manager initialized") -else: - sandbox_manager = None - logger.info("Sandbox manager disabled") - -initialize_terminal_tools() -logger.info("Application initialization completed") - -if __name__ == "__main__": - port = SERVER_CONFIG["port"] - debug = SERVER_CONFIG["debug"] - - logger.info(f"Starting Flask application on port {port}") - if debug: - logger.warning("🚨 DEBUG MODE ENABLED - Do not use in production!") - - # Show security status - logger.info("🔒 Security Configuration:") - logger.info(f" - Firecracker sandbox: {'ENABLED' if FIRECRACKER_CONFIG['enable_sandbox'] else 'DISABLED'}") - logger.info(f" - Dangerous tools: {'ALLOWED' if SECURITY_CONFIG['allow_dangerous_tools'] else 'BLOCKED'}") - logger.info(f" - User ID required: {'YES' if SECURITY_CONFIG['require_user_id'] else 'NO'}") - logger.info(f" - Default timeout: {SECURITY_CONFIG['default_timeout']}s") - - app.run(debug=debug, port=port, host="0.0.0.0") - logger.info("Flask application stopped") \ No newline at end of file diff --git a/slop-terminal/vars.sh b/slop-terminal/vars.sh deleted file mode 100644 index 0f6d30d..0000000 --- a/slop-terminal/vars.sh +++ /dev/null @@ -1,407 +0,0 @@ -#!/bin/bash -# vars.sh - SLOP Monolith Configuration for Linux/WSL2 -# Usage: source vars.sh - -echo "SLOP Monolith Configuration" -echo "============================" - -CURRENT_DIR=$(pwd) - -# OS Detection -if [[ -n "$WSL_DISTRO_NAME" ]]; then - IS_WSL=true - IS_LINUX=true - OS_NAME="WSL2 ($WSL_DISTRO_NAME)" -elif [[ "$(uname -s)" == "Linux" ]]; then - IS_WSL=false - IS_LINUX=true - OS_NAME="Linux" -else - IS_WSL=false - IS_LINUX=false - OS_NAME="Unknown" -fi - -# Architecture Detection -DETECTED_ARCH=$(uname -m) -case $DETECTED_ARCH in - "x86_64") - ARCH="x86_64" - ;; - "aarch64"|"arm64") - ARCH="aarch64" - ;; - *) - ARCH="x86_64" - ;; -esac - -echo "System Info:" -echo " OS: $OS_NAME" -echo " Architecture: $DETECTED_ARCH -> $ARCH" - -# Configuration -FIRECRACKER_VERSION="v1.12.1" -CAN_USE_FIRECRACKER=true - -# URLs -FIRECRACKER_URL="https://github.com/firecracker-microvm/firecracker/releases/download/$FIRECRACKER_VERSION/firecracker-$FIRECRACKER_VERSION-$ARCH.tgz" -KERNEL_URL="https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/$ARCH/kernels/vmlinux.bin" -ROOTFS_URL="https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/$ARCH/rootfs/bionic.rootfs.ext4" - -echo "Firecracker URL: $FIRECRACKER_URL" - -# Create directories -echo "" -echo "Creating directories..." -DIRS=("firecracker" "firecracker/kernel" "firecracker/rootfs" "data" "data/terminal" "downloads" "bin" "Scripts") -for dir in "${DIRS[@]}"; do - mkdir -p "$dir" - echo " Created: $dir" -done - -# Install system dependencies -echo "" -echo "Installing system dependencies..." -if command -v apt-get &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y curl wget tar gzip python3 python3-pip - echo " System packages installed (apt)" -elif command -v yum &> /dev/null; then - sudo yum install -y curl wget tar gzip python3 python3-pip - echo " System packages installed (yum)" -elif command -v dnf &> /dev/null; then - sudo dnf install -y curl wget tar gzip python3 python3-pip - echo " System packages installed (dnf)" -elif command -v pacman &> /dev/null; then - sudo pacman -S --noconfirm curl wget tar gzip python python-pip - echo " System packages installed (pacman)" -else - echo " Package manager not detected - ensure curl, wget, tar, python3 are installed" -fi - -# Download function -download_file() { - local url="$1" - local output="$2" - local description="$3" - - echo "Downloading $description..." - echo " URL: $url" - - if command -v curl &> /dev/null; then - if curl -L -o "$output" "$url" --connect-timeout 30 --max-time 300 --silent --fail; then - local size=$(stat -c%s "$output" 2>/dev/null || stat -f%z "$output" 2>/dev/null || echo "0") - echo " Downloaded: $((size / 1024 / 1024)) MB" - return 0 - else - echo " Download failed with curl" - return 1 - fi - elif command -v wget &> /dev/null; then - if wget -O "$output" "$url" --timeout=300 --quiet; then - local size=$(stat -c%s "$output" 2>/dev/null || stat -f%z "$output" 2>/dev/null || echo "0") - echo " Downloaded: $((size / 1024 / 1024)) MB" - return 0 - else - echo " Download failed with wget" - return 1 - fi - else - echo " Neither curl nor wget available" - return 1 - fi -} - -# Setup Firecracker -echo "" -echo "Setting up Firecracker..." - -# Download binary -if [[ ! -f "bin/firecracker" ]]; then - if download_file "$FIRECRACKER_URL" "downloads/firecracker.tgz" "Firecracker Binary"; then - echo "Extracting Firecracker..." - tar -xzf "downloads/firecracker.tgz" -C "downloads" - - # Find and copy binary - BINARY_PATH=$(find downloads -name "firecracker-*" -type f | grep -v "\.tgz$" | head -1) - if [[ -n "$BINARY_PATH" ]]; then - cp "$BINARY_PATH" "bin/firecracker" - chmod +x "bin/firecracker" - echo " Firecracker binary ready" - else - echo " Failed to find firecracker binary in archive" - fi - else - echo " Failed to download Firecracker binary" - fi -else - echo " Firecracker binary already exists" - chmod +x "bin/firecracker" -fi - -# Download kernel -if [[ ! -f "firecracker/kernel/vmlinux.bin" ]] || [[ $(stat -c%s "firecracker/kernel/vmlinux.bin" 2>/dev/null || echo "0") -lt 1024 ]]; then - if ! download_file "$KERNEL_URL" "firecracker/kernel/vmlinux.bin" "Kernel"; then - echo " Download failed, creating placeholder..." - dd if=/dev/zero of="firecracker/kernel/vmlinux.bin" bs=1M count=10 2>/dev/null - echo " Kernel placeholder created" - fi -else - echo " Kernel already exists" -fi - -# Download rootfs -if [[ ! -f "firecracker/rootfs/rootfs.ext4" ]] || [[ $(stat -c%s "firecracker/rootfs/rootfs.ext4" 2>/dev/null || echo "0") -lt 1024 ]]; then - if ! download_file "$ROOTFS_URL" "firecracker/rootfs/rootfs.ext4" "Root Filesystem"; then - echo " Download failed, creating placeholder..." - dd if=/dev/zero of="firecracker/rootfs/rootfs.ext4" bs=1M count=100 2>/dev/null - echo " Rootfs placeholder created" - fi -else - echo " Rootfs already exists" -fi - -# Check and fix KVM -echo "Checking KVM support..." -if [[ -c "/dev/kvm" ]]; then - if [[ -w "/dev/kvm" ]]; then - echo " KVM available and writable" - KVM_AVAILABLE=true - else - echo " KVM exists but not writable - fixing automatically..." - - # Check if user is already in kvm group - if groups $USER | grep -q '\bkvm\b'; then - echo " User already in kvm group - permissions should work after restart" - KVM_AVAILABLE=true - else - echo " Adding user to kvm group..." - if sudo usermod -a -G kvm $USER; then - echo " User added to kvm group successfully" - echo " KVM permissions fixed!" - echo " Note: You may need to restart your terminal for full effect" - KVM_AVAILABLE=true - else - echo " Failed to add user to kvm group" - KVM_AVAILABLE=false - fi - fi - fi -else - echo " KVM device not found - installing..." - if sudo apt-get update && sudo apt-get install -y qemu-kvm; then - echo " KVM installed successfully" - # After installation, add user to kvm group - if sudo usermod -a -G kvm $USER; then - echo " User added to kvm group" - KVM_AVAILABLE=true - echo " KVM setup complete!" - echo " Note: You may need to restart your terminal for full effect" - else - echo " Failed to add user to kvm group" - KVM_AVAILABLE=false - fi - else - echo " Failed to install KVM" - KVM_AVAILABLE=false - fi -fi - -# Test Firecracker and final verification -echo "Testing Firecracker setup..." -FIRECRACKER_WORKS=false -if [[ -f "bin/firecracker" ]]; then - if ./bin/firecracker --version &>/dev/null; then - echo " Firecracker binary works" - FIRECRACKER_WORKS=true - else - echo " Firecracker binary failed to run" - fi -fi - -# Final KVM test after potential fixes -echo "Final KVM verification..." -if [[ -c "/dev/kvm" ]] && [[ -w "/dev/kvm" ]]; then - echo " KVM fully functional" - KVM_AVAILABLE=true -elif [[ -c "/dev/kvm" ]]; then - echo " KVM device exists - permissions may need terminal restart" - KVM_AVAILABLE=true # Mark as available since we fixed the group membership -else - echo " KVM not available" - KVM_AVAILABLE=false -fi - -# Determine Firecracker status -if [[ "$FIRECRACKER_WORKS" == true ]] && [[ -f "firecracker/kernel/vmlinux.bin" ]] && [[ -f "firecracker/rootfs/rootfs.ext4" ]]; then - FIRECRACKER_READY=true -else - FIRECRACKER_READY=false -fi - -# Install Python dependencies -echo "" -echo "Installing Python dependencies..." -if command -v pip3 &> /dev/null; then - pip3 install flask flask-swagger-ui openai expr.py psutil - echo " Python packages installed" -elif command -v pip &> /dev/null; then - pip install flask flask-swagger-ui openai expr.py psutil - echo " Python packages installed" -else - echo " pip not found - install manually" -fi - -# Set Environment Variables -echo "" -echo "Setting environment variables..." - -export FIRECRACKER_KERNEL="$CURRENT_DIR/firecracker/kernel/vmlinux.bin" -export FIRECRACKER_ROOTFS="$CURRENT_DIR/firecracker/rootfs/rootfs.ext4" -export FIRECRACKER_MEMORY="512" -export FIRECRACKER_VCPU="1" - -export MODEL_ENDPOINT_0="https://hermes.ai.unturf.com/v1" -export MODEL_API_KEY_0="not-needed" - -export SLOP_PORT="31337" -export SLOP_DATA_DIR="$CURRENT_DIR/data" -export SLOP_LOG_LEVEL="INFO" -export FLASK_DEBUG="true" - -export REQUIRE_USER_ID="true" -export DEFAULT_TIMEOUT="30" - -if [[ "$FIRECRACKER_READY" == true ]]; then - export ENABLE_FIRECRACKER="true" - export ALLOW_DANGEROUS_TOOLS="false" - STATUS="ENABLED" -else - export ENABLE_FIRECRACKER="false" - export ALLOW_DANGEROUS_TOOLS="true" - STATUS="DISABLED" -fi - -# Create helper scripts -echo "Creating helper scripts..." - -# Create firecracker wrapper -cat > Scripts/start_firecracker.sh << 'EOF' -#!/bin/bash -export PATH="$(pwd)/bin:$PATH" -cd "$(dirname "$0")" -./bin/firecracker "$@" -EOF -chmod +x Scripts/start_firecracker.sh -echo " Created start_firecracker.sh" - -# Create test script -cat > Scripts/test_firecracker.sh << 'EOF' -#!/bin/bash -echo "Testing Firecracker setup..." -echo "Binary: $(pwd)/bin/firecracker" -echo "Kernel: $(pwd)/firecracker/kernel/vmlinux.bin" -echo "Rootfs: $(pwd)/firecracker/rootfs/rootfs.ext4" -echo "" - -if [[ -f "bin/firecracker" ]]; then - echo "✓ Binary exists" - if [[ -x "bin/firecracker" ]]; then - echo "✓ Binary is executable" - if ./bin/firecracker --version 2>/dev/null; then - echo "✓ Binary works correctly" - else - echo "✗ Binary failed to run" - fi - else - echo "✗ Binary not executable" - fi -else - echo "✗ Binary missing" -fi - -if [[ -f "firecracker/kernel/vmlinux.bin" ]]; then - size=$(stat -c%s firecracker/kernel/vmlinux.bin 2>/dev/null || stat -f%z firecracker/kernel/vmlinux.bin 2>/dev/null || echo 0) - echo "✓ Kernel exists ($((size / 1024 / 1024)) MB)" -else - echo "✗ Kernel missing" -fi - -if [[ -f "firecracker/rootfs/rootfs.ext4" ]]; then - size=$(stat -c%s firecracker/rootfs/rootfs.ext4 2>/dev/null || stat -f%z firecracker/rootfs/rootfs.ext4 2>/dev/null || echo 0) - echo "✓ Rootfs exists ($((size / 1024 / 1024)) MB)" -else - echo "✗ Rootfs missing" -fi - -if [[ -c "/dev/kvm" ]]; then - if [[ -w "/dev/kvm" ]]; then - echo "✓ KVM available and writable" - else - echo "⚠ KVM exists but not writable" - fi -else - echo "✗ KVM not available" -fi -EOF -chmod +x Scripts/test_firecracker.sh -echo " Created test_firecracker.sh" - -# Create permanent environment file -cat > Scripts/set_env.sh << EOF -#!/bin/bash -# SLOP Environment Variables -export FIRECRACKER_KERNEL="$CURRENT_DIR/firecracker/kernel/vmlinux.bin" -export FIRECRACKER_ROOTFS="$CURRENT_DIR/firecracker/rootfs/rootfs.ext4" -export FIRECRACKER_MEMORY="512" -export FIRECRACKER_VCPU="1" -export MODEL_ENDPOINT_0="https://hermes.ai.unturf.com/v1" -export MODEL_API_KEY_0="not-needed" -export SLOP_PORT="31337" -export SLOP_DATA_DIR="$CURRENT_DIR/data" -export SLOP_LOG_LEVEL="INFO" -export FLASK_DEBUG="true" -export REQUIRE_USER_ID="true" -export DEFAULT_TIMEOUT="30" -export ENABLE_FIRECRACKER="$ENABLE_FIRECRACKER" -export ALLOW_DANGEROUS_TOOLS="$ALLOW_DANGEROUS_TOOLS" -EOF -chmod +x Scripts/set_env.sh -echo " Created set_env.sh" - -# Final Status -echo "" -echo "Configuration Summary:" -echo "======================" -echo " System: $OS_NAME ($ARCH)" -echo " Firecracker: $STATUS" -echo " Port: $SLOP_PORT" -echo " Data Dir: $SLOP_DATA_DIR" - -echo "" -echo "Files Status:" -echo " bin/firecracker: $(test -f 'bin/firecracker' && echo 'OK' || echo 'MISSING')" -echo " kernel: $(test -f 'firecracker/kernel/vmlinux.bin' && echo 'OK' || echo 'MISSING')" -echo " rootfs: $(test -f 'firecracker/rootfs/rootfs.ext4' && echo 'OK' || echo 'MISSING')" -echo " KVM: $(test -c '/dev/kvm' && test -w '/dev/kvm' && echo 'OK' || echo 'MISSING')" - -echo "" -echo "Next Steps:" -echo " 1. Test setup: ./Scripts/test_firecracker.sh" -echo " 2. Start server: python3 server.py" -if [[ "$KVM_AVAILABLE" == true ]] && [[ ! -w "/dev/kvm" ]]; then - echo " 3. If KVM errors occur, restart your terminal" -fi -echo " 4. Test endpoints:" -echo " curl http://localhost:31337/models" -echo " curl http://localhost:31337/tools" - -echo "" -echo "Configuration complete!" -if [[ "$KVM_AVAILABLE" == true ]] && [[ ! -w "/dev/kvm" ]]; then - echo "KVM permissions were fixed - you may need to restart your terminal" - echo "Then run: python3 server.py" -else - echo "Run: python3 server.py" -fi \ No newline at end of file diff --git a/slop_with_models.py b/slop_with_models.py index 2b0e69d..da8cbec 100644 --- a/slop_with_models.py +++ b/slop_with_models.py @@ -1,4 +1,4 @@ -from flask import Flask, request, jsonify +from flask import Flask, request, jsonify, render_template_string from datetime import datetime import os import json @@ -687,6 +687,99 @@ def play_blackjack(bet, agent_id, game_id=None, action=None): LOCK_TIMEOUT = 2 # Seconds to wait for lock acquisition +@app.route("/", methods=["GET"]) +def index(): + logger.info("Received / GET request") + html_template = """ + + + + SLOP API Server + + + +

SLOP API Server

+

Welcome to the SLOP API Server v1.0

+ + + +
+

API Endpoints:

+
    +
  • /memory - Memory storage endpoints
  • +
  • /chat - Chat completion endpoint
  • +
  • /models - List available models
  • +
  • /tools - Tool execution endpoints
  • +
  • /resources - Resource management
  • +
  • /pay - Payment processing
  • +
+
+ + + """ + return render_template_string(html_template) + + @app.route("/memory", methods=["POST"]) def store_memory(): logger.info("Received /memory POST request")