aptove-bridge 0.2.1

A secure bridge between ACP agents and clients
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
# Push Notifications System

The bridge implements a secure push notification system that enables mobile apps (iOS and Android) to receive real-time alerts when agents have new activity, even when the app is in the background or the device is asleep.

## Architecture Overview

```
┌──────────────────┐                 ┌─────────────────┐                 ┌──────────────────┐
│   Mobile App     │                 │     Bridge      │                 │   Push Relay     │
│  (iOS/Android)   │                 │  (Your Machine) │                 │ (Cloudflare CDN) │
└──────────────────┘                 └─────────────────┘                 └──────────────────┘
         │                                    │                                    │
         │  1. Scan QR Code                   │                                    │
         │─────────────────────────────────►  │                                    │
         │    (auth_token + relay_url)        │                                    │
         │                                    │                                    │
         │  2. Request Device Token from OS   │                                    │
         │    (APNs for iOS / FCM for Android)│                                    │
         │◄───────────────────────────────────│                                    │
         │                                    │                                    │
         │  3. Register Device Token          │                                    │
         │    via WebSocket                   │                                    │
         │─────────────────────────────────►  │                                    │
         │    bridge/registerPushToken        │                                    │
         │                                    │                                    │
         │                                    │  4. Forward Registration           │
         │                                    │─────────────────────────────────►  │
         │                                    │    POST /register                  │
         │                                    │    {                               │
         │                                    │      relay_token: auth_token,      │
         │                                    │      device_token: "...",          │
         │                                    │      platform: "ios"               │
         │                                    │    }                               │
         │                                    │                                    │
         │                                    │  5. Store Mapping                  │
         │                                    │◄────────────────────────────────── │
         │                                    │    relay_token → [device_tokens]   │
         │                                    │                                    │
         │                                    │                                    │
         │  [ User interacts, agent responds ] │                                   │
         │                                    │                                    │
         │                                    │  6. Send Push Notification         │
         │                                    │─────────────────────────────────►  │
         │                                    │    POST /push                      │
         │                                    │    {                               │
         │                                    │      relay_token: auth_token,      │
         │                                    │      title: "Agent Name",          │
         │                                    │      body: "New activity"          │
         │                                    │    }                               │
         │                                    │                                    │
         │  7. Push to APNs/FCM              │                                    │
         │◄──────────────────────────────────────────────────────────────────────│
         │    Notification appears!           │                                    │
         │                                    │                                    │
```

## Why a Relay Service?

The bridge never holds APNs (Apple Push Notification service) credentials or FCM (Firebase Cloud Messaging) credentials. Instead:

1. **Security**: Distributing APNs `.p8` keys or FCM service account JSON files to every bridge would be a critical security risk
2. **Simplicity**: Bridge instances only need to know the relay URL and use their existing `auth_token`
3. **Isolation**: Each bridge's `auth_token` serves as its `relay_token`, ensuring devices are isolated per bridge
4. **Centralization**: The relay centralizes credential management and handles the complexity of JWT generation and OAuth2 tokens

## Key Components

### 1. Push Relay (Cloudflare Worker)
- Hosted at `https://push.aptove.com`
- Stores APNs and FCM credentials as encrypted secrets
- Maintains mapping: `relay_token` → list of device tokens
- Forwards notifications to Apple APNs and Google FCM
- Auto-refreshes authentication tokens (JWT for APNs, OAuth2 for FCM)

### 2. Bridge (This Crate)
- Generates secure `auth_token` during startup
- Uses `auth_token` as `relay_token` when communicating with push relay
- Accepts device token registration from mobile apps
- Forwards registration to push relay
- Sends push notifications when agents have activity
- Implements debouncing (30s cooldown) to prevent notification spam

### 3. Mobile App (iOS/Android)
- Scans QR code to get `auth_token` and `relay_url`
- Requests device token from OS (APNs for iOS, FCM for Android)
- Registers device token with bridge via WebSocket
- Receives push notifications when agent has activity
- Can unregister when disconnecting

## Security Model

