diff --git a/slop-terminal/.gitignore b/slop-terminal/.gitignore new file mode 100644 index 0000000..cdac1bf --- /dev/null +++ b/slop-terminal/.gitignore @@ -0,0 +1,54 @@ +# 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 new file mode 100644 index 0000000..e69de29 diff --git a/slop-terminal/.gitlab.ci.yml b/slop-terminal/.gitlab.ci.yml new file mode 100644 index 0000000..e0b3b66 --- /dev/null +++ b/slop-terminal/.gitlab.ci.yml @@ -0,0 +1,72 @@ +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 new file mode 100644 index 0000000..5d02807 --- /dev/null +++ b/slop-terminal/Dockerfile @@ -0,0 +1,43 @@ +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 new file mode 100644 index 0000000..cd0c939 --- /dev/null +++ b/slop-terminal/Makefile @@ -0,0 +1,39 @@ +.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 new file mode 100644 index 0000000..1f8b77f --- /dev/null +++ b/slop-terminal/README.md @@ -0,0 +1,388 @@ +# 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 new file mode 100644 index 0000000..a5febb1 --- /dev/null +++ b/slop-terminal/config/.env.example @@ -0,0 +1,23 @@ +# 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 new file mode 100644 index 0000000..e69de29 diff --git a/slop-terminal/docker-compose.yml b/slop-terminal/docker-compose.yml new file mode 100644 index 0000000..f5bcd9c --- /dev/null +++ b/slop-terminal/docker-compose.yml @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..ff21ec1 --- /dev/null +++ b/slop-terminal/requirements.txt @@ -0,0 +1,18 @@ +# 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 new file mode 100644 index 0000000..385bbaf --- /dev/null +++ b/slop-terminal/slop_with_terminal.py @@ -0,0 +1,2182 @@ +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 new file mode 100644 index 0000000..0f6d30d --- /dev/null +++ b/slop-terminal/vars.sh @@ -0,0 +1,407 @@ +#!/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 da8cbec..2b0e69d 100644 --- a/slop_with_models.py +++ b/slop_with_models.py @@ -1,4 +1,4 @@ -from flask import Flask, request, jsonify, render_template_string +from flask import Flask, request, jsonify from datetime import datetime import os import json @@ -687,99 +687,6 @@ 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")