Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .assets-revision
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# is a git revision (branch name like `main`, a tag, or a specific commit
# sha). Override at runtime with the ASSETS_REVISION env var.
repo: ChilleD/WebHarbor
revision: d4bee3a21751cdbe900b846dfcec37130f765c36
revision: c0053deaf15d082bf90a94d42532b891d4ad216a
10 changes: 5 additions & 5 deletions .claude/skills/review-env/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ gh pr checkout <pr-number>
./scripts/fetch_assets.sh # pull the pinned HF revision
./scripts/build.sh webharbor:dev
docker run -d --rm --name wh-review \
-p 8201:8101 -p 41000-41015:40000-40015 webharbor:dev
-p 8201:8101 -p 41000-41016:40000-40016 webharbor:dev
```

Confirm the new/changed site is on the expected port (40000 + index). Note: the image now runs 16 sites (40000-40015).
Confirm the new/changed site is on the expected port (40000 + index). Note: the image now runs 17 sites (40000-40016).

### Step 2: The mechanical checks (5 minutes)

Run the same Pre-PR checks the contributor was supposed to run.

```bash
# 1. all 16 sites return 200
for p in $(seq 41000 41015); do
# 1. all 17 sites return 200
for p in $(seq 41000 41016); do
curl -so /dev/null -w "$p:%{http_code}\n" http://localhost:$p/
done

Expand Down Expand Up @@ -231,7 +231,7 @@ Leave a structured comment on the PR:
## Review: <site_name>

### Mechanical checks: PASS / FAIL
- [x] All 15 sites return 200
- [x] All 17 sites return 200
- [x] Control plane healthy
- [x] Byte-identical reset (md5 match)
- [x] Parallel reset <10s
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,4 @@ secrets.json
# ============================================================
# Agent demo results
# =============================================================
agent_demo/runs/
agent_demo/runs/
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A coding agent (Claude Code, Cursor, Aider, Codex, ...) is reading this. Read on

## What it is

17 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image.
18 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image.

