|
| 1 | +# Agent System — Operator Guide |
| 2 | + |
| 3 | +How the distributed agent network works, how to operate it, and what to |
| 4 | +do when things break. |
| 5 | + |
| 6 | +## The problem |
| 7 | + |
| 8 | +The production server's IP is blocked by the St. John's WAF (F5 BIG-IP). |
| 9 | +The entire domain `map.stjohns.ca` returns a CAPTCHA challenge from the |
| 10 | +server. Agents run on volunteer residential IPs that aren't blocked. |
| 11 | + |
| 12 | +## Architecture |
| 13 | + |
| 14 | +``` |
| 15 | +Volunteer 1 Volunteer 2 Volunteer N |
| 16 | +┌─────────┐ ┌─────────┐ ┌─────────┐ |
| 17 | +│plow-agent│ │plow-agent│ │plow-agent│ |
| 18 | +└────┬─────┘ └────┬─────┘ └────┬─────┘ |
| 19 | + │ signed reports │ │ |
| 20 | + └─────────────────────┼─────────────────────┘ |
| 21 | + ▼ |
| 22 | + ┌───────────────┐ |
| 23 | + │ plow server │ |
| 24 | + │ (FastAPI) │ |
| 25 | + │ │ |
| 26 | + │ coordinator → │ assigns schedules |
| 27 | + │ agent_routes → │ receives reports |
| 28 | + │ collector → │ skips direct fetch |
| 29 | + └───────────────┘ |
| 30 | +``` |
| 31 | + |
| 32 | +**Data flow:** |
| 33 | + |
| 34 | +1. Agent registers with the server (self-registration, generates its own |
| 35 | + ECDSA keypair, sends public key) |
| 36 | +2. Admin approves the agent via `/admin` |
| 37 | +3. Server assigns a fetch schedule — N agents each fetch every `6*N` |
| 38 | + seconds with staggered offsets, so globally there's a fetch every ~6s |
| 39 | +4. Agent fetches AVL data from `map.stjohns.ca` using browser-like headers |
| 40 | +5. Agent signs the response body with its private key and POSTs to |
| 41 | + `/agents/report` |
| 42 | +6. Server verifies the signature, parses the AVL data, inserts into DuckDB |
| 43 | +7. While agents are active (last report <30s ago), the server's own |
| 44 | + collector skips the St. John's source |
| 45 | + |
| 46 | +## Day-to-day operations |
| 47 | + |
| 48 | +### Admin panel |
| 49 | + |
| 50 | +Go to `/admin` on the server. Login with the `ADMIN_PASSWORD` env var. |
| 51 | + |
| 52 | +**What you see:** |
| 53 | + |
| 54 | +- **Collector** section: pause/resume button for St. John's. Use this |
| 55 | + when setting up agents for the first time — pause the collector, get |
| 56 | + agents approved and running, then resume (or leave paused and let agents |
| 57 | + handle it) |
| 58 | +- **Agents** table: name, ID, status, health, IP, last seen, reports |
| 59 | + (success/failed), and approve/revoke buttons |
| 60 | +- **Health** column: `healthy` (green), `degraded` (orange, 5+ consecutive |
| 61 | + failures), `hibernating` (red, 30+ consecutive failures) |
| 62 | + |
| 63 | +### Setting up a new agent |
| 64 | + |
| 65 | +1. Have your volunteer download the binary from GitHub Releases, or pull |
| 66 | + the Docker image |
| 67 | +2. They run it: `./plow-agent --server https://plow.jackharrhy.dev` |
| 68 | +3. It generates a keypair, registers with the server, and prints |
| 69 | + "Waiting for approval..." |
| 70 | +4. You see it appear as "pending" in the admin panel |
| 71 | +5. Click "Approve" |
| 72 | +6. The agent starts fetching and reporting within seconds |
| 73 | + |
| 74 | +### When an agent gets WAF'd |
| 75 | + |
| 76 | +The agent detects consecutive fetch failures and reacts: |
| 77 | + |
| 78 | +- **Failures 1-4**: normal operation, errors reported to server |
| 79 | +- **Failures 5-29**: exponential backoff (6s → 12s → 24s → ... → 10min |
| 80 | + cap). The admin panel shows `degraded` (orange) |
| 81 | +- **Failures 30+**: agent enters **hibernate mode**. It stops fetching |
| 82 | + and instead checks in with the server every 10 minutes. Each checkin |
| 83 | + includes a single probe fetch to test if the block has lifted. The |
| 84 | + admin panel shows `hibernating` (red) |
| 85 | +- **Recovery**: if any fetch succeeds, the agent resets to normal |
| 86 | + operation immediately |
| 87 | + |
| 88 | +You don't need to restart agents manually. They'll come back on their own |
| 89 | +if the WAF unblocks their IP. |
| 90 | + |
| 91 | +### When the server's IP gets unblocked |
| 92 | + |
| 93 | +If you get the city to whitelist the server IP, you can stop using agents |
| 94 | +entirely: |
| 95 | + |
| 96 | +1. Revoke all agents in the admin panel (or just leave them — the server's |
| 97 | + own collector will automatically start fetching when no agents have |
| 98 | + reported in the last 30 seconds) |
| 99 | +2. The collector resumes direct fetching |
| 100 | + |
| 101 | +### Collector pause behavior |
| 102 | + |
| 103 | +The pause button in the admin panel only pauses the **server's direct |
| 104 | +polling** of the St. John's AVL source. It does NOT pause agent report |
| 105 | +processing — agent data still flows in. This is intentional: the pause |
| 106 | +is for the server's own fetching, not for the entire data pipeline. |
| 107 | + |
| 108 | +The pause state is **in-memory only**. If the server restarts, the |
| 109 | +collector resumes automatically. This is deliberate — it's an operational |
| 110 | +toggle, not a persistent config. |
| 111 | + |
| 112 | +## Authentication |
| 113 | + |
| 114 | +Two separate auth systems: |
| 115 | + |
| 116 | +**Admin auth**: password-based. Set `ADMIN_PASSWORD` env var. Stored as |
| 117 | +an HMAC cookie. Used for the `/admin` panel. |
| 118 | + |
| 119 | +**Agent auth**: ECDSA P-256. Each agent generates its own keypair on first |
| 120 | +run. The public key is sent during registration. All subsequent requests |
| 121 | +(checkin, report) are signed: `SHA-256(body || timestamp_bytes)` signed |
| 122 | +with the private key, sent as `X-Agent-Sig` header. The server verifies |
| 123 | +against the stored public key. Timestamps must be within 30 seconds. |
| 124 | + |
| 125 | +## Configuration |
| 126 | + |
| 127 | +All via environment variables (or `.env` file — pydantic-settings handles |
| 128 | +this automatically): |
| 129 | + |
| 130 | +| Variable | Default | Purpose | |
| 131 | +|---|---|---| |
| 132 | +| `ADMIN_PASSWORD` | (none) | Required for admin panel | |
| 133 | +| `DB_PATH` | `/data/plow.db` | DuckDB file location | |
| 134 | +| `SOURCE_ST_JOHNS_ENABLED` | `true` | Enable St. John's source | |
| 135 | +| `SOURCE_MT_PEARL_ENABLED` | `true` | Enable Mt. Pearl source | |
| 136 | +| `SOURCE_PROVINCIAL_ENABLED` | `true` | Enable Provincial source | |
| 137 | + |
| 138 | +See `.env.dist` for the full template. |
| 139 | + |
| 140 | +## Database |
| 141 | + |
| 142 | +The agents table is managed by migrations 003-005: |
| 143 | + |
| 144 | +| Column | Purpose | |
| 145 | +|---|---| |
| 146 | +| `agent_id` | SHA-256 fingerprint of public key (16 hex chars) | |
| 147 | +| `name` | Human-readable name (set by volunteer) | |
| 148 | +| `public_key` | ECDSA P-256 public key PEM | |
| 149 | +| `status` | `pending`, `approved`, or `revoked` | |
| 150 | +| `consecutive_failures` | Resets to 0 on success, increments on failure | |
| 151 | +| `total_reports` | Lifetime successful reports | |
| 152 | +| `failed_reports` | Lifetime failed reports | |
| 153 | +| `last_seen_at` | Last time the agent submitted any report | |
| 154 | +| `ip` | Registration IP (from X-Forwarded-For) | |
| 155 | +| `system_info` | OS/arch from agent | |
| 156 | + |
| 157 | +## Release process |
| 158 | + |
| 159 | +Any push to `main` that touches `agent/**` triggers the GitHub Actions |
| 160 | +workflow `.github/workflows/release-agent.yml`. It: |
| 161 | + |
| 162 | +1. Creates a tag `agent-YYYY.MM.DD-<short-sha>` |
| 163 | +2. Runs goreleaser to cross-compile for linux/darwin/windows (amd64+arm64) |
| 164 | +3. Publishes binaries to a GitHub Release |
| 165 | + |
| 166 | +The server's Docker image is built by the existing |
| 167 | +`.github/workflows/build-and-push.yml` on pushes to `main`. |
| 168 | + |
| 169 | +## File map |
| 170 | + |
| 171 | +``` |
| 172 | +src/where_the_plow/ |
| 173 | + agent_auth.py — ECDSA key generation, signing, verification |
| 174 | + agent_routes.py — /agents/register, /checkin, /report |
| 175 | + admin_routes.py — /admin/login, /agents/*, /collector/pause|resume |
| 176 | + coordinator.py — schedule computation, timestamp validation |
| 177 | + collector.py — background polling, pause checks, agent fallback |
| 178 | + db.py — agent CRUD, consecutive_failures tracking |
| 179 | + migrations/ |
| 180 | + 003_add_agents_table.py |
| 181 | + 004_agent_status.py |
| 182 | + 005_agent_consecutive_failures.py |
| 183 | + static/admin/ — admin panel (HTML/JS/CSS) |
| 184 | +
|
| 185 | +agent/ |
| 186 | + main.go — entry point, backoff/hibernate loop |
| 187 | + client.go — register, checkin, report HTTP calls |
| 188 | + fetch.go — AVL fetch with browser mimicry |
| 189 | + config.go — credential storage (~/.config/plow-agent/) |
| 190 | + crypto.go — ECDSA signing |
| 191 | + .goreleaser.yml — cross-compile config |
| 192 | + build.sh — local dev build |
| 193 | + Dockerfile — container build |
| 194 | + k8s.yaml — Kubernetes manifest |
| 195 | +``` |
0 commit comments