### Authentication Token Isolation
```
Bridge Instance A                Bridge Instance B
    │                                │
    ├─ auth_token: ABC...            ├─ auth_token: XYZ...
    │                                │
    ├─ relay_token: ABC...           ├─ relay_token: XYZ...
    │                                │
    └─ Devices:                      └─ Devices:
       • iPhone (token: 111...)          • Android (token: 888...)
       • iPad (token: 222...)            • Pixel (token: 999...)

Push Relay Storage (KV):
┌──────────────────────────────────────────────────────┐
│  Key (relay_token)  │  Value (device tokens)         │
├────────────────────┼────────────────────────────────┤
│  ABC...             │  [                             │
│                     │    {platform:"ios",token:"111"},│
│                     │    {platform:"ios",token:"222"} │
│                     │  ]                             │
├────────────────────┼────────────────────────────────┤
│  XYZ...             │  [                             │
│                     │    {platform:"android",         │
│                     │     token:"888"},              │
│                     │    {platform:"android",         │
│                     │     token:"999"}               │
│                     │  ]                             │
└────────────────────┴────────────────────────────────┘
```

**Key Properties:**
- Bridge A cannot send notifications to Bridge B's devices
- Each bridge's devices are isolated by the `auth_token`
- The relay never needs to authenticate the bridge - the token itself provides isolation
- Lost/stolen bridge instances can be blocked at the relay by invalidating their `relay_token`

### Credential Protection
```
┌────────────────────────────────────────────────────────────┐
│                    APNs/FCM Credentials                     │
│                  (NEVER leave the relay)                    │
├────────────────────────────────────────────────────────────┤
│  • APNs Private Key (.p8 file)                             │
│  • APNs Key ID                                              │
│  • APNs Team ID                                             │
│  • FCM Private Key (RSA from service account JSON)         │
│  • FCM Client Email                                         │
│                                                             │
│  Stored as: Cloudflare Worker Secrets (encrypted at rest)  │
│  Access: Relay worker code only                            │
│  Rotation: Via `wrangler secret put` CLI                   │
└────────────────────────────────────────────────────────────┘
```

## QR Code Contents

When the bridge runs with `--qr`, it displays a QR code containing a JSON payload:

### QR Code Data Structure

```json
{
  "url": "wss://192.168.1.100:3001",
  "protocol": "acp",
  "version": "1.0",
  "authToken": "base64-encoded-token-min-32-chars-xxxxxxxxxxxxxxxx",
  "certFingerprint": "SHA256:ABCD1234...",
  "relayUrl": "https://push.aptove.com"
}
```

### Field Descriptions

| Field | Type | Description |
|-------|------|-------------|
| `url` | string | WebSocket URL to connect to the bridge |
| `protocol` | string | Protocol identifier, always "acp" |
| `version` | string | ACP protocol version |
| `authToken` | string | Bridge authentication token (≥32 chars)<br>**Used as `relay_token` for push notifications** |
| `certFingerprint` | string | SHA256 fingerprint of TLS certificate (local mode only) |
| `relayUrl` | string | Push relay service URL<br>**New field for push notifications** |

### What's Different with Push Support?

**Before push notifications:**
```json
{
  "url": "wss://192.168.1.100:3001",
  "protocol": "acp",
  "version": "1.0",
  "authToken": "auth-token-here",
  "certFingerprint": "SHA256:..."
}
```

**With push notifications:**
```json
{
  "url": "wss://192.168.1.100:3001",
  "protocol": "acp",
  "version": "1.0",
  "authToken": "auth-token-here",
  "certFingerprint": "SHA256:...",
  "relayUrl": "https://push.aptove.com"  ← NEW
}
```

The mobile app uses:
- `url` + `authToken` → Connect to bridge WebSocket
- `authToken` + `relayUrl` → Register for push notifications (as `relay_token`)

## Bridge Configuration

### Starting with Push Notifications

```bash
bridge run \
  -a "copilot --acp" \
  --port 3001 \
  --stdio-proxy \
  --push-relay-url https://push.aptove.com
```

### Command-Line Arguments

| Argument | Default | Description |
|----------|---------|-------------|
| `--push-relay-url <URL>` | `https://push.oss.aptov.com` | Push relay service URL |

### Environment Variables

None specifically for push (uses existing `auth_token` from bridge session).

## Message Flow

### Device Registration

**1. Mobile app sends to bridge (WebSocket):**
```json
{
  "method": "bridge/registerPushToken",
  "params": {
    "deviceToken": "apns-device-token-or-fcm-token",
    "platform": "ios",
    "bundleId": "com.aptove.ios"
  }
}
```

