|
| 1 | +# PwrAgent Federation Operator Guide |
| 2 | + |
| 3 | +PwrAgent federation connects multiple PwrAgent profiles through an authenticated |
| 4 | +WebSocket control plane. A gateway accepts connections, clients connect to that |
| 5 | +gateway, and the gateway relays authorized traffic between clients. |
| 6 | + |
| 7 | +Federation is disabled by default. The first supported remote posture is a |
| 8 | +loopback gateway listener published through Cloudflare Tunnel. Direct |
| 9 | +localhost/LAN WebSocket URLs use the same PwrAgent enrollment and peer |
| 10 | +authentication. |
| 11 | + |
| 12 | +## Security Boundaries |
| 13 | + |
| 14 | +Federation uses two independent admission layers: |
| 15 | + |
| 16 | +1. Cloudflare can restrict which traffic reaches the tunnel hostname with IP, |
| 17 | + Access service-token, or mTLS policy. |
| 18 | +2. PwrAgent always requires an enrolled instance identity and a signed |
| 19 | + challenge. Cloudflare headers or certificates never replace PwrAgent peer |
| 20 | + authentication. |
| 21 | + |
| 22 | +Each PwrAgent profile owns an Ed25519 identity key in encrypted desktop secret |
| 23 | +storage. Enrollment invites are short-lived and single-use. The gateway stores |
| 24 | +the enrolled public identity and can revoke it. Revocation closes the active |
| 25 | +gateway connection and blocks reconnects with that identity. |
| 26 | + |
| 27 | +Keep the gateway listener on `127.0.0.1` when using `cloudflared`. Do not expose |
| 28 | +the listener port through a router or host firewall. |
| 29 | + |
| 30 | +## Local Dogfood Setup |
| 31 | + |
| 32 | +Build the desktop app once, then launch isolated profiles: |
| 33 | + |
| 34 | +```sh |
| 35 | +pnpm --filter @pwragent/desktop build |
| 36 | +PWRAGENT_PROFILE=gateway pnpm --filter @pwragent/desktop preview |
| 37 | +PWRAGENT_PROFILE=laptop pnpm --filter @pwragent/desktop preview |
| 38 | +``` |
| 39 | + |
| 40 | +On the gateway profile, open Settings -> Federation and set: |
| 41 | + |
| 42 | +- Mode: `gateway` |
| 43 | +- Listen host: `127.0.0.1` |
| 44 | +- Listen port: `47830` |
| 45 | +- Public URL: `ws://127.0.0.1:47830` |
| 46 | + |
| 47 | +Save, generate an invite, and import it on the laptop profile. The imported |
| 48 | +profile switches to client mode and connects immediately. |
| 49 | + |
| 50 | +Both profiles should list the other instance as Connected. Open the remote |
| 51 | +instance from either profile. A client can also open the gateway or another |
| 52 | +connected client; sibling traffic is relayed by the gateway. |
| 53 | + |
| 54 | +## Cloudflare Tunnel |
| 55 | + |
| 56 | +Cloudflare Tunnel supports WebSockets and connects to the origin without an |
| 57 | +inbound public port. See Cloudflare's |
| 58 | +[Tunnel overview](https://developers.cloudflare.com/tunnel/) and |
| 59 | +[Tunnel WebSocket support](https://developers.cloudflare.com/cloudflare-one/faq/cloudflare-tunnels-faq/). |
| 60 | + |
| 61 | +Create a named tunnel and DNS route: |
| 62 | + |
| 63 | +```sh |
| 64 | +cloudflared tunnel login |
| 65 | +cloudflared tunnel create pwragent-federation |
| 66 | +cloudflared tunnel route dns pwragent-federation pwragent.example.com |
| 67 | +``` |
| 68 | + |
| 69 | +Use a `cloudflared` configuration like: |
| 70 | + |
| 71 | +```yaml |
| 72 | +tunnel: YOUR_TUNNEL_ID |
| 73 | +credentials-file: /absolute/path/to/YOUR_TUNNEL_ID.json |
| 74 | + |
| 75 | +ingress: |
| 76 | + - hostname: pwragent.example.com |
| 77 | + service: http://127.0.0.1:47830 |
| 78 | + - service: http_status:404 |
| 79 | +``` |
| 80 | +
|
| 81 | +Start the tunnel: |
| 82 | +
|
| 83 | +```sh |
| 84 | +cloudflared tunnel run pwragent-federation |
| 85 | +``` |
| 86 | + |
| 87 | +On the gateway, keep the listener at `127.0.0.1:47830` and set Public URL to |
| 88 | +`wss://pwragent.example.com`. Generate new invites after changing the public |
| 89 | +URL because the URL is embedded in each invite. |
| 90 | + |
| 91 | +### Source IP Restriction |
| 92 | + |
| 93 | +An IP allow rule at Cloudflare is a useful outer gate while the gateway is at a |
| 94 | +stable location. Verify the rule against the hostname before relying on it and |
| 95 | +remove or change it deliberately when the connecting laptop moves networks. |
| 96 | +PwrAgent enrollment remains mandatory whether or not the IP rule is active. |
| 97 | + |
| 98 | +### Cloudflare Access Service Token |
| 99 | + |
| 100 | +Cloudflare Access service tokens use the |
| 101 | +`CF-Access-Client-Id` and `CF-Access-Client-Secret` headers. Configure a |
| 102 | +Self-hosted Access application and a Service Auth policy for the federation |
| 103 | +hostname. Cloudflare documents the current flow in |
| 104 | +[Service tokens](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/) |
| 105 | +and [Access policies](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/). |
| 106 | + |
| 107 | +On every client profile, before importing its federation invite: |
| 108 | + |
| 109 | +1. Open Settings -> Federation -> Cloudflare. |
| 110 | +2. Enable Access service auth. |
| 111 | +3. Enter the Access client ID and client secret. |
| 112 | +4. Select Save edge policy. |
| 113 | +5. Import the federation invite. |
| 114 | + |
| 115 | +PwrAgent stores both values in encrypted desktop secret storage and sends them |
| 116 | +only in the WebSocket upgrade request. If either value is missing while the |
| 117 | +mode is enabled, the connector fails closed with a Settings diagnostic. |
| 118 | + |
| 119 | +### Cloudflare Access mTLS |
| 120 | + |
| 121 | +Cloudflare Access can enforce a Service Auth policy with a Valid Certificate or |
| 122 | +Common Name selector. Its current setup and certificate requirements are in |
| 123 | +[Cloudflare Access mTLS](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/mutual-tls-authentication/). |
| 124 | +Availability depends on the Cloudflare account and product configuration; check |
| 125 | +the dashboard before treating mTLS as an available outer gate. |
| 126 | + |
| 127 | +Issue each client a certificate and private key from the CA associated with the |
| 128 | +federation hostname. On the client profile, before importing its invite: |
| 129 | + |
| 130 | +1. Open Settings -> Federation -> Cloudflare. |
| 131 | +2. Enable mTLS. |
| 132 | +3. Paste the PEM client certificate and matching PEM private key. |
| 133 | +4. Select Save edge policy. |
| 134 | +5. Import the federation invite. |
| 135 | + |
| 136 | +The private key stays in encrypted desktop secret storage. PwrAgent supplies |
| 137 | +the certificate and key only during the TLS handshake. Missing certificate |
| 138 | +material causes a closed failure before a WebSocket is opened. |
| 139 | + |
| 140 | +Access service tokens and mTLS can be enabled together when the Access policy |
| 141 | +requires both. |
| 142 | + |
| 143 | +## Remote Operation |
| 144 | + |
| 145 | +A remote window routes its navigation, thread reads, prompt submission, |
| 146 | +steering, interruption, compaction, approvals, model and execution settings, |
| 147 | +environment selection/actions, environment action stop, reviews, forks, |
| 148 | +launchpad materialization, and workspace handoff to the selected instance. |
| 149 | +Backend events and environment setup output stream back with the source |
| 150 | +instance identity. |
| 151 | + |
| 152 | +Global thread search fans out metadata queries to connected peers. Remote |
| 153 | +results carry their instance label and open directly in a window scoped to that |
| 154 | +instance. |
| 155 | + |
| 156 | +Messaging thread browse includes connected remote threads with an instance |
| 157 | +label. Selecting one persists its full federated identity, so prompts, steering, |
| 158 | +interrupts, compaction, settings changes, approvals, status reads, and streamed |
| 159 | +events continue to route to the owning instance. A disconnected remote binding |
| 160 | +remains stored and reports unavailable thread state rather than rebinding to a |
| 161 | +same-named local thread. |
| 162 | + |
| 163 | +Disconnected or revoked peers remain visible for diagnosis, but Open is |
| 164 | +disabled. PwrAgent does not fall back to local execution when a remote target |
| 165 | +is unavailable. |
| 166 | + |
| 167 | +Remote filesystem paths describe the remote machine. Do not assume a path is |
| 168 | +present on the machine displaying the remote window. |
| 169 | + |
| 170 | +## Diagnostics And Revocation |
| 171 | + |
| 172 | +Settings -> Federation shows: |
| 173 | + |
| 174 | +- local mode, listener, public URL, and connector status |
| 175 | +- peer role, status, protocol version, negotiated capabilities, and activity |
| 176 | +- recent connection attempts, accepts, rejects, disconnects, relays, and errors |
| 177 | +- redacted failure details without invites, private keys, or raw credentials |
| 178 | + |
| 179 | +Select Revoke next to a peer to close its active gateway connection and prevent |
| 180 | +that identity from reconnecting. Re-enrollment requires a fresh invite and a |
| 181 | +new instance identity. |
| 182 | + |
| 183 | +Common failures: |
| 184 | + |
| 185 | +- `unknown_peer`: the client identity was never enrolled or local state was |
| 186 | + replaced; generate and import a new invite. |
| 187 | +- `revoked_peer`: the gateway has revoked this identity. |
| 188 | +- `bad_signature`: the stored private key does not match the enrolled public |
| 189 | + identity. |
| 190 | +- HTTP `401` or `403` before a PwrAgent audit event: Cloudflare rejected the |
| 191 | + WebSocket upgrade. Check IP, Access token, or mTLS policy. |
| 192 | +- Missing Cloudflare credential error: enable the edge mode only after storing |
| 193 | + all required fields. |
| 194 | + |
| 195 | +## Threat Model |
| 196 | + |
| 197 | +The design protects against arbitrary Internet clients reaching a tunnel, |
| 198 | +unenrolled PwrAgent instances, replay of used enrollment invites, reconnect by |
| 199 | +revoked identities, and accidental routing of remote commands to the local |
| 200 | +profile. |
| 201 | + |
| 202 | +It does not protect against a compromised enrolled machine, an attacker who can |
| 203 | +use that profile's unlocked desktop secret storage, or commands explicitly |
| 204 | +approved on a remote coding-agent thread. Treat every enrolled peer as able to |
| 205 | +exercise the capabilities shown in Settings and revoke peers that are lost or |
| 206 | +retired. |
0 commit comments