Skip to content
Merged
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: 480c892e976bada6c0ea3f5a66e2b9efda65525d
revision: db9c73e62d853ed91f6152b5b7105571502b7e07
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

20 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.
21 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-40019:40000-40019 webharbor:dev
docker run -d -p 8101:8101 -p 40000-40020:40000-40020 webharbor:dev
```

Or use the published image directly:

```bash
docker run -d -p 8101:8101 -p 40000-40019:40000-40019 \
docker run -d -p 8101:8101 -p 40000-40020:40000-40020 \
battalion7244/webharbor:latest
```

Sites are on `40000`-`40019` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane:
Sites are on `40000`-`40020` 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-41019:40000-40019 webharbor:dev
-p 8201:8101 -p 41000-41020:40000-40020 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 41019); do
for p in $(seq 41000 41020); 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-40019`, 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-41019`).
If a container is already running on `:8101` / `:40000-40020`, 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-41020`).
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-40019:40000-40019 webharbor:dev
-p 8101:8101 -p 40000-40020:40000-40020 webharbor:dev
# iterate locally...

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

FROM python:3.12-slim-bookworm

Expand Down Expand Up @@ -41,6 +41,16 @@ 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-40019
# OSU's real-site image bundle is required, while its database is generated
# deterministically from tracked source data.
RUN test -n "$(ls -A /opt/WebSyn/osu/static/images)"
RUN cd /opt/WebSyn/osu && python3 -c "\
import app; \
import os, shutil; \
os.makedirs('instance_seed', exist_ok=True); \
shutil.copy2('instance/osu.db', 'instance_seed/osu.db'); \
print('osu seed DB generated at build time.')" && rm -rf /opt/WebSyn/osu/instance

EXPOSE 8101 40000-40020

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** — 20 sites today, scaling to 100+ together
- **Community-driven** — 21 sites today, scaling to 100+ together

## 🚀 Quickstart

One command to run all web environments:

```bash
docker run -p 8101:8101 -p 40000-40019:40000-40019 battalion7244/webharbor:latest
docker run -p 8101:8101 -p 40000-40020:40000-40020 battalion7244/webharbor:latest
```

