---
name: vpn-egress-orchestrator
description: "Fan external-reachability work out to parallel subagents over the shared vpn-egress Mullvad browser stack. Use when a batch of targets each need real per-target investigation -- diagnosing why each of N sites fails, auditing many domains and reading each one's config -- or when a list is large enough that wall-clock matters. NOT for simply checking several URLs: check-url.js already takes many URLs in one invocation, and one call beats any fan-out. Covers stack lifecycle ownership, the literal-command dispatch contract, the concurrency ceiling, and merging JSON results."
---

# VPN Egress Orchestrator

Running many reachability tasks in parallel over **one shared container**.
The stack tolerates this — it was configured for it — but only under a
specific contract. Break the contract and agents kill each other's runs.

## First: do you actually need subagents?

`check-url.js` takes **any number of URLs in one invocation**, sharing one
browser. Twenty URLs in one call is faster than twenty subagents, uses no
extra context, and produces one already-merged result.

```bash
MSYS_NO_PATHCONV=1 docker exec playwright-vpn \
  node /scripts/check-url.js url1 url2 url3 … url20
```

**Fan out only when each target needs real per-target work** — reading a
config to explain *why* one failed, cross-referencing a Caddyfile, chasing a
redirect chain, judging whether a page rendered correctly. That's reasoning
work, which parallelizes. Navigation alone does not.

If the task is "check these N URLs", stop here and use `vpn-egress-testing`.

## The contract

### 1. The parent owns the stack. Always.

Subagents **never** run `docker compose`, `tools/stack.sh up`, or
`tools/stack.sh stop`.

Parallel agents managing one shared stack race: two `up` calls collide, and a
`stop` from the first agent to finish kills every run still in flight —
producing timeouts that look exactly like unreachable sites.

Before dispatching anything:

```bash
cd projects/vpn-egress && bash tools/stack.sh up
MSYS_NO_PATHCONV=1 docker exec playwright-vpn node /scripts/check-ip.js
```

Both must pass. `check-ip.js` reporting `ok: false` means every result you are
about to collect is worthless — fix it first (`vpn-egress-setup`).

### 2. Dispatch literal commands, not skill names

Put the **exact command** in each subagent's prompt, along with what pass and
fail look like. Never "use the vpn-egress-testing skill" — subagents may have
no Skill access, and one re-deriving commands will invent a variant that
breaks the contract.

`docker exec` is available to any agent with Bash, which is why this works at
all.

### 3. Group output under one RUN_ID

Set a shared `RUN_ID` so every subagent's output lands under one directory and
you can find it all afterwards:

```bash
MSYS_NO_PATHCONV=1 docker exec -e RUN_ID=sweep-2026-07-30 playwright-vpn \
  node /scripts/check-url.js --label=shard-1 https://a.example
```

Without it, each invocation gets its own timestamp+pid directory — still
collision-free, just scattered.

### 4. Respect the concurrency ceiling

**Four concurrent runs is verified good.** All subagents `docker exec` into
the same `playwright-vpn` container, each launching its own Chromium against
one shared `/dev/shm` (1GB).

Beyond four, shard the work rather than raising the fan-out — give each of
four agents more targets. Exceeding the shm budget produces *timeouts*, which
are indistinguishable from unreachable sites, so the failure quietly corrupts
your results instead of announcing itself.

## Dispatch template

Give each subagent something self-contained:

> Check these URLs from an external vantage point and diagnose each failure.
>
> Run exactly this, from `projects/vpn-egress`:
>
> ```bash
> MSYS_NO_PATHCONV=1 docker exec -e RUN_ID=<run-id> playwright-vpn \
>   node /scripts/check-url.js --label=<shard> --timeout=20000 <urls…>
> ```
>
> stdout is a JSON envelope; stderr is progress. `ok: true` needs status < 400.
>
> - `errorKind: "timeout"` — nothing came back.
> - `errorKind: "refused"` — actively rejected; port or backend problem.
> - `errorKind: "dns"` — name doesn't resolve.
> - status 200 but `tls.issuer` is `Caddy Local Authority` — reachable but no
>   real browser will trust it; ACME issuance never completed. Report this as
>   a failure, not a pass.
>
> Do NOT run `docker compose` or `tools/stack.sh` — the stack is already
> running and managed by the parent. Do NOT write your own check script.
>
> For each failing URL, investigate the cause and report it. Return the raw
> JSON envelope plus your per-URL diagnosis.

Substitute `<run-id>`, `<shard>`, and `<urls…>` before dispatching. The
subagent should never have to fill in a blank.

## Merging results

A subagent's final report is not shown to the user — **relay what matters
yourself.**

1. Parse each returned envelope. Every result has the same shape, so they
   concatenate directly.
2. Reconcile against the input list. **Name every URL no agent conclusively
   answered** — an agent that crashed or timed out leaves a gap, and a gap
   silently dropped reads as a pass.
3. Report per-URL: status, `errorKind`, and `tls.issuer` where it matters.
4. Separate *reachable* from *correct*. A 200 with an untrusted cert is a
   failure users will experience, and it is the most-missed one here.

Screenshots and JSON are under `scripts/out/<RUN_ID>/`. Read them if a
diagnosis needs evidence, then delete — they're disposable and gitignored.

## Teardown

Only after **all** delegated work is done:

```bash
cd projects/vpn-egress && docker compose stop
```

`stop`, not `down` — keeps volumes and the Playwright install. Leave it
running if the user also wants the GUI browser.

Stopping while any subagent is still working produces timeouts that look like
site failures. Confirm every agent has reported before tearing down.

## Failure modes specific to fan-out

| Symptom | Cause |
| --- | --- |
| Several shards time out together, others fine | Concurrency ceiling exceeded — `/dev/shm` exhausted. Re-run the failed shard alone before believing it. |
| All shards fail at once, mid-run | Someone ran `docker compose stop`, or the tunnel dropped. Check `tools/stack.sh health` and re-run. |
| A shard reports `MODULE_NOT_FOUND` on a Windows path | Its command lost `MSYS_NO_PATHCONV=1`. A dispatch-template bug, not a network result. |
| Screenshots missing or overwritten | A subagent wrote its own script with a fixed output path. They shouldn't be writing scripts. |
| Results contradict a known-good domain | Verify the browser is on the tunnel (`check-ip.js`) before trusting *any* of the batch. |

## Related

- `vpn-egress-testing` — one URL, or several in one call. The common case.
- `vpn-egress-setup` — stack won't start.
- `projects/vpn-egress/docs/ARCHITECTURE.md` — why one container is shared and
  what that constrains.
