Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ __pycache__/
# Local config files
*.local.*

# Video recordings
recordings/*

# C extensions
*.so

Expand Down
39 changes: 39 additions & 0 deletions agents/browser-use-muzzle/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Use an official Python runtime as a parent image
FROM python:3.12-slim

# Install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
chromium \
fonts-unifont \
fonts-liberation \
fonts-dejavu-core \
fonts-freefont-ttf \
fonts-noto-core \
ffmpeg \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& ln -s /usr/bin/chromium /usr/bin/chromium-browser

# Install the zoo certificate
RUN curl -o /usr/local/share/ca-certificates/zoo-root.crt \
https://raw.githubusercontent.com/bgrins/the_zoo/refs/heads/main/core/caddy/root.crt \
&& update-ca-certificates

# Set environment variables for Chromium
ENV CHROME_PATH=/usr/bin/chromium-browser

RUN pip install --no-cache-dir "browser-use[video]==0.7.10"

# Create a working directory
WORKDIR /app

# Copy worker script into the container
COPY main.py .

# Create directory for recordings
RUN mkdir -p /app/tmp/recordings

# Keep container running (users will docker exec to run tasks)
CMD ["tail", "-f", "/dev/null"]
57 changes: 57 additions & 0 deletions agents/browser-use-muzzle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Browser Use Muzzle Agent

A containerized browser automation worker using [browser-use](https://github.com/browser-use/browser-use) for AI-powered web tasks.

## Setup

### Start the container
```bash
docker-compose up -d browser-use
```

This starts a persistent container with browser-use installed and ready to execute tasks.

## Usage

Execute browser automation tasks by running the worker script inside the container:

### Basic task
```bash
docker-compose exec browser-use python main.py \
--task "Go to example.zoo and summarize the contents"
```

### With starting URL
```bash
docker-compose exec browser-use python main.py \
--task "Summarize this page" \
--starting-url "https://status.zoo"
```

### Custom max steps
```bash
docker-compose exec browser-use python main.py \
--task "Navigate to example.zoo, find the contact page, and extract email addresses" \
--max-steps 15
```

## Environment Variables

Set these in the `.env` file or manually in your environment

- `OPENAI_API_KEY` (required): Your OpenAI API key
- `OPENAI_MODEL` (optional): OpenAI model to use (default: `gpt-4o-mini`)

## Features

- **Proxy support**: Requires proxy at `host.docker.internal:3128` (fails if unavailable)
- **Domain restrictions**: Only allows navigation to `*.zoo` domains
- **Video recordings**: Saved to `./recordings/` directory
- **SSL bypass**: Ignores certificate errors for self-signed certs

## Architecture

- Container runs continuously with `tail -f /dev/null`
- Each task execution is isolated (separate `docker exec` invocation)
- No shared state between task runs
- Worker script exits after each task completion
113 changes: 113 additions & 0 deletions agents/browser-use-muzzle/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Browser-use worker - executes a single browser automation task and exits.
Usage: python browser_use_worker.py --task "Your task here" [--starting-url URL] [--max-steps N]
"""

import argparse
import asyncio
import os
import socket
import sys
from pathlib import Path
from typing import Optional

from browser_use import Agent, Browser, ChatOpenAI
from browser_use.browser import ProxySettings


def check_proxy_available(host: str, port: int) -> bool:
"""Check if proxy is available before attempting to use it."""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex((host, port))
sock.close()
return result == 0
except Exception as e:
print(f"Error checking proxy: {e}", file=sys.stderr)
return False


async def run_task(task: str, max_steps: int = 10, starting_url: Optional[str] = None):
"""Run a browser-use agent task with proxy."""
proxy_host = 'host.docker.internal'
proxy_port = 3128

# Verify proxy is available
if not check_proxy_available(proxy_host, proxy_port):
raise RuntimeError(f"Proxy not available at {proxy_host}:{proxy_port}")

print(f"Proxy available at {proxy_host}:{proxy_port}")

# Configure browser session
browser_session = Browser(
record_video_dir=Path('./tmp/recordings'),
proxy=ProxySettings(server=f'http://{proxy_host}:{proxy_port}'),
args=['--ignore-certificate-errors']
)

# Build initial actions if starting URL is provided
initial_actions = None
if starting_url:
initial_actions = [{'go_to_url': {'url': starting_url}}]
print(f"Starting at URL: {starting_url}")

# Create and run agent
agent = Agent(
task=task,
llm=ChatOpenAI(
model=os.environ.get('OPENAI_MODEL', 'gpt-4o-mini'),
api_key=os.environ.get('OPENAI_API_KEY')
),
browser_session=browser_session,
allowed_domains=['*.zoo'],
initial_actions=initial_actions,
)

print(f"Running task: {task}")
result = await agent.run(max_steps=max_steps)

print(f"\nTask completed successfully")
print(f"Result: {result}")

return result


def main():
parser = argparse.ArgumentParser(
description='Execute a browser automation task using browser-use'
)
parser.add_argument(
'--task',
required=True,
help='Task description for the browser agent'
)
parser.add_argument(
'--starting-url',
help='Optional starting URL for the task'
)
parser.add_argument(
'--max-steps',
type=int,
default=10,
help='Maximum number of steps for the agent (default: 10)'
)

args = parser.parse_args()

# Validate required environment variables
if not os.environ.get('OPENAI_API_KEY'):
print("Error: OPENAI_API_KEY environment variable not set", file=sys.stderr)
sys.exit(1)

# Run the task
try:
asyncio.run(run_task(args.task, args.max_steps, args.starting_url))
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)


if __name__ == '__main__':
main()
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
browser-use:
build: ./agents/browser-use-muzzle
image: browser-use-muzzle
container_name: browser-use-muzzle
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o-mini}
volumes:
- ./recordings:/app/tmp/recordings
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
105 changes: 99 additions & 6 deletions muzzle/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@
import os

class Experiment:
def __init__(self, task_config_path, agent_config_path, browser_config_path, injection_config_path):
def __init__(self, task_config_path, agent_config_path, browser_config_path, injection_config_path, agent_name=None):
self.id = str(uuid.uuid4())
self.browser_config, self.agent_config, self.task_config = get_configs(
browser_config_path, agent_config_path, task_config_path, injection_config_path
)
self.agent_name = agent_name

# Only load configs if not using a built-in agent
if agent_name:
# For built-in agents like browser_use, only load task config
self.task_config = get_configs(None, None, task_config_path, injection_config_path)[2]
self.browser_config = None
self.agent_config = None
else:
self.browser_config, self.agent_config, self.task_config = get_configs(
browser_config_path, agent_config_path, task_config_path, injection_config_path
)

self.logger = logging.getLogger(self.__class__.__name__)
self.docker_client = docker.from_env()

Expand Down Expand Up @@ -133,6 +143,86 @@ def _evaluate_task_completion(self):
evaluator = Evaluator()
evaluator.evaluate(self.task_config)

# Map of agent names to docker-compose service names
AGENT_SERVICE_MAP = {
"browser_use": "browser-use",
}

def _run_named_agent_worker(self, agent_name):
"""
Run a named agent worker via docker-compose exec.

Args:
agent_name (str): Name of the agent (e.g., 'browser_use')
"""
self.logger.info(f"Running {agent_name} worker...")

# Get service name from map
service_name = self.AGENT_SERVICE_MAP.get(agent_name)
if not service_name:
raise ValueError(f"Unknown agent name: {agent_name}. Supported agents: {list(self.AGENT_SERVICE_MAP.keys())}")

import subprocess

# Check if container is running, if not start it
self.logger.info(f"Checking if {service_name} container is running...")
check_cmd = ["docker-compose", "ps", "-q", service_name]
result = subprocess.run(check_cmd, capture_output=True, text=True)

if not result.stdout.strip():
self.logger.info(f"Container {service_name} is not running. Starting it now...")
start_cmd = ["docker-compose", "up", "-d", service_name]
start_result = subprocess.run(start_cmd, capture_output=True, text=True)
if start_result.returncode != 0:
raise RuntimeError(f"Failed to start {service_name} container: {start_result.stderr}")
self.logger.info(f"Container {service_name} started successfully")
else:
self.logger.info(f"Container {service_name} is already running")

# Build the docker-compose exec command
# Format task with credentials and starting URL like the old agent does
parameters = self.task_config.get("parameters", {})

base_task = self.task_config.get("task", "NO TASK PROVIDED")
username = parameters.get("gitlab_username", "user")
password = parameters.get("gitlab_password", "pass")
default_url = self.task_config.get("default_url", "about:blank")
max_steps = self.task_config.get("max_steps", 10)

self.logger.info(f"Extracted - username: {username}, password: {password}, default_url: {default_url}")

task_description = f"Your username is {username} and your password is {password}. FIRST GO TO {default_url}. {base_task}"

cmd = [
"docker-compose", "exec", "-T", service_name,
"python", "main.py",
"--task", task_description,
"--max-steps", str(max_steps),
"--starting-url", default_url
]

self.logger.info(f"Running command: {' '.join(cmd)}")

# Execute the command and stream output
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)

# Stream output in real-time
for line in process.stdout:
self.logger.info(line.rstrip())

# Wait for completion
return_code = process.wait()
if return_code != 0:
raise RuntimeError(f"{agent_name} worker failed with exit code {return_code}")

self.logger.info(f"{agent_name} worker completed successfully")

def run(self):
"""
Run the experiment.
Expand All @@ -145,8 +235,11 @@ def run(self):
# Step 2: Seed the Zoo with the desired content for the experiment
self._seed_zoo()

# Step 3: Run the docker container with the desired experiment parameters
self._run_docker_container()
# Step 3: Run the agent (either docker container or named agent worker)
if self.agent_name:
self._run_named_agent_worker(self.agent_name)
else:
self._run_docker_container()

# Step 4: Evaluate the task completion
self._evaluate_task_completion()
Loading