diff --git a/Dockerfile b/Dockerfile
index 1343aae..726c847 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -97,10 +97,9 @@ EXPOSE 8222
# Start the FastAPI application
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002", "--workers", "1", "--no-access-log"]
-RUN apt-get --fix-broken install
# Ensure Node.js, npm (and npx) are set up
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
-RUN apt-get install -y nodejs
+RUN apt-get update && apt-get install -y nodejs
diff --git a/README.md b/README.md
index 4dabf1a..990b499 100644
--- a/README.md
+++ b/README.md
@@ -86,7 +86,41 @@ pip install -r examples/requirements.txt
You can now ask Claude to execute code, and it will run safely in the sandbox!
-### Option 2: Python OpenAI Agents
+### Option 2: Claude Code CLI
+
+
+Use CodeRunner with Claude Code CLI for terminal-based AI assistance:
+
+
+
+**Quick Start:**
+
+```bash
+# 1. Install and start CodeRunner (one-time setup)
+git clone https://github.com/instavm/coderunner.git
+cd coderunner
+sudo ./install.sh
+
+# 2. Install the Claude Code plugin
+claude plugin marketplace add github:BandarLabs/coderunner/instavm-coderunner-plugin
+claude plugin install instavm-coderunner@instavm-plugins
+
+# 3. Reconnect to MCP servers
+/mcp
+```
+
+That's it! Claude Code now has access to all CodeRunner tools:
+- **execute_python_code** - Run Python code in persistent Jupyter kernel
+- **navigate_and_get_all_visible_text** - Web scraping with Playwright
+- **list_skills** - List available skills (docx, xlsx, pptx, pdf, image processing, etc.)
+- **get_skill_info** - Get documentation for specific skills
+- **get_skill_file** - Read skill files and examples
+
+**Learn more:** See [instavm-coderunner-plugin/README.md](instavm-coderunner-plugin/README.md) for detailed documentation.
+
+
+
+### Option 3: Python OpenAI Agents
Use CodeRunner with OpenAI's Python agents library:
@@ -106,7 +140,7 @@ pip install -r examples/requirements.txt
Enter prompts like "write python code to generate 100 prime numbers" and watch it execute safely in the sandbox!
-### Option 3: Gemini-CLI
+### Option 4: Gemini-CLI
[Gemini CLI](https://github.com/google-gemini/gemini-cli) is recently launched by Google.
@@ -132,7 +166,7 @@ pip install -r examples/requirements.txt

-### Option 4: Kiro by Amazon
+### Option 5: Kiro by Amazon
[Kiro](https://kiro.dev/blog/introducing-kiro/) is recently launched by Amazon.
@@ -161,7 +195,7 @@ pip install -r examples/requirements.txt
-### Option 5: Coderunner-UI (Offline AI Workspace)
+### Option 6: Coderunner-UI (Offline AI Workspace)
[Coderunner-UI](https://github.com/instavm/coderunner-ui) is our own offline AI workspace tool designed for full privacy and local processing.
@@ -256,6 +290,10 @@ The `examples/` directory contains:
- `openai-agents` - Example OpenAI agents integration
- `claude-desktop` - Example Claude Desktop integration
+## Building Container Image Tutorial
+
+https://github.com/apple/container/blob/main/docs/tutorial.md
+
## Contributing
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
diff --git a/install.sh b/install.sh
index 93d4047..8f02327 100755
--- a/install.sh
+++ b/install.sh
@@ -19,7 +19,7 @@ else
echo "✅ macOS system detected."
fi
-download_url="https://github.com/apple/container/releases/download/0.5.0/container-0.5.0-installer-signed.pkg"
+download_url="https://github.com/apple/container/releases/download/0.8.0/container-installer-signed.pkg"
# Check if container is installed and display its version
if command -v container &> /dev/null
diff --git a/instavm-coderunner-plugin/.claude-plugin/marketplace.json b/instavm-coderunner-plugin/.claude-plugin/marketplace.json
new file mode 100644
index 0000000..c081e37
--- /dev/null
+++ b/instavm-coderunner-plugin/.claude-plugin/marketplace.json
@@ -0,0 +1,13 @@
+{
+ "name": "instavm-plugins",
+ "owner": {
+ "name": "InstaVM"
+ },
+ "plugins": [
+ {
+ "name": "instavm-coderunner",
+ "source": ".",
+ "description": "Execute Python code in a local sandboxed Apple container with fast kernel pool performance"
+ }
+ ]
+}
diff --git a/instavm-coderunner-plugin/.claude-plugin/plugin.json b/instavm-coderunner-plugin/.claude-plugin/plugin.json
new file mode 100644
index 0000000..c8494ad
--- /dev/null
+++ b/instavm-coderunner-plugin/.claude-plugin/plugin.json
@@ -0,0 +1,10 @@
+{
+ "name": "instavm-coderunner",
+ "version": "1.0.0",
+ "description": "Execute Python code in a sandboxed Apple container via local CodeRunner MCP server",
+ "author": {
+ "name": "InstaVM"
+ },
+ "homepage": "https://github.com/instavm/coderunner",
+ "license": "MIT"
+}
diff --git a/instavm-coderunner-plugin/.mcp.json b/instavm-coderunner-plugin/.mcp.json
new file mode 100644
index 0000000..e807d64
--- /dev/null
+++ b/instavm-coderunner-plugin/.mcp.json
@@ -0,0 +1,8 @@
+{
+ "mcpServers": {
+ "instavm-coderunner": {
+ "command": "python3",
+ "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp-proxy.py"]
+ }
+ }
+}
diff --git a/instavm-coderunner-plugin/README.md b/instavm-coderunner-plugin/README.md
new file mode 100644
index 0000000..0127935
--- /dev/null
+++ b/instavm-coderunner-plugin/README.md
@@ -0,0 +1,258 @@
+# InstaVM CodeRunner Plugin for Claude Code
+
+This plugin enables Claude Code to execute Python code in a local sandboxed Apple container using [InstaVM CodeRunner](https://github.com/instavm/coderunner) via the Model Context Protocol (MCP).
+
+## Quick Start
+
+```bash
+# 1. Install and start CodeRunner (one-time setup)
+git clone https://github.com/instavm/coderunner.git
+cd coderunner
+sudo ./install.sh
+
+# 2. Install the Claude Code plugin
+claude plugin marketplace add github:BandarLabs/coderunner/instavm-coderunner-plugin
+claude plugin install instavm-coderunner@instavm-plugins
+
+# 3. Reconnect to MCP servers
+/mcp
+```
+
+That's it! Claude Code now has access to all CodeRunner tools.
+
+## Installation
+
+### Prerequisites
+
+**IMPORTANT:** You must have CodeRunner installed and running **before** installing this plugin.
+
+#### Install CodeRunner
+
+```bash
+git clone https://github.com/instavm/coderunner.git
+cd coderunner
+sudo ./install.sh
+```
+
+This will:
+- Install Apple's container runtime (version 0.8.0+)
+- Pull the `instavm/coderunner` container image
+- Start the CodeRunner MCP service at `http://coderunner.local:8222/mcp`
+
+**Verify CodeRunner is running:**
+```bash
+curl http://coderunner.local:8222/execute -X POST -H "Content-Type: application/json" -d '{"code":"print(\"test\")"}'
+```
+
+### Install via GitHub URL (Recommended)
+
+```bash
+# Add the InstaVM marketplace from GitHub
+claude plugin marketplace add github:BandarLabs/coderunner/instavm-coderunner-plugin
+
+# Install the plugin
+claude plugin install instavm-coderunner@instavm-plugins
+```
+
+### Option 2: Install from Local Path
+
+If you've cloned the repository locally:
+
+```bash
+# Add local marketplace
+claude plugin marketplace add /path/to/coderunner/instavm-coderunner-plugin
+
+# Install the plugin
+claude plugin install instavm-coderunner@instavm-plugins
+```
+
+## Usage
+
+Once installed, Claude will have access to the `execute_python_code` tool from CodeRunner. You can ask Claude to execute Python code:
+
+```
+Please execute this Python code:
+```python
+import math
+print(f"The square root of 16 is {math.sqrt(16)}")
+```
+```
+
+Or simply:
+```
+Execute this code: print("Hello from CodeRunner!")
+```
+
+## Available Tools
+
+The plugin exposes the following MCP tools from CodeRunner:
+
+- **execute_python_code**: Execute Python code in a persistent Jupyter kernel with full stdout/stderr capture
+- **navigate_and_get_all_visible_text**: Web scraping using Playwright - navigate to URLs and extract visible text
+- **list_skills**: List all available skills (both public and user-added) in CodeRunner
+- **get_skill_info**: Get documentation for a specific skill (reads SKILL.md)
+- **get_skill_file**: Read any file from a skill's directory (e.g., EXAMPLES.md, API.md)
+
+## Features
+
+- **Local Execution**: Code runs on your machine in a sandboxed container
+- **No Cloud Uploads**: Process files locally without uploading to cloud services
+- **Fast Performance**: Pre-warmed Jupyter kernel pool (2-5 kernels) for quick execution
+- **Full Output**: Returns stdout, stderr, execution time, and CPU time
+- **Security**: Runs in Apple container isolation
+- **MCP Integration**: Uses standard Model Context Protocol for tool communication
+
+## Configuration
+
+By default, the plugin connects to `http://coderunner.local:8222/mcp` via a stdio proxy. To customize the URL, set the `MCP_URL` environment variable in `.mcp.json`:
+
+```json
+{
+ "mcpServers": {
+ "instavm-coderunner": {
+ "command": "python3",
+ "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp-proxy.py"],
+ "env": {
+ "MCP_URL": "http://your-custom-url:8222/mcp"
+ }
+ }
+ }
+}
+```
+
+## Example Output
+
+```python
+# Code:
+print("Hello from CodeRunner!")
+import time
+time.sleep(0.1)
+for i in range(3):
+ print(f"Count: {i}")
+```
+
+**Result:**
+```
+Hello from CodeRunner!
+Count: 0
+Count: 1
+Count: 2
+
+Execution time: 0.156s
+```
+
+## Requirements
+
+- macOS 26.0+ (recommended) for Apple Container support
+- Python 3.10+
+- CodeRunner installed and running
+- Claude Code with MCP plugin support
+
+## Troubleshooting
+
+### Plugin shows "failed" status
+
+**Most common cause:** CodeRunner container is not running.
+
+**Solution:**
+```bash
+# Check if CodeRunner is running
+curl http://coderunner.local:8222/execute -X POST -H "Content-Type: application/json" -d '{"code":"print(\"test\")"}'
+
+# If not running, restart it:
+cd /path/to/coderunner
+sudo ./install.sh
+```
+
+### "Invalid Host header" error
+
+The CodeRunner MCP server is running but rejecting requests. This means the container needs to be restarted with proper hostname configuration.
+
+**Solution:**
+```bash
+# Stop existing containers
+sudo pkill -f container
+container system start
+
+# Restart CodeRunner container
+container run --name coderunner --detach --rm --cpus 8 --memory 4g \
+ --volume "$HOME/.coderunner/assets/skills/user:/app/uploads/skills/user" \
+ --volume "$HOME/.coderunner/outputs:/app/uploads/outputs" \
+ instavm/coderunner
+```
+
+### MCP connection errors
+
+1. **Check MCP logs:**
+ ```bash
+ cat ~/Library/Caches/claude-cli-nodejs/-Users-manish-coderunner-instavm-coderunner-plugin/mcp-logs-*/latest/*.jsonl
+ ```
+
+2. **Test MCP endpoint manually:**
+ ```bash
+ curl -H "Host: coderunner.local:8222" http://coderunner.local:8222/mcp \
+ -X POST -H "Content-Type: application/json" \
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}'
+ ```
+
+3. **Verify proxy script:**
+ ```bash
+ cd /path/to/instavm-coderunner-plugin
+ python3 scripts/mcp-proxy.py
+ # Then send: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}
+ ```
+
+## How It Works
+
+This plugin uses Claude Code's MCP server integration to connect to the local CodeRunner instance:
+
+1. The `.mcp.json` file defines the MCP server connection
+2. Python proxy script bridges stdio MCP to HTTP endpoint
+3. Tools from CodeRunner are automatically discovered and made available
+4. When you ask Claude to execute code, it uses the `execute_python_code` tool
+5. Results are returned via the MCP protocol
+
+## Publishing to GitHub Marketplace
+
+This plugin is published on GitHub and can be installed directly via the GitHub URL. Here's how to publish updates:
+
+### 1. Commit and Push Changes
+
+```bash
+cd instavm-coderunner-plugin
+git add .
+git commit -m "Update plugin"
+git push origin main
+```
+
+### 2. Users Install from GitHub
+
+Users can install the plugin directly from GitHub:
+
+```bash
+# Add the InstaVM marketplace
+claude plugin marketplace add github:BandarLabs/coderunner/instavm-coderunner-plugin
+
+# Install the plugin
+claude plugin install instavm-coderunner@instavm-plugins
+```
+
+That's it! Claude Code will automatically pull the plugin files from GitHub.
+
+## Repository Structure
+
+```
+coderunner/
+└── instavm-coderunner-plugin/ # Plugin directory
+ ├── .claude-plugin/
+ │ ├── marketplace.json # Marketplace metadata
+ │ └── plugin.json # Plugin manifest
+ ├── scripts/
+ │ └── mcp-proxy.py # stdio-to-HTTP MCP proxy
+ ├── .mcp.json # MCP server configuration
+ └── README.md # This file
+```
+
+## License
+
+MIT
diff --git a/instavm-coderunner-plugin/install.sh b/instavm-coderunner-plugin/install.sh
new file mode 100755
index 0000000..5dbb4f2
--- /dev/null
+++ b/instavm-coderunner-plugin/install.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+# InstaVM CodeRunner Plugin Installer for Claude Code
+
+PLUGIN_NAME="instavm-coderunner"
+PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+echo "🚀 Installing $PLUGIN_NAME plugin for Claude Code..."
+echo ""
+
+# Check if CodeRunner is running
+echo "🔍 Checking if CodeRunner is running..."
+if curl -s http://coderunner.local:8222/health > /dev/null 2>&1; then
+ echo "✅ CodeRunner is running at http://coderunner.local:8222"
+else
+ echo "⚠️ Warning: CodeRunner does not appear to be running."
+ echo " Please install and start CodeRunner first:"
+ echo " cd /path/to/coderunner && sudo ./install.sh"
+ echo ""
+ read -p "Continue anyway? (y/N) " -n 1 -r
+ echo ""
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ exit 1
+ fi
+fi
+
+# Check MCP endpoint
+echo "🔍 Checking MCP endpoint..."
+if curl -s http://coderunner.local:8222/mcp > /dev/null 2>&1; then
+ echo "✅ MCP endpoint is accessible"
+else
+ echo "⚠️ Warning: MCP endpoint not accessible"
+fi
+
+echo ""
+echo "📦 Plugin location: $PLUGIN_DIR"
+echo ""
+echo "To use this plugin with Claude Code, run:"
+echo ""
+echo " cd $PLUGIN_DIR"
+echo " claude --plugin-dir ."
+echo ""
+echo "Or add it permanently:"
+echo ""
+echo " claude add plugin $PLUGIN_DIR"
+echo ""
+echo "✅ Setup complete!"
diff --git a/instavm-coderunner-plugin/scripts/mcp-proxy.py b/instavm-coderunner-plugin/scripts/mcp-proxy.py
new file mode 100755
index 0000000..3823d24
--- /dev/null
+++ b/instavm-coderunner-plugin/scripts/mcp-proxy.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+"""
+InstaVM CodeRunner MCP Proxy
+Bridges stdio MCP protocol to HTTP endpoint with FastMCP session management
+"""
+
+import sys
+import os
+import json
+import urllib.request
+import urllib.error
+from urllib.parse import urlparse, urlencode
+
+# Get MCP URL from environment or use default
+MCP_URL = os.environ.get("MCP_URL", "http://coderunner.local:8222/mcp")
+
+# Session management
+session_id = None
+
+def send_request(request):
+ """Forward request to HTTP MCP server with session ID"""
+ global session_id
+
+ try:
+ data = json.dumps(request).encode('utf-8')
+ parsed_url = urlparse(MCP_URL)
+ host_header = f"{parsed_url.hostname}:{parsed_url.port}" if parsed_url.port else parsed_url.hostname
+
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json, text/event-stream',
+ 'Host': host_header
+ }
+
+ # Add session ID header if we have one (but not for initialize)
+ if session_id and request.get("method") != "initialize":
+ headers['mcp-session-id'] = session_id
+
+ url = MCP_URL
+
+ req = urllib.request.Request(
+ url,
+ data=data,
+ headers=headers,
+ method='POST'
+ )
+
+ with urllib.request.urlopen(req, timeout=120) as response:
+ # Extract session ID from initialize response
+ if request.get("method") == "initialize":
+ session_id = response.headers.get('mcp-session-id')
+ if not session_id:
+ # Fallback: try Set-Cookie header
+ set_cookie = response.headers.get('Set-Cookie', '')
+ if 'mcp_session_id=' in set_cookie:
+ cookie_parts = set_cookie.split(';')
+ for part in cookie_parts:
+ if 'mcp_session_id=' in part:
+ session_id = part.split('=')[1].strip()
+ break
+
+ # Handle SSE response format
+ body = response.read().decode('utf-8')
+ for line in body.split('\n'):
+ line = line.strip()
+ if line.startswith('data: '):
+ data_str = line[6:] # Remove 'data: ' prefix
+ if data_str and data_str != '[DONE]':
+ try:
+ response_obj = json.loads(data_str)
+ print(json.dumps(response_obj), flush=True)
+ except json.JSONDecodeError:
+ pass
+
+ except urllib.error.HTTPError as e:
+ error_body = ""
+ try:
+ error_body = e.read().decode('utf-8')
+ except:
+ pass
+
+ print(json.dumps({
+ "jsonrpc": "2.0",
+ "id": request.get("id"),
+ "error": {
+ "code": e.code,
+ "message": f"HTTP Error: {e.reason}"
+ }
+ }), flush=True)
+
+ except urllib.error.URLError as e:
+ print(json.dumps({
+ "jsonrpc": "2.0",
+ "id": request.get("id"),
+ "error": {
+ "code": -32603,
+ "message": f"Connection Error: {e.reason}. Is CodeRunner running?"
+ }
+ }), flush=True)
+
+ except Exception as e:
+ print(json.dumps({
+ "jsonrpc": "2.0",
+ "id": request.get("id"),
+ "error": {
+ "code": -32603,
+ "message": f"Internal Error: {str(e)}"
+ }
+ }), flush=True)
+
+def main():
+ """Main proxy loop"""
+ # Don't log on startup to avoid confusing MCP clients
+ for line in sys.stdin:
+ line = line.strip()
+ if not line:
+ continue
+
+ try:
+ request = json.loads(line)
+ send_request(request)
+ except json.JSONDecodeError:
+ print(json.dumps({
+ "jsonrpc": "2.0",
+ "id": None,
+ "error": {
+ "code": -32700,
+ "message": "Parse error"
+ }
+ }), flush=True)
+
+if __name__ == "__main__":
+ main()
diff --git a/server.py b/server.py
index 7be87a4..828d2f9 100644
--- a/server.py
+++ b/server.py
@@ -19,9 +19,12 @@
import httpx
# Import Context for progress reporting
from mcp.server.fastmcp import FastMCP, Context
+from mcp.server.transport_security import TransportSecuritySettings
from playwright.async_api import async_playwright
from bs4 import BeautifulSoup
import socket
+from starlette.requests import Request
+from starlette.responses import JSONResponse
# --- CONFIGURATION & SETUP ---
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
@@ -29,7 +32,24 @@
logger = logging.getLogger(__name__)
# Initialize the MCP server with a descriptive name for the toolset
-mcp = FastMCP("CodeRunner")
+# Configure DNS rebinding protection to allow coderunner.local
+mcp = FastMCP(
+ "CodeRunner",
+ transport_security=TransportSecuritySettings(
+ enable_dns_rebinding_protection=True,
+ allowed_hosts=[
+ "localhost:*",
+ "127.0.0.1:*",
+ "coderunner.local:*",
+ "0.0.0.0:*",
+ ],
+ allowed_origins=[
+ "http://localhost:*",
+ "http://127.0.0.1:*",
+ "http://coderunner.local:*",
+ ],
+ )
+)
# Kernel pool configuration
MAX_KERNELS = 5
@@ -731,5 +751,278 @@ async def get_skill_file(skill_name: str, filename: str) -> str:
return header + content
+# --- REST API ENDPOINTS FOR SANDBOX CLIENT COMPATIBILITY ---
+# These endpoints provide REST API access compatible with the instavm SDK client
+# allowing local execution without cloud API
+
+class MockContext:
+ """Mock context for REST API calls that don't have MCP context"""
+ async def report_progress(self, progress: int, message: str):
+ # Log progress instead of reporting through MCP
+ logger.info(f"Progress {progress}%: {message}")
+
+
# Use the streamable_http_app as it's the modern standard
-app = mcp.streamable_http_app()
\ No newline at end of file
+app = mcp.streamable_http_app()
+
+# Add custom REST API endpoints compatible with instavm SDK client
+async def api_execute(request: Request):
+ """
+ REST API endpoint for executing Python code (compatible with InstaVM SDK).
+
+ Request body (JSON):
+ {
+ "command": "print('hello world')",
+ "session_id": "optional-ignored-for-local",
+ "language": "python", // optional, only python supported
+ "timeout": 300 // optional, not used in local execution
+ }
+
+ Response (JSON) - matches api.instavm.io/execute format:
+ {
+ "stdout": "hello world\\n",
+ "stderr": "",
+ "execution_time": 0.39,
+ "cpu_time": 0.03
+ }
+ """
+ import time
+ start_time = time.time()
+
+ try:
+ # Parse request body
+ body = await request.json()
+
+ # SDK sends "code" field, direct API calls use "command"
+ command = body.get("code") or body.get("command")
+
+ if not command:
+ return JSONResponse({
+ "stdout": "",
+ "stderr": "Missing 'code' or 'command' field in request body",
+ "execution_time": 0.0,
+ "cpu_time": 0.0
+ }, status_code=400)
+
+ # Create mock context for progress reporting
+ ctx = MockContext()
+
+ # Execute the code
+ result = await execute_python_code(command, ctx)
+
+ # Calculate execution time
+ execution_time = time.time() - start_time
+
+ # Check if result contains an error
+ if result.startswith("Error:"):
+ return JSONResponse({
+ "stdout": "",
+ "stderr": result,
+ "execution_time": execution_time,
+ "cpu_time": execution_time # Approximate CPU time as execution time
+ })
+
+ # For compatibility with api.instavm.io, return stdout/stderr format
+ # Since execute_python_code returns combined output, we put it all in stdout
+ return JSONResponse({
+ "stdout": result,
+ "stderr": "",
+ "execution_time": execution_time,
+ "cpu_time": execution_time # Approximate CPU time as execution time
+ })
+
+ except Exception as e:
+ logger.error(f"Error in /execute endpoint: {e}", exc_info=True)
+ execution_time = time.time() - start_time
+ return JSONResponse({
+ "stdout": "",
+ "stderr": f"Error: {str(e)}",
+ "execution_time": execution_time,
+ "cpu_time": execution_time
+ }, status_code=500)
+
+
+async def api_browser_navigate(request: Request):
+ """
+ REST API endpoint for browser navigation (compatible with InstaVM SDK).
+
+ Request body (JSON):
+ {
+ "url": "https://example.com",
+ "session_id": "optional-ignored-for-local",
+ "wait_timeout": 30000 // optional
+ }
+
+ Response (JSON):
+ {
+ "status": "success",
+ "url": "https://example.com",
+ "title": "Example Domain"
+ }
+ or
+ {
+ "status": "error",
+ "error": "error message"
+ }
+ """
+ try:
+ # Parse request body
+ body = await request.json()
+ url = body.get("url")
+
+ if not url:
+ return JSONResponse({
+ "status": "error",
+ "error": "Missing 'url' field in request body"
+ })
+
+ # Navigate and get text
+ result = await navigate_and_get_all_visible_text(url)
+
+ # Check if result contains an error
+ if result.startswith("Error:"):
+ return JSONResponse({
+ "status": "error",
+ "error": result
+ })
+
+ return JSONResponse({
+ "status": "success",
+ "url": url,
+ "content": result,
+ "title": "Navigation successful"
+ })
+
+ except Exception as e:
+ logger.error(f"Error in /v1/browser/interactions/navigate endpoint: {e}", exc_info=True)
+ return JSONResponse({
+ "status": "error",
+ "error": f"Error: {str(e)}"
+ })
+
+
+async def api_browser_extract_content(request: Request):
+ """
+ REST API endpoint for extracting browser content (compatible with InstaVM SDK).
+
+ Request body (JSON):
+ {
+ "session_id": "optional-ignored-for-local",
+ "url": "https://example.com", // required for local execution
+ "include_interactive": true,
+ "include_anchors": true,
+ "max_anchors": 50
+ }
+
+ Response (JSON):
+ {
+ "readable_content": {"content": "text content"},
+ "status": "success"
+ }
+ """
+ try:
+ # Parse request body
+ body = await request.json()
+ url = body.get("url")
+
+ if not url:
+ return JSONResponse({
+ "status": "error",
+ "error": "Missing 'url' field in request body (required for local execution)"
+ })
+
+ # Navigate and get text
+ result = await navigate_and_get_all_visible_text(url)
+
+ # Check if result contains an error
+ if result.startswith("Error:"):
+ return JSONResponse({
+ "status": "error",
+ "error": result
+ })
+
+ return JSONResponse({
+ "readable_content": {
+ "content": result
+ },
+ "status": "success"
+ })
+
+ except Exception as e:
+ logger.error(f"Error in /v1/browser/interactions/content endpoint: {e}", exc_info=True)
+ return JSONResponse({
+ "status": "error",
+ "error": f"Error: {str(e)}"
+ })
+
+# --- SESSION MANAGEMENT ENDPOINTS FOR SDK COMPATIBILITY ---
+
+# Simple in-memory session store (for local use, sessions are lightweight)
+_session_store = {}
+_session_counter = 0
+
+
+async def api_start_session(request: Request):
+ """
+ Start a new session (compatible with InstaVM SDK).
+
+ For local execution, sessions are lightweight - we just return a session ID.
+ The SDK uses this for tracking, but locally we don't need complex session state.
+
+ Response (JSON):
+ {
+ "session_id": "session_123",
+ "status": "active"
+ }
+ """
+ global _session_counter
+ _session_counter += 1
+ session_id = f"session_{_session_counter}"
+
+ # Store session (minimal state for local use)
+ _session_store[session_id] = {
+ "status": "active",
+ "created_at": __import__('time').time()
+ }
+
+ return JSONResponse({
+ "session_id": session_id,
+ "status": "active"
+ })
+
+
+async def api_get_session(request: Request):
+ """
+ Get session status (compatible with InstaVM SDK).
+
+ Response (JSON):
+ {
+ "session_id": "session_123",
+ "status": "active"
+ }
+ """
+ # For local use, just return active status
+ return JSONResponse({
+ "session_id": "session",
+ "status": "active"
+ })
+
+
+async def api_stop_session(request: Request):
+ """
+ Stop a session (compatible with InstaVM SDK).
+
+ For local use, this is a no-op since we don't have real session state.
+ """
+ return JSONResponse({
+ "status": "stopped"
+ })
+
+
+# Add routes to the Starlette app
+app.add_route("/execute", api_execute, methods=["POST"])
+app.add_route("/v1/sessions/session", api_start_session, methods=["POST"])
+app.add_route("/v1/sessions/session", api_get_session, methods=["GET"])
+app.add_route("/v1/sessions/session", api_stop_session, methods=["DELETE"])
+app.add_route("/v1/browser/interactions/navigate", api_browser_navigate, methods=["POST"])
+app.add_route("/v1/browser/interactions/content", api_browser_extract_content, methods=["POST"])
\ No newline at end of file