# Vpn Egress Orchestrator

> 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.

- Skill: `therocksss/vpn-egress-orchestrator` (Agent Skill)
- Install (CLI): `npx skillmds@latest add therocksss/vpn-egress-orchestrator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/therocksss/vpn-egress-orchestrator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: THEROCKSSS (https://skillmd.com/u/therocksss)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/therocksss/vpn-egress-orchestrator

---


# 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`).

Each shard also verifies the tunnel for itself before touching a URL, so an
off-tunnel fan-out produces refusals rather than a batch of confident wrong
answers. Checking here first is still worth it: it fails once, up front,
instead of N times across N dispatched agents.

**Do not run `tools/exit.sh` while a sweep is in flight.** Changing the exit
city recreates gluetun, which destroys the network namespace every shard is
using — same class of damage as `docker compose stop`, and the half of the
results taken before the switch came from a different country than the half
after. Pick the exit city *before* dispatching.

### 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.
>
> Read the exit code before interpreting anything:
> - `0` — checked, at least one URL was reachable.
> - `1` — checked, everything failed. A real result about the URLs.
> - `2` — **no answer was obtained.** Either a usage error, or the tunnel
>   gate refused to run (`blocked: true`, `total: 0` in the envelope).
>   Report that you could not test. Never report these URLs as down.
>
> Do NOT run `docker compose`, `tools/stack.sh`, or `tools/exit.sh` — the
> stack is already running and managed by the parent. Do NOT write your own
> check script. Do NOT add `--allow-off-tunnel` to make a refusal go away;
> it disables the guarantee that results came from outside the network.
>
> 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. **Check `blocked` on every envelope first.** A shard with `blocked: true`
   tested nothing. Its URLs are unanswered, not down — fold them into the gap
   list in step 3, never into the failure list.
3. Reconcile against the input list. **Name every URL no agent conclusively
   answered** — an agent that crashed, timed out, or was blocked leaves a gap,
   and a gap silently dropped reads as a pass.
4. **Confirm every shard came from the same exit.** Compare `tunnel.ip` and
   `tunnel.server` across envelopes. If they differ, the tunnel moved
   mid-sweep and the results are not one comparable set — say so rather than
   merging them silently.
5. Report per-URL: status, `errorKind`, and `tls.issuer` where it matters.
6. Separate *reachable* from *correct*. A 200 with an untrusted cert is a
   failure users will experience, and it is the most-missed one here.
7. State the vantage point in the summary — exit city and country from
   `tunnel`. A reachability claim without it can't be interpreted later.

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

**Known bug — evidence can be overwritten when shards share a `RUN_ID`.**
Screenshot filenames are de-duplicated per *process*, not per run directory,
so if the same host appears in two shards, the second overwrites the first's
screenshot with no error. The JSON results are unaffected. Until it's fixed,
shard by host (never split one host across shards) if screenshots matter.

## Teardown

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

```bash
cd projects/vpn-egress && bash tools/stack.sh stop
```

Use the tool, not a raw `docker compose stop`. The containers may be owned by
a larger compose project that `include`s this repo's file, in which case a
plain `docker compose stop` run from this directory matches nothing and
**silently succeeds without stopping anything**. `tools/stack.sh` resolves the
owning project first.

`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 `tools/exit.sh`, or the tunnel dropped. Check `tools/stack.sh health` and re-run. |
| Shards return `blocked: true` and exit 2 | The tunnel gate refused — egress wasn't confirmed. Nothing was tested. Fix the tunnel, then re-run; do not report those URLs as down. |
| Shards disagree on `tunnel.ip` | The exit moved mid-sweep. The results describe two different vantage points and must not be merged as one set. |
| 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.