**2. Bridge forwards to relay (HTTP):**
```bash
POST https://push.aptove.com/register
Content-Type: application/json

{
  "relay_token": "bridge-auth-token",
  "device_token": "apns-device-token-or-fcm-token",
  "platform": "ios",
  "bundle_id": "com.aptove.ios"
}
```

**3. Relay responds:**
```json
{
  "ok": true,
  "message": "Device registered"
}
```

**4. Bridge confirms to mobile app:**
```json
{
  "result": {
    "success": true
  }
}
```

### Sending Push Notifications

**When agent has new activity:**

**1. Bridge sends to relay:**
```bash
POST https://push.aptove.com/push
Content-Type: application/json

{
  "relay_token": "bridge-auth-token",
  "title": "GitHub Copilot",
  "body": "Your agent has new activity"
}
```

**2. Relay sends to APNs/FCM:**

**For iOS (APNs):**
```bash
POST https://api.push.apple.com/3/device/{device_token}
Authorization: bearer {JWT_TOKEN}
apns-topic: com.aptove.ios

{
  "aps": {
    "alert": {
      "title": "GitHub Copilot",
      "body": "Your agent has new activity"
    },
    "sound": "default",
    "badge": 1
  }
}
```

**For Android (FCM):**
```bash
POST https://fcm.googleapis.com/v1/projects/{project_id}/messages:send
Authorization: Bearer {OAUTH2_TOKEN}

{
  "message": {
    "token": "{device_token}",
    "notification": {
      "title": "GitHub Copilot",
      "body": "Your agent has new activity"
    }
  }
}
```

**3. Device receives notification:**
- Notification appears in system tray
- User can tap to open app
- App reconnects to bridge if needed

## Debouncing Strategy

To prevent notification spam, the bridge implements per-relay-token debouncing:

```rust
// Default cooldown: 30 seconds
const COOLDOWN: Duration = Duration::from_secs(30);

// Debounce tracking
HashMap<relay_token, last_notification_time>
```

**Behavior:**
- First notification: Sent immediately
- Subsequent notifications within 30s: Silently dropped
- After 30s cooldown: Next notification sent

**Example timeline:**
```
T+0s:  Agent responds → Notification sent ✅
T+10s: Agent responds → Dropped (20s remaining) ❌
T+20s: Agent responds → Dropped (10s remaining) ❌
T+35s: Agent responds → Notification sent ✅
```

## Device Unregistration

**Mobile app sends to bridge:**
```json
{
  "method": "bridge/unregisterPushToken",
  "params": {
    "deviceToken": "device-token-to-remove"
  }
}
```

**Bridge forwards to relay:**
```bash
DELETE https://push.aptove.com/register
Content-Type: application/json

{
  "relay_token": "bridge-auth-token",
  "device_token": "device-token-to-remove"
}
```

## Error Handling

### Registration Errors

| Scenario | Bridge Behavior | Mobile App Action |
|----------|-----------------|-------------------|
| Relay is down | Log warning, return error to app | Show error, allow retry |
| Invalid device token | Relay rejects, bridge returns error | Regenerate token, retry |
| Network timeout (10s) | Return timeout error | Retry with backoff |

### Notification Errors

| Scenario | Bridge Behavior | User Impact |
|----------|-----------------|-------------|
| Relay is down | Log warning, continue | No notification (silent failure) |
| No devices registered | Relay returns success | None (expected) |
| APNs/FCM rejects token | Relay removes stale token | Future notifications work |
| Debounced | Drop notification | Reduces spam |

**Philosophy: Push notifications are non-critical**
- Failure to send a notification should never break the core bridge functionality
- Mobile app should not rely solely on push notifications for updates
- WebSocket connection is the primary communication channel

## Testing

### Local Testing Setup

1. **Start bridge with push relay:**
```bash
cd bridge
cargo build --release
./target/release/bridge run \
  -a "copilot --acp" \
  --port 3001 \
  --stdio-proxy \
  --push-relay-url https://push.aptove.com \
  --verbose
```

2. **Scan QR code** with mobile app

3. **Register device token:**
```json
{
  "method": "bridge/registerPushToken",
  "params": {
    "deviceToken": "test-token-from-mobile-os",
    "platform": "ios"
  }
}
```