Then point your agent at `http://localhost:40000` through `http://localhost:40019` to explore 20 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, Phys.org, Target, and TED`.
Then point your agent at `http://localhost:40000` through `http://localhost:40020` to explore 21 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, Phys.org, Target, TED, and Ohio State University`.

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 20 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 21 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-40019:40000-40019 battalion7244/webharbor:latest`).
WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40020:40000-40020 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', 'phys_org', 'target', 'ted',
'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu',
]
BASE_PORT = 40000
WEBSYN_DIR = '/opt/WebSyn'
Expand Down
40 changes: 40 additions & 0 deletions review-reports/PR-63-FINAL-AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# PR #63 independent review and remediation audit

## Scope

Seven independent review contexts examined PR #63 head `4890c86374daea2fa29455b919b0ab55095efc3d` against its original base `438a029c04d86b22c710ad5d985d1d1491d2cb98`. Integration was also checked against current main `e911a6adb28d04397ed30d8293e77b0c62a112d5`. Raw reports are retained outside the repository under `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr63-agents/reports/`.

## Agent findings and dispositions

| Review agent | Findings on original PR head | Verification and disposition |
|---|---|---|
| Security and state | Hard-coded secret, login/bookmark open redirects, GET logout, news GET writes, unconstrained/dangling bookmarks, no bookmark uniqueness constraint, malformed session crash, weak username normalization, and unbounded notes. | Confirmed in `sites/osu/app.py`. Fixed with environment/random secret, local redirect validation, POST logout, read-only news GET, bookmark type/object validation, 500-character notes, a database unique constraint with race handling, robust user loading, lowercase usernames, request limits, session rotation, and SQLite foreign keys. |
| Task and data | Task 1 conflicted with the number of seeded team rows; several tasks were answerable from prior knowledge; task 9 accepted incomplete degree types; task 17 did not ask for a precise output; many valid-answer aliases increased grading ambiguity. | Rewrote all 20 tasks around explicit visible routes, filters, multi-field facts, and comparisons. Task 1 now explicitly asks for the About-page display. Task 9 requires all three Engineering degree types. Task 17 requires exact article title and author. Multi-entity tasks bind each number/value to its entity. |
| Verifier robustness | URL substring checks accepted external or wrong paths; required search/filter/topic steps were missing; negated answers and unrelated numbers could pass; `--no_llm` removed semantic enforcement; the synthetic selfcheck omitted adversarial cases. | Replaced the common verifier library and all 20 verifiers with same-origin exact path/query/order checks, visible-link transition checks, exact task IDs, non-empty output, negation-aware facts, entity-bound comparison values, and complete database equality for every read-only task. Added positive and adversarial tests for wrong task IDs, answer-only runs, external origins, database mutation, missing filters, negation, and swapped values. |
| UI and responsive behavior | Fixed two-column detail layouts overflowed on mobile; header/search/top bar could overflow; navigation lacked an overflow affordance and current-page semantics; forms lacked clear labels; focus styling was incomplete; cards with inline flex layouts could overflow; the mirror contained no real photographic assets. | Added responsive detail layout classes, mobile header stacking, horizontally scrollable navigation, `min-width:0` and wrapping protections, fixed grid breakpoint ordering, accessible search names, current-page `aria-current`, and focus-visible styles. Crawled 19 photographs from official Ohio State web properties, preserved source-page URLs and hashes in `sites/osu/image_sources.json`, and integrated the photographs into home, section, card, and detail layouts. Automated tests cover 320 px, 390 px, and 1440 px layouts. |
| Integration | The PR was based on a 17-site tree while current main has 20 sites; OSU collided with IKEA at `40016`; docs and asset pins were stale; clean `scripts/build.sh` would reject OSU because it intentionally has no HF seed bundle. | Merged current main locally, retained all existing sites, appended OSU as site index 20 on `40020`, exposed `40000-40020`, and updated all documentation. Added `.build-generated-seed` support for the deterministic OSU database and `.requires-images` enforcement for its HF-hosted image bundle. The pinned HF revision now contains `osu.tar.gz`. |
| Application and data model | Import-time seed recursion under `python app.py`, broad partial-state seed gate, wall-clock event filtering, nondeterministic seeded user hashes/timestamps, missing bookmark referential validation, and brittle phrase search. | Added module-safe seed resolution, partial-database failure, fixed benchmark time, stable seeded password hash and timestamps, exact bookmark validation, and token-overlap ranked global search. The same source now generates byte-identical OSU databases in repeated clean runs. |
| Test evidence | The original selfcheck used fabricated trajectories only; it did not run the site, verify rendered facts, test external-origin/path spoofing, detect database mutations, validate responsive pages, or prove a current-main Docker build. | Replaced the original selfcheck with a complete unittest entry point and added HTTP, seed, integration, positive verifier, and adversarial verifier suites. Actual Playwright task trajectories, screenshots, and before/after databases were produced for all 20 tasks. A clean build without a host OSU seed and a 21-site container smoke/reset test were completed. |

## Validation

- Python compilation, shell syntax, Ruff fatal/undefined-name checks, and `git diff --check`: PASS.
- OSU HTTP, seed, image provenance, integration, and verifier suite: 24 tests PASS.
- Actual Playwright completion from fresh databases: 20/20 PASS.
- Deterministic verifier results for those browser runs: 20/20 PASS.
- Responsive checks at 320 px, 390 px, and 1440 px: 45/45 PASS with no page-level horizontal overflow.
- Repeated clean OSU seed generation: byte-identical.
- Official-image provenance manifest: 19/19 assets present, non-empty, and SHA-256 verified.
- Hugging Face dataset PR #56 merged; pinned revision `db9c73e62d853ed91f6152b5b7105571502b7e07` contains `osu.tar.gz`.
- Pinned HF `osu.tar.gz` download, deterministic tar SHA-256 (`1fc684a25890262137714b56577cd8bcbe0c5a867c65da12f3d45ab7662949f7`), and extraction: PASS.
- Clean Docker build with `sites/osu/instance_seed` absent from the build context: PASS.
- Container health: all 21 sites alive and all 21 site roots returned HTTP 200.
- `/reset/osu`: PASS; runtime and seed SHA-256 values match.
- `/reset-all`: PASS for all 21 sites.

## Evidence

- Agent reports: `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr63-agents/reports/`
- Browser trajectories, screenshots, and verifier outputs: `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr63-fixes/e2e/`
- Responsive results and screenshots: `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr63-fixes/responsive/`
- Official image source pages, source URLs, dimensions, and hashes: `sites/osu/image_sources.json`
7 changes: 7 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ TAG="${1:-webharbor:dev}"
# Fast probe — if any site is missing instance_seed/, run fetch_assets.
need_fetch=0
for site in sites/*/; do
if [[ -f "${site}.requires-images" ]] && { [[ ! -d "${site}static/images" ]] || [[ -z $(ls -A "${site}static/images" 2>/dev/null) ]]; }; then
need_fetch=1
break
fi
if [[ -f "${site}.build-generated-seed" ]]; then
continue
fi
if [[ ! -d "${site}instance_seed" ]]; then
need_fetch=1
break
Expand Down
8 changes: 7 additions & 1 deletion scripts/check_assets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ warnings=0
for site in sites/*/; do
s=$(basename "$site")
for sub in "${REQUIRED[@]}"; do
if [[ -f "sites/$s/.build-generated-seed" && "$sub" == "instance_seed" ]]; then
continue
fi
if [[ ! -d "sites/$s/$sub" ]] || [[ -z $(ls -A "sites/$s/$sub" 2>/dev/null) ]]; then
echo " MISSING (required): sites/$s/$sub"
missing=$((missing + 1))
fi
done
for sub in "${OPTIONAL[@]}"; do
if [[ ! -d "sites/$s/$sub" ]] || [[ -z $(ls -A "sites/$s/$sub" 2>/dev/null) ]]; then
if [[ -f "sites/$s/.requires-images" && "$sub" == "static/images" ]] && { [[ ! -d "sites/$s/$sub" ]] || [[ -z $(ls -A "sites/$s/$sub" 2>/dev/null) ]]; }; then
echo " MISSING (required): sites/$s/$sub"
missing=$((missing + 1))
elif [[ ! -d "sites/$s/$sub" ]] || [[ -z $(ls -A "sites/$s/$sub" 2>/dev/null) ]]; then
warnings=$((warnings + 1))
fi
done
Expand Down
3 changes: 3 additions & 0 deletions scripts/extract_assets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ for site_dir in sites/*/; do

members=()
for sub in "${SUBPATHS[@]}"; do
if [[ "$sub" == "instance_seed" && -f "${site_dir}.build-generated-seed" ]]; then
continue
fi
[[ -e "$site_dir$sub" ]] && members+=("$site/$sub")
done
if [[ ${#members[@]} -eq 0 ]]; then
Expand Down
1 change: 1 addition & 0 deletions sites/osu/.build-generated-seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Dockerfile generates instance_seed/osu.db deterministically from tracked source data.
1 change: 1 addition & 0 deletions sites/osu/.requires-images
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This site requires the static/images directory supplied by the pinned Hugging Face asset bundle.
17 changes: 17 additions & 0 deletions sites/osu/_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Health check module for OSU mirror site."""


def health_check(app, db, College, Program):
"""Return health status dict."""
try:
with app.app_context():
college_count = College.query.count()
program_count = Program.query.count()
return {
'ok': True,
'site': 'osu',
'colleges': college_count,
'programs': program_count,
}
except Exception as e:
return {'ok': False, 'error': str(e)}
Loading