Two repos:
- **code** (this one) — Flask apps, control plane, scripts.
Expand Down Expand Up @@ -48,17 +48,17 @@ Inside the image, sites live at `/opt/WebSyn/<site>/`. The path predates the ren
# fresh clone
./scripts/fetch_assets.sh # pulls assets from HF
./scripts/build.sh # docker build -t webharbor:dev .
docker run -d -p 8101:8101 -p 40000-40016:40000-40016 webharbor:dev
docker run -d -p 8101:8101 -p 40000-40017:40000-40017 webharbor:dev
```

Or use the published image directly:

```bash
docker run -d -p 8101:8101 -p 40000-40016:40000-40016 \
docker run -d -p 8101:8101 -p 40000-40017:40000-40017 \
battalion7244/webharbor:latest
```

Sites are on `40000`-`40016` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane:
Sites are on `40000`-`40017` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane:

| Method | Path | Purpose |
|--------|---------------------|-------------------------------------------|
Expand Down Expand Up @@ -136,13 +136,13 @@ python3 -m py_compile sites/<site>/app.py

# 3. run on alt ports (don't collide with anything you already have running)
docker run -d --rm --name wh-test \
-p 8201:8101 -p 41000-41016:40000-40016 webharbor:dev
-p 8201:8101 -p 41000-41017:40000-40017 webharbor:dev

# 4. control plane healthy, all sites alive
curl -s http://localhost:8201/health | python3 -m json.tool | head

# 5. every site renders 200
for p in $(seq 41000 41016); do
for p in $(seq 41000 41017); do
curl -so /dev/null -w "$p:%{http_code}\n" http://localhost:$p/
done

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ The full agent guide is loaded above via `@AGENTS.md`. The notes below apply onl

## Existing containers

If a container is already running on `:8101` / `:40000-40016`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41016`).
If a container is already running on `:8101` / `:40000-40017`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41017`).
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ git clone https://github.com/<you>/webharbor && cd webharbor
./scripts/fetch_assets.sh # pull current assets
./scripts/new_site.py mywebsite # OR edit an existing site
./scripts/build.sh && docker run -d --rm \
-p 8101:8101 -p 40000-40016:40000-40016 webharbor:dev
-p 8101:8101 -p 40000-40017:40000-40017 webharbor:dev
# iterate locally...

./scripts/extract_assets.sh ../webharbor-static-pr/ # split assets out
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# WebHarbor — slim, self-contained image.
# 17 Flask mirror sites + control plane on :8101.
# 18 Flask mirror sites + control plane on :8101.

FROM python:3.12-slim-bookworm

Expand Down Expand Up @@ -36,6 +36,6 @@ COPY control_server.py /opt/control_server.py
COPY site_runner.py /opt/site_runner.py
RUN chmod +x /opt/websyn_start.sh

EXPOSE 8101 40000-40016
EXPOSE 8101 40000-40017

CMD ["/opt/websyn_start.sh"]
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ WebHarbor takes a different approach. We leverage coding agent (e.g., Claude Cod
- **Deep features unlocked** — carts, checkouts, accounts, all fully testable
- **Evolving** — harder tasks drive richer mirrors; the environment grows with agents
- **RL-ready** — sub-second database resets between rollouts
- **Community-driven** — 17 sites today, scaling to 100+ together
- **Community-driven** — 18 sites today, scaling to 100+ together

## 🚀 Quickstart

One command to run all web environments:

```bash
docker run -p 8101:8101 -p 40000-40016:40000-40016 battalion7244/webharbor:latest
docker run -p 8101:8101 -p 40000-40017:40000-40017 battalion7244/webharbor:latest
```

Then point your agent at `http://localhost:40000` through `http://localhost:40016` to explore 17 local mirrors of webvoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, and IKEA`.
Then point your agent at `http://localhost:40000` through `http://localhost:40017` to explore 18 local mirrors of webvoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, and Phys.org`.

For sub-second reset between rollouts, expose the control plane and call `/reset/<site>`:

Expand All @@ -65,7 +65,7 @@ git clone https://github.com/aiming-lab/WebHarbor && cd WebHarbor

## 🤝 Contribute

We have built 17 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together.
We have built 18 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together.

There are two ways to join the author list:

Expand Down
2 changes: 1 addition & 1 deletion agent_demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export OPENAI_BASE_URL=https://api.openai.com/v1 # or your Azure / vLLM endpoi

## Run a task

WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40016:40000-40016 battalion7244/webharbor:latest`).
WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40017:40000-40017 battalion7244/webharbor:latest`).

Run a single task from a site's `tasks.jsonl`:

Expand Down
2 changes: 1 addition & 1 deletion control_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking',
'github', 'google_flights', 'google_map', 'google_search',
'huggingface', 'wolfram_alpha', 'cambridge_dictionary',
'coursera', 'espn', 'merriam_webster', 'ikea',
'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org',
]
BASE_PORT = 40000
WEBSYN_DIR = '/opt/WebSyn'
Expand Down
4 changes: 3 additions & 1 deletion scripts/extract_assets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ for site_dir in sites/*/; do
fi

out="$TARGET/$site.tar.gz"
tar -czf "$out" -C sites "${members[@]}"
# macOS may synthesize AppleDouble ``._*`` metadata while archiving files.
# Exclude it explicitly so uploaded assets are portable and reproducible.
COPYFILE_DISABLE=1 tar --exclude='._*' -czf "$out" -C sites "${members[@]}"
sz=$(du -sh "$out" 2>/dev/null | cut -f1)
printf " %-22s -> %-30s %s\n" "$site" "$site.tar.gz" "$sz"
count=$((count + 1))
Expand Down
72 changes: 72 additions & 0 deletions sites/phys_org/_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Phys.org mirror health check."""
from healthcheck import random_user


def run(p):
# 1. Home page renders
p.assert_get('home', '/', must_contain='Phys.org')

# 2. Category pages render (DB read)
p.assert_get('category physics', '/category/physics', must_contain='Physics')
p.assert_get('category technology', '/category/technology', must_contain='Technology')

# 3. Trending list renders
p.assert_get('trending', '/trending', must_contain='Trending')

# 4. Search returns results (token-overlap match)
p.assert_get('search quantum', '/search?q=quantum', must_contain='quantum')

# 5. User profile (DB read)
p.assert_get('user profile', '/user/alice_j', must_contain='alice_j')

# 6. Article detail page (DB read; pick the first article slug from home)
home_html = p.get('/').text if hasattr(p.get('/'), 'text') else ''
# Fallback: known seed article slug pattern uses kebab; we look up by id 1.
# The home grid links to /article/<slug>; just pick a simple test that the
# detail route is wired up at all.
p.assert_get('article first', '/article/' + _first_slug(home_html, fallback='nonexistent'),
accept_status=(200, 404))

# 7. Register page renders (CSRF visible)
user = random_user()
html = p.assert_get('register page', '/register', must_contain='csrf_token')
token = p.csrf(html)
if not token:
p.check('register csrf token', False, 'no csrf in register form')
return

# 8. Submit registration (DB write)
p.assert_post('register submit', '/register', {
'csrf_token': token,
'username': user['name'],
'email': f"{user['name']}@test.com",
'full_name': user['name'].title(),
'password': user['password'],
}, accept_status=(200, 302, 303))

# Logout to confirm /login renders
p.get('/logout')

# 9. Login page renders
html = p.assert_get('login page', '/login', accept_status=(200, 302, 303))
token = p.csrf(html) if html else ''

# 10. Submit login (DB read + session)
if token:
p.assert_post('login submit', '/login', {
'csrf_token': token,
'email': f"{user['name']}@test.com",
'password': user['password'],
}, accept_status=(200, 302, 303))
else:
p.check('login submit', True, 'already authenticated from register')

# 11. Authenticated: account page accessible
p.assert_get('account page', '/account', accept_status=(200, 302, 303))


def _first_slug(html: str, fallback: str) -> str:
"""Best-effort: pull the first /article/<slug> link from the home page."""
import re
m = re.search(r'/article/([a-z0-9-]+)', html or '')
return m.group(1) if m else fallback
Loading