4. **Trigger agent activity** to test notifications

### Manual Relay Testing

Test the relay directly with curl:

```bash
# 1. Register a test device
curl -X POST https://push.aptove.com/register \
  -H "Content-Type: application/json" \
  -d '{
    "relay_token": "test-token-min-32-chars-xxxxx",
    "device_token": "your-apns-or-fcm-token",
    "platform": "ios"
  }'

# 2. Send test notification
curl -X POST https://push.aptove.com/push \
  -H "Content-Type: application/json" \
  -d '{
    "relay_token": "test-token-min-32-chars-xxxxx",
    "title": "Test Notification",
    "body": "Testing push notifications"
  }'

# 3. Health check
curl https://push.aptove.com/health
```

## Implementation Details

### Bridge Code Structure

```
bridge/src/
├── push.rs              # PushRelayClient implementation
│   ├── register_device()   # Register device token with relay
│   ├── unregister_device() # Remove device token from relay
│   └── notify()            # Send push notification (with debouncing)
│
├── main.rs              # CLI argument parsing
│   └── --push-relay-url    # Relay URL configuration
│
├── session.rs           # WebSocket message handling
│   ├── bridge/registerPushToken   # Handle registration from app
│   └── bridge/unregisterPushToken # Handle unregistration from app
│
└── pairing.rs           # QR code generation
    └── relayUrl field   # Include relay URL in QR code
```

### Relay Code Structure (Cloudflare Worker)

```
cf-push-relay/src/
├── index.ts       # Main worker entry point
├── router.ts      # HTTP request routing
├── apns.ts        # Apple Push Notification service
├── fcm.ts         # Firebase Cloud Messaging
├── kv.ts          # Cloudflare KV storage
└── types.ts       # TypeScript interfaces
```

## Monitoring and Debugging

### Bridge Logs

```bash
# Enable verbose logging
bridge run --verbose ...

# Sample log output:
INFO  📱 Registering ios device token with push relay
DEBUG Push relay URL: https://push.aptove.com/register
INFO  ✅ Device token registered with push relay
INFO  🔔 Sending push notification via relay for agent 'GitHub Copilot'
DEBUG Push notification throttled (15s remaining)
INFO  ✅ Push notification sent via relay
```

### Relay Logs

```bash
# Tail Cloudflare Worker logs
cd cf-push-relay
npx wrangler tail

# Sample output:
[INFO] POST /register - ios device registered for relay_token: abc...
[INFO] POST /push - sending to 1 device(s) for relay_token: abc...
[INFO] APNs response: 200 OK
```

## Best Practices

1. **Always use HTTPS** for relay URL in production
2. **Implement retry logic** in mobile apps for registration
3. **Handle token refresh** when OS regenerates device tokens
4. **Test with real devices** - simulators don't support push notifications
5. **Monitor relay health** - set up alerts for downtime
6. **Rotate credentials** periodically (APNs keys, FCM service accounts)
7. **Use sandbox mode** for development (set `APNS_SANDBOX=true` in relay)

## Troubleshooting

### No notifications received

1. Check device is registered:
   ```bash
   # Bridge logs should show
   INFO ✅ Device token registered with push relay
   ```

2. Verify relay is reachable:
   ```bash
   curl https://push.aptove.com/health
   ```

3. Check debouncing isn't blocking:
   ```bash
   # Bridge logs show
   DEBUG Push notification throttled (Xs remaining)
   ```

4. Verify APNs/FCM credentials in relay:
   ```bash
   npx wrangler secret list
   ```

### Registration fails

1. Check relay URL is correct in bridge runup
2. Verify network connectivity
3. Check relay logs for errors:
   ```bash
   npx wrangler tail
   ```

### Notifications delayed

- Expected: APNs/FCM may delay notifications when device is in power-saving mode
- Check debouncing isn't too aggressive (30s default)
- Verify relay cron job is running (refreshes auth tokens every 45 min)

## Future Enhancements

- [ ] Support rich notifications (images, actions)
- [ ] Custom notification sounds
- [ ] Per-agent notification preferences
- [ ] Notification history/badges
- [ ] Silent notifications for background sync
- [ ] Rate limiting by device (not just relay_token)
- [ ] Analytics dashboard for notification delivery