Description
## Problem
`POST /api/v1/air/provision` regenerates the WireGuard keypair every time it is called, even for peers that already exist in mesh.yaml. Combined with `auto_apply: true`, this silently breaks WireGuard connectivity for existing peers.
This caused repeated key mismatches during the Brussels hub migration — every `hub apply` or hydracluster provision call triggered a new keypair.
## Fix
Make the endpoint truly idempotent: if the peer already exists in mesh.yaml, return the existing config (with the existing private key) instead of generating a new keypair. Only generate a new keypair when the peer is genuinely new.
## Files
- Hydraguard repo: air provision handler
`/api/v1/air/provision` and `/api/v1/headipad/provision` generated a keypair **before** checking whether the peer existed, then overwrote its stored public key. With `auto_apply: true` the new hub config applied immediately, so a call that read like a lookup silently disconnected a peer still holding the old private key.
### The fix proposed here was not implementable
The issue asks to "return the existing config (with the existing private key)". That cannot be done: `mesh.Air` stores only `PublicKey`. The private key is shown once at creation and never persisted — by design — so there is nothing to hand back.
The closest correct behaviour is to refuse, and say how to proceed deliberately:
- an existing peer now returns **409** and is left untouched
- re-keying requires an explicit `"force": true`, logged as a warning since the device is disconnected until updated
- key generation moved after the existence check, so nothing is generated unless needed
Same change applied to the head-iPad endpoint, which had the identical bug.
### Verified on the live hub
```
POST /api/v1/air/provision {"name":"chunky-turnip-23"}
HTTP 409
{"error":"air peer \"chunky-turnip-23\" already exists on 10.10.100.16/32; its private
key was shown only at creation and is not stored. Re-send with \"force\": true to issue
a new keypair, which will disconnect the peer until the device is updated."}
key UNCHANGED — peer not disconnected
wg peers: 24
```
### The API service no longer needs to be kept stopped for this reason
The runbook's standing instruction to keep `hydraguard` stopped existed because of this bug. That warning has been replaced.
### Two things found while fixing it
**`keygen.Generate` was untestable.** It shells out to `wg genkey`, so provisioning could not be tested anywhere without wireguard-tools installed. Now overridable via `SetGenerator`/`ResetGenerator`, matching the existing `wireguard.SetApplier` convention. The regression tests fail against the old behaviour with "the peer's key changed on a call that should have been a no-op".
**The runbook's mesh table was wrong and caused a real mistake.** It listed peers as `air-001`, `air-tvl-one` and so on; the actual IDs are bare (`001`, `tvl-one`). Trusting it, I provisioned `air-001` against the live hub while testing this fix — which correctly found no such peer and created a new one at `10.10.100.17`. Removed and reapplied; the mesh is back to 24 peers. The table has been regenerated from the live mesh, with the command to regenerate it so the next update is a paste rather than a transcription.