agent-relay 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +158 -0
  2. package/dist/bridge/config.d.ts +41 -0
  3. package/dist/bridge/config.d.ts.map +1 -0
  4. package/dist/bridge/config.js +143 -0
  5. package/dist/bridge/config.js.map +1 -0
  6. package/dist/bridge/index.d.ts +10 -0
  7. package/dist/bridge/index.d.ts.map +1 -0
  8. package/dist/bridge/index.js +10 -0
  9. package/dist/bridge/index.js.map +1 -0
  10. package/dist/bridge/multi-project-client.d.ts +99 -0
  11. package/dist/bridge/multi-project-client.d.ts.map +1 -0
  12. package/dist/bridge/multi-project-client.js +386 -0
  13. package/dist/bridge/multi-project-client.js.map +1 -0
  14. package/dist/bridge/spawner.d.ts +46 -0
  15. package/dist/bridge/spawner.d.ts.map +1 -0
  16. package/dist/bridge/spawner.js +223 -0
  17. package/dist/bridge/spawner.js.map +1 -0
  18. package/dist/bridge/types.d.ts +55 -0
  19. package/dist/bridge/types.d.ts.map +1 -0
  20. package/dist/bridge/types.js +6 -0
  21. package/dist/bridge/types.js.map +1 -0
  22. package/dist/bridge/utils.d.ts +30 -0
  23. package/dist/bridge/utils.d.ts.map +1 -0
  24. package/dist/bridge/utils.js +54 -0
  25. package/dist/bridge/utils.js.map +1 -0
  26. package/dist/cli/index.js +564 -5
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/daemon/agent-registry.d.ts.map +1 -1
  29. package/dist/daemon/agent-registry.js +6 -1
  30. package/dist/daemon/agent-registry.js.map +1 -1
  31. package/dist/daemon/connection.d.ts +22 -0
  32. package/dist/daemon/connection.d.ts.map +1 -1
  33. package/dist/daemon/connection.js +59 -13
  34. package/dist/daemon/connection.js.map +1 -1
  35. package/dist/daemon/router.d.ts +27 -0
  36. package/dist/daemon/router.d.ts.map +1 -1
  37. package/dist/daemon/router.js +108 -3
  38. package/dist/daemon/router.js.map +1 -1
  39. package/dist/daemon/server.d.ts +8 -0
  40. package/dist/daemon/server.d.ts.map +1 -1
  41. package/dist/daemon/server.js +95 -23
  42. package/dist/daemon/server.js.map +1 -1
  43. package/dist/dashboard/metrics.d.ts +105 -0
  44. package/dist/dashboard/metrics.d.ts.map +1 -0
  45. package/dist/dashboard/metrics.js +192 -0
  46. package/dist/dashboard/metrics.js.map +1 -0
  47. package/dist/dashboard/needs-attention.d.ts +24 -0
  48. package/dist/dashboard/needs-attention.d.ts.map +1 -0
  49. package/dist/dashboard/needs-attention.js +78 -0
  50. package/dist/dashboard/needs-attention.js.map +1 -0
  51. package/dist/dashboard/public/bridge.html +1272 -0
  52. package/dist/dashboard/public/index.html +2017 -879
  53. package/dist/dashboard/public/js/app.js +184 -0
  54. package/dist/dashboard/public/js/app.js.map +7 -0
  55. package/dist/dashboard/public/metrics.html +999 -0
  56. package/dist/dashboard/server.d.ts +13 -0
  57. package/dist/dashboard/server.d.ts.map +1 -1
  58. package/dist/dashboard/server.js +568 -13
  59. package/dist/dashboard/server.js.map +1 -1
  60. package/dist/dashboard/start.js +1 -1
  61. package/dist/dashboard/start.js.map +1 -1
  62. package/dist/dashboard-v2/index.d.ts +10 -0
  63. package/dist/dashboard-v2/index.d.ts.map +1 -0
  64. package/dist/dashboard-v2/index.js +54 -0
  65. package/dist/dashboard-v2/index.js.map +1 -0
  66. package/dist/dashboard-v2/lib/api.d.ts +95 -0
  67. package/dist/dashboard-v2/lib/api.d.ts.map +1 -0
  68. package/dist/dashboard-v2/lib/api.js +270 -0
  69. package/dist/dashboard-v2/lib/api.js.map +1 -0
  70. package/dist/dashboard-v2/lib/colors.d.ts +61 -0
  71. package/dist/dashboard-v2/lib/colors.d.ts.map +1 -0
  72. package/dist/dashboard-v2/lib/colors.js +198 -0
  73. package/dist/dashboard-v2/lib/colors.js.map +1 -0
  74. package/dist/dashboard-v2/lib/hierarchy.d.ts +74 -0
  75. package/dist/dashboard-v2/lib/hierarchy.d.ts.map +1 -0
  76. package/dist/dashboard-v2/lib/hierarchy.js +196 -0
  77. package/dist/dashboard-v2/lib/hierarchy.js.map +1 -0
  78. package/dist/dashboard-v2/types/index.d.ts +154 -0
  79. package/dist/dashboard-v2/types/index.d.ts.map +1 -0
  80. package/dist/dashboard-v2/types/index.js +6 -0
  81. package/dist/dashboard-v2/types/index.js.map +1 -0
  82. package/dist/storage/adapter.d.ts +21 -1
  83. package/dist/storage/adapter.d.ts.map +1 -1
  84. package/dist/storage/adapter.js +36 -0
  85. package/dist/storage/adapter.js.map +1 -1
  86. package/dist/storage/sqlite-adapter.d.ts +34 -0
  87. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  88. package/dist/storage/sqlite-adapter.js +253 -12
  89. package/dist/storage/sqlite-adapter.js.map +1 -1
  90. package/dist/utils/agent-config.d.ts +45 -0
  91. package/dist/utils/agent-config.d.ts.map +1 -0
  92. package/dist/utils/agent-config.js +118 -0
  93. package/dist/utils/agent-config.js.map +1 -0
  94. package/dist/wrapper/client.d.ts +8 -0
  95. package/dist/wrapper/client.d.ts.map +1 -1
  96. package/dist/wrapper/client.js +26 -0
  97. package/dist/wrapper/client.js.map +1 -1
  98. package/dist/wrapper/parser.d.ts +17 -0
  99. package/dist/wrapper/parser.d.ts.map +1 -1
  100. package/dist/wrapper/parser.js +334 -10
  101. package/dist/wrapper/parser.js.map +1 -1
  102. package/dist/wrapper/tmux-wrapper.d.ts +37 -2
  103. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  104. package/dist/wrapper/tmux-wrapper.js +178 -18
  105. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  106. package/docs/AGENTS.md +105 -0
  107. package/docs/ARCHITECTURE_DECISIONS.md +175 -0
  108. package/docs/COMPETITIVE_ANALYSIS.md +897 -0
  109. package/docs/DESIGN_BRIDGE_STAFFING.md +878 -0
  110. package/docs/MONETIZATION.md +1679 -0
  111. package/docs/agent-relay-snippet.md +61 -0
  112. package/docs/dashboard-v2-plan.md +179 -0
  113. package/package.json +5 -2
@@ -0,0 +1,1679 @@
1
+ # Agent Relay Monetization Strategy
2
+
3
+ > RFC: Open Core Model with Tiered Features
4
+
5
+ **Status**: Draft
6
+ **Author**: Khaliq Gant
7
+ **Created**: 2025-12-26
8
+ **Last Updated**: 2025-12-26
9
+
10
+ ---
11
+
12
+ ## Executive Summary
13
+
14
+ This document outlines a monetization strategy for Agent Relay using an **Open Core** model. The core messaging functionality remains MIT-licensed and free, while premium features targeting teams and enterprises are offered through paid tiers.
15
+
16
+ ### Recommended Pricing Tiers
17
+
18
+ | Tier | Price | Target User |
19
+ |------|-------|-------------|
20
+ | **Community** | Free | Individual developers, OSS projects |
21
+ | **Pro** | $29-49/month per machine | Professional developers, small teams |
22
+ | **Team** | $99-199/month | Engineering teams, multi-machine setups |
23
+ | **Enterprise** | Custom | Large organizations, compliance needs |
24
+
25
+ ---
26
+
27
+ ## Table of Contents
28
+
29
+ 1. [Market Analysis](#1-market-analysis)
30
+ 2. [Feature Matrix](#2-feature-matrix)
31
+ 3. [Technical Specifications](#3-technical-specifications)
32
+ - [3.1 Licensing System](#31-licensing-system)
33
+ - [3.2 Authentication & API Keys](#32-authentication--api-keys)
34
+ - [3.3 TLS Encryption](#33-tls-encryption)
35
+ - [3.4 Scale Optimizations](#34-scale-optimizations)
36
+ - [3.5 Message Retention](#35-message-retention)
37
+ - [3.6 Webhooks](#36-webhooks)
38
+ 4. [Implementation Roadmap](#4-implementation-roadmap)
39
+ 5. [Pricing Justification](#5-pricing-justification)
40
+ 6. [Risks & Mitigations](#6-risks--mitigations)
41
+
42
+ ---
43
+
44
+ ## 1. Market Analysis
45
+
46
+ ### Target Users
47
+
48
+ | Segment | Size | Willingness to Pay | Key Needs |
49
+ |---------|------|-------------------|-----------|
50
+ | **Hobbyists** | Large | Low | Free, easy setup |
51
+ | **Indie Developers** | Medium | Medium | Reliability, simple pricing |
52
+ | **Startups** | Medium | Medium-High | Scale, integrations |
53
+ | **Enterprise** | Small | High | Security, compliance, support |
54
+
55
+ ### Competitive Landscape
56
+
57
+ | Competitor | Model | Price Range | Differentiator |
58
+ |-----------|-------|-------------|----------------|
59
+ | LangGraph | Open Core | Free - $500/mo | Workflow orchestration |
60
+ | CrewAI | SaaS | Free - Custom | Pre-built agent roles |
61
+ | AutoGen | MIT (Microsoft) | Free | Deep MS integration |
62
+ | **Agent Relay** | Open Core | Free - Custom | Real-time messaging, CLI-native |
63
+
64
+ ### Our Advantages
65
+
66
+ 1. **Zero-modification integration**: Works with any CLI agent via output patterns
67
+ 2. **Sub-5ms latency**: Unix socket architecture
68
+ 3. **Simple mental model**: Relay messaging, not workflow orchestration
69
+ 4. **Composable**: Works alongside other tools (Mimir, Beads, etc.)
70
+
71
+ ---
72
+
73
+ ## 2. Feature Matrix
74
+
75
+ ### Community Tier (Free)
76
+
77
+ | Feature | Limit | Notes |
78
+ |---------|-------|-------|
79
+ | Agents | 10 | Per daemon instance |
80
+ | Message throughput | 100 msg/sec | Soft limit |
81
+ | Message retention | 7 days | SQLite storage |
82
+ | Dashboard | Basic | Real-time view |
83
+ | Storage | SQLite only | Local file |
84
+ | Transport | Unix socket | Single machine |
85
+ | Support | Community | GitHub issues |
86
+
87
+ ### Pro Tier ($29-49/month)
88
+
89
+ | Feature | Limit | Notes |
90
+ |---------|-------|-------|
91
+ | Agents | 100 | Per daemon instance |
92
+ | Message throughput | 1,000 msg/sec | Optimized routing |
93
+ | Message retention | 90 days | Configurable |
94
+ | Dashboard | Enhanced | Metrics, analytics |
95
+ | **Authentication** | API keys | Per-agent keys |
96
+ | **TLS encryption** | Full | Socket + storage |
97
+ | **Webhooks** | 10 endpoints | HTTP callbacks |
98
+ | Support | Email | 48hr response |
99
+
100
+ ### Team Tier ($99-199/month)
101
+
102
+ | Feature | Limit | Notes |
103
+ |---------|-------|-------|
104
+ | Agents | 500 | Across machines |
105
+ | Message throughput | 5,000 msg/sec | Distributed |
106
+ | Message retention | 365 days | Configurable |
107
+ | **Multi-machine** | TCP transport | Cluster mode |
108
+ | **Team dashboard** | Shared | Role-based access |
109
+ | **Audit logs** | Full | Compliance-ready |
110
+ | **SSO** | SAML/OIDC | GitHub, Google, Okta |
111
+ | Support | Priority | 24hr response |
112
+
113
+ ### Enterprise Tier (Custom)
114
+
115
+ | Feature | Limit | Notes |
116
+ |---------|-------|-------|
117
+ | Agents | Unlimited | - |
118
+ | Message throughput | Unlimited | - |
119
+ | Message retention | Unlimited | - |
120
+ | **Air-gapped** | Offline license | No phone-home |
121
+ | **Compliance** | SOC2, HIPAA | Documentation |
122
+ | **Custom SLA** | 99.9%+ | Contractual |
123
+ | **Dedicated support** | Slack/Teams | 4hr response |
124
+ | **Custom features** | 2/year | Built to spec |
125
+
126
+ ---
127
+
128
+ ## 3. Technical Specifications
129
+
130
+ ### 3.1 Licensing System
131
+
132
+ #### Overview
133
+
134
+ A lightweight license validation system that:
135
+ - Validates license keys on daemon startup
136
+ - Enforces feature gates at runtime
137
+ - Tracks usage for billing (optional telemetry)
138
+ - Supports offline/air-gapped mode for Enterprise
139
+
140
+ #### Architecture
141
+
142
+ ```
143
+ src/
144
+ licensing/
145
+ index.ts # Public API
146
+ license-validator.ts # Key validation
147
+ feature-flags.ts # Feature gating
148
+ usage-tracker.ts # Telemetry (opt-in)
149
+ offline-license.ts # Air-gapped support
150
+ ```
151
+
152
+ #### License Key Format
153
+
154
+ ```
155
+ ar_[tier]_[random]_[checksum]
156
+
157
+ Examples:
158
+ ar_pro_a1b2c3d4e5f6g7h8_x9y0
159
+ ar_team_k8j7h6g5f4d3s2a1_m3n4
160
+ ar_ent_q1w2e3r4t5y6u7i8_p9o0
161
+ ```
162
+
163
+ #### Validation Flow
164
+
165
+ ```typescript
166
+ interface LicenseInfo {
167
+ key: string;
168
+ tier: 'community' | 'pro' | 'team' | 'enterprise';
169
+ validUntil: Date;
170
+ features: string[];
171
+ limits: {
172
+ maxAgents: number;
173
+ maxMessagesPerSecond: number;
174
+ retentionDays: number;
175
+ };
176
+ offline?: boolean; // Enterprise air-gapped
177
+ }
178
+
179
+ async function validateLicense(key: string): Promise<LicenseInfo> {
180
+ // 1. Check local cache
181
+ const cached = await licenseCache.get(key);
182
+ if (cached && !isExpired(cached)) {
183
+ return cached;
184
+ }
185
+
186
+ // 2. Validate with license server (unless offline mode)
187
+ if (!isOfflineLicense(key)) {
188
+ const remote = await fetchLicense(key);
189
+ await licenseCache.set(key, remote);
190
+ return remote;
191
+ }
192
+
193
+ // 3. Validate offline license signature
194
+ return validateOfflineLicense(key);
195
+ }
196
+ ```
197
+
198
+ #### Feature Flags
199
+
200
+ ```typescript
201
+ // licensing/feature-flags.ts
202
+
203
+ export const TIER_FEATURES = {
204
+ community: [
205
+ 'basic_messaging',
206
+ 'dashboard_basic',
207
+ 'sqlite_storage',
208
+ ],
209
+ pro: [
210
+ // Includes all community features
211
+ 'authentication',
212
+ 'api_keys',
213
+ 'tls_encryption',
214
+ 'storage_encryption',
215
+ 'extended_retention',
216
+ 'webhooks',
217
+ 'high_throughput',
218
+ 'dashboard_metrics',
219
+ ],
220
+ team: [
221
+ // Includes all pro features
222
+ 'multi_machine',
223
+ 'tcp_transport',
224
+ 'team_dashboard',
225
+ 'role_based_access',
226
+ 'audit_logs',
227
+ 'sso_saml',
228
+ 'sso_oidc',
229
+ ],
230
+ enterprise: [
231
+ // Includes all team features
232
+ 'unlimited_retention',
233
+ 'offline_license',
234
+ 'custom_sla',
235
+ 'dedicated_support',
236
+ ],
237
+ } as const;
238
+
239
+ export function hasFeature(tier: string, feature: string): boolean {
240
+ const tierIndex = ['community', 'pro', 'team', 'enterprise'].indexOf(tier);
241
+
242
+ for (let i = tierIndex; i >= 0; i--) {
243
+ const tierName = ['community', 'pro', 'team', 'enterprise'][i];
244
+ if (TIER_FEATURES[tierName].includes(feature)) {
245
+ return true;
246
+ }
247
+ }
248
+
249
+ return false;
250
+ }
251
+ ```
252
+
253
+ #### Database Schema
254
+
255
+ ```sql
256
+ -- License tracking (local cache)
257
+ CREATE TABLE license_cache (
258
+ key_hash TEXT PRIMARY KEY,
259
+ tier TEXT NOT NULL,
260
+ valid_until INTEGER NOT NULL,
261
+ features TEXT NOT NULL, -- JSON array
262
+ limits TEXT NOT NULL, -- JSON object
263
+ cached_at INTEGER NOT NULL
264
+ );
265
+
266
+ -- Usage tracking (for metered billing, opt-in)
267
+ CREATE TABLE usage_daily (
268
+ date TEXT NOT NULL, -- '2025-01-15'
269
+ metric TEXT NOT NULL, -- 'agents_peak', 'messages_sent'
270
+ value INTEGER NOT NULL,
271
+ PRIMARY KEY (date, metric)
272
+ );
273
+ ```
274
+
275
+ ---
276
+
277
+ ### 3.2 Authentication & API Keys
278
+
279
+ #### Overview
280
+
281
+ Pro tier adds optional authentication:
282
+ - API keys for programmatic access
283
+ - Per-agent identity verification
284
+ - Key rotation and revocation
285
+ - Usage tracking per key
286
+
287
+ #### Protocol Changes
288
+
289
+ **HELLO payload extension** (`protocol/types.ts`):
290
+
291
+ ```typescript
292
+ export interface HelloPayload {
293
+ agent: string;
294
+ capabilities: { ... };
295
+
296
+ // NEW: Authentication
297
+ auth?: {
298
+ type: 'api_key' | 'token';
299
+ credential: string;
300
+ };
301
+
302
+ // Existing optional fields
303
+ cli?: string;
304
+ program?: string;
305
+ model?: string;
306
+ }
307
+ ```
308
+
309
+ **WELCOME payload extension**:
310
+
311
+ ```typescript
312
+ export interface WelcomePayload {
313
+ session_id: string;
314
+ server: { ... };
315
+
316
+ // NEW: License info
317
+ license?: {
318
+ tier: string;
319
+ features: string[];
320
+ limits: {
321
+ maxAgents: number;
322
+ retentionDays: number;
323
+ };
324
+ };
325
+ }
326
+ ```
327
+
328
+ **New ERROR codes**:
329
+
330
+ ```typescript
331
+ export type ErrorCode =
332
+ | 'BAD_REQUEST'
333
+ | 'UNAUTHORIZED' // Missing or invalid credentials
334
+ | 'FORBIDDEN' // Valid credentials, insufficient permissions
335
+ | 'QUOTA_EXCEEDED' // Agent/message limit reached
336
+ | 'NOT_FOUND'
337
+ | 'INTERNAL'
338
+ | 'RESUME_TOO_OLD';
339
+ ```
340
+
341
+ #### API Key Management
342
+
343
+ **Database schema**:
344
+
345
+ ```sql
346
+ CREATE TABLE api_keys (
347
+ id TEXT PRIMARY KEY,
348
+ key_hash TEXT UNIQUE NOT NULL, -- SHA256(key)
349
+ key_prefix TEXT NOT NULL, -- First 8 chars for display
350
+ name TEXT NOT NULL,
351
+ description TEXT,
352
+
353
+ -- Permissions
354
+ scopes TEXT NOT NULL DEFAULT '["send","receive"]', -- JSON array
355
+ allowed_agents TEXT, -- JSON array, null = all
356
+
357
+ -- Limits
358
+ rate_limit_per_minute INTEGER DEFAULT 1000,
359
+
360
+ -- Metadata
361
+ created_at INTEGER NOT NULL,
362
+ created_by TEXT,
363
+ last_used_at INTEGER,
364
+ expires_at INTEGER,
365
+ revoked_at INTEGER,
366
+
367
+ -- Usage counters (updated periodically)
368
+ total_messages INTEGER DEFAULT 0
369
+ );
370
+
371
+ CREATE INDEX idx_api_keys_hash ON api_keys (key_hash);
372
+ CREATE INDEX idx_api_keys_expires ON api_keys (expires_at) WHERE revoked_at IS NULL;
373
+ ```
374
+
375
+ **Key generation**:
376
+
377
+ ```typescript
378
+ // licensing/api-keys.ts
379
+
380
+ import crypto from 'crypto';
381
+
382
+ export interface ApiKeyCreate {
383
+ name: string;
384
+ description?: string;
385
+ scopes?: ('send' | 'receive' | 'admin')[];
386
+ allowedAgents?: string[];
387
+ expiresIn?: number; // milliseconds
388
+ }
389
+
390
+ export interface ApiKey {
391
+ id: string;
392
+ key: string; // Only returned on creation
393
+ keyPrefix: string;
394
+ name: string;
395
+ scopes: string[];
396
+ createdAt: Date;
397
+ expiresAt?: Date;
398
+ }
399
+
400
+ export function generateApiKey(): { key: string; hash: string; prefix: string } {
401
+ // Format: ar_key_[32 random chars]
402
+ const random = crypto.randomBytes(24).toString('base64url');
403
+ const key = `ar_key_${random}`;
404
+ const hash = crypto.createHash('sha256').update(key).digest('hex');
405
+ const prefix = key.substring(0, 15); // 'ar_key_XXXXXXX'
406
+
407
+ return { key, hash, prefix };
408
+ }
409
+
410
+ export async function createApiKey(
411
+ storage: StorageAdapter,
412
+ options: ApiKeyCreate
413
+ ): Promise<ApiKey> {
414
+ const { key, hash, prefix } = generateApiKey();
415
+ const id = crypto.randomUUID();
416
+ const now = Date.now();
417
+
418
+ await storage.exec(`
419
+ INSERT INTO api_keys (id, key_hash, key_prefix, name, description, scopes, allowed_agents, created_at, expires_at)
420
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
421
+ `, [
422
+ id,
423
+ hash,
424
+ prefix,
425
+ options.name,
426
+ options.description || null,
427
+ JSON.stringify(options.scopes || ['send', 'receive']),
428
+ options.allowedAgents ? JSON.stringify(options.allowedAgents) : null,
429
+ now,
430
+ options.expiresIn ? now + options.expiresIn : null,
431
+ ]);
432
+
433
+ return {
434
+ id,
435
+ key, // Only time the full key is available
436
+ keyPrefix: prefix,
437
+ name: options.name,
438
+ scopes: options.scopes || ['send', 'receive'],
439
+ createdAt: new Date(now),
440
+ expiresAt: options.expiresIn ? new Date(now + options.expiresIn) : undefined,
441
+ };
442
+ }
443
+
444
+ export async function validateApiKey(
445
+ storage: StorageAdapter,
446
+ key: string
447
+ ): Promise<{ valid: boolean; reason?: string; keyInfo?: any }> {
448
+ const hash = crypto.createHash('sha256').update(key).digest('hex');
449
+
450
+ const row = await storage.get(`
451
+ SELECT * FROM api_keys
452
+ WHERE key_hash = ? AND revoked_at IS NULL
453
+ `, [hash]);
454
+
455
+ if (!row) {
456
+ return { valid: false, reason: 'Invalid API key' };
457
+ }
458
+
459
+ if (row.expires_at && row.expires_at < Date.now()) {
460
+ return { valid: false, reason: 'API key expired' };
461
+ }
462
+
463
+ // Update last used
464
+ await storage.exec(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`, [Date.now(), row.id]);
465
+
466
+ return {
467
+ valid: true,
468
+ keyInfo: {
469
+ id: row.id,
470
+ name: row.name,
471
+ scopes: JSON.parse(row.scopes),
472
+ allowedAgents: row.allowed_agents ? JSON.parse(row.allowed_agents) : null,
473
+ },
474
+ };
475
+ }
476
+ ```
477
+
478
+ #### Connection Authentication
479
+
480
+ **Changes to `daemon/connection.ts`**:
481
+
482
+ ```typescript
483
+ // connection.ts - handleHello method
484
+
485
+ private async handleHello(envelope: Envelope<HelloPayload>): Promise<void> {
486
+ if (this._state !== 'HANDSHAKING') {
487
+ this.sendError('BAD_REQUEST', 'Unexpected HELLO', false);
488
+ return;
489
+ }
490
+
491
+ // Check if authentication is required
492
+ if (this.config.requireAuth) {
493
+ const auth = envelope.payload.auth;
494
+
495
+ if (!auth) {
496
+ this.sendError('UNAUTHORIZED', 'Authentication required', true);
497
+ this.close();
498
+ return;
499
+ }
500
+
501
+ const validation = await this.authProvider.validate(auth);
502
+
503
+ if (!validation.valid) {
504
+ this.sendError('UNAUTHORIZED', validation.reason || 'Invalid credentials', true);
505
+ this.close();
506
+ return;
507
+ }
508
+
509
+ // Store auth context for permission checks
510
+ this._authContext = validation.context;
511
+
512
+ // Check if this agent name is allowed
513
+ if (validation.context.allowedAgents) {
514
+ if (!validation.context.allowedAgents.includes(envelope.payload.agent)) {
515
+ this.sendError('FORBIDDEN', `Agent name "${envelope.payload.agent}" not allowed for this key`, true);
516
+ this.close();
517
+ return;
518
+ }
519
+ }
520
+ }
521
+
522
+ // Check agent limit
523
+ const currentAgents = this.getAgentCount();
524
+ const maxAgents = this._license?.limits.maxAgents ?? 10;
525
+
526
+ if (currentAgents >= maxAgents) {
527
+ this.sendError('QUOTA_EXCEEDED', `Agent limit reached (${maxAgents}). Upgrade to increase limit.`, true);
528
+ this.close();
529
+ return;
530
+ }
531
+
532
+ // Continue with existing logic...
533
+ this._agentName = envelope.payload.agent;
534
+ // ...
535
+ }
536
+ ```
537
+
538
+ #### Dashboard API
539
+
540
+ **New endpoints in `dashboard/server.ts`**:
541
+
542
+ ```typescript
543
+ // Require Pro tier for API key management
544
+ const requirePro = (req, res, next) => {
545
+ if (!license || !hasFeature(license.tier, 'api_keys')) {
546
+ return res.status(403).json({ error: 'Pro tier required for API key management' });
547
+ }
548
+ next();
549
+ };
550
+
551
+ // List API keys (without full key values)
552
+ app.get('/api/keys', requirePro, async (req, res) => {
553
+ const keys = await storage.all(`
554
+ SELECT id, key_prefix, name, description, scopes, created_at, last_used_at, expires_at
555
+ FROM api_keys
556
+ WHERE revoked_at IS NULL
557
+ ORDER BY created_at DESC
558
+ `);
559
+
560
+ res.json({
561
+ keys: keys.map(k => ({
562
+ id: k.id,
563
+ keyPrefix: k.key_prefix,
564
+ name: k.name,
565
+ description: k.description,
566
+ scopes: JSON.parse(k.scopes),
567
+ createdAt: new Date(k.created_at).toISOString(),
568
+ lastUsedAt: k.last_used_at ? new Date(k.last_used_at).toISOString() : null,
569
+ expiresAt: k.expires_at ? new Date(k.expires_at).toISOString() : null,
570
+ })),
571
+ });
572
+ });
573
+
574
+ // Create new API key
575
+ app.post('/api/keys', requirePro, async (req, res) => {
576
+ const { name, description, scopes, allowedAgents, expiresInDays } = req.body;
577
+
578
+ if (!name) {
579
+ return res.status(400).json({ error: 'Name is required' });
580
+ }
581
+
582
+ const apiKey = await createApiKey(storage, {
583
+ name,
584
+ description,
585
+ scopes,
586
+ allowedAgents,
587
+ expiresIn: expiresInDays ? expiresInDays * 24 * 60 * 60 * 1000 : undefined,
588
+ });
589
+
590
+ // Return full key only on creation
591
+ res.json({
592
+ id: apiKey.id,
593
+ key: apiKey.key, // IMPORTANT: Only shown once
594
+ keyPrefix: apiKey.keyPrefix,
595
+ name: apiKey.name,
596
+ message: 'Save this key now. It will not be shown again.',
597
+ });
598
+ });
599
+
600
+ // Revoke API key
601
+ app.delete('/api/keys/:id', requirePro, async (req, res) => {
602
+ const { id } = req.params;
603
+
604
+ await storage.exec(`UPDATE api_keys SET revoked_at = ? WHERE id = ?`, [Date.now(), id]);
605
+
606
+ res.json({ success: true });
607
+ });
608
+ ```
609
+
610
+ ---
611
+
612
+ ### 3.3 TLS Encryption
613
+
614
+ #### Overview
615
+
616
+ Pro tier adds optional TLS encryption for:
617
+ - Socket transport (daemon ↔ client)
618
+ - SQLite storage (at-rest encryption)
619
+
620
+ #### Socket TLS
621
+
622
+ **Configuration** (`daemon/server.ts`):
623
+
624
+ ```typescript
625
+ export interface TlsConfig {
626
+ enabled: boolean;
627
+ certPath: string; // PEM certificate
628
+ keyPath: string; // PEM private key
629
+ caPath?: string; // CA for client verification (mTLS)
630
+ mutualTls?: boolean; // Require client certificates
631
+ }
632
+
633
+ export interface DaemonConfig extends ConnectionConfig {
634
+ socketPath: string;
635
+ pidFilePath: string;
636
+ storagePath?: string;
637
+
638
+ // NEW
639
+ tls?: TlsConfig;
640
+ }
641
+ ```
642
+
643
+ **Server implementation**:
644
+
645
+ ```typescript
646
+ // daemon/server.ts
647
+
648
+ import tls from 'node:tls';
649
+
650
+ constructor(config: Partial<DaemonConfig> = {}) {
651
+ this.config = { ...DEFAULT_DAEMON_CONFIG, ...config };
652
+
653
+ // Validate TLS config requires Pro tier
654
+ if (this.config.tls?.enabled && !hasFeature(this.license?.tier, 'tls_encryption')) {
655
+ throw new Error('TLS encryption requires Pro tier');
656
+ }
657
+
658
+ if (this.config.tls?.enabled) {
659
+ const tlsOptions: tls.TlsOptions = {
660
+ key: fs.readFileSync(this.config.tls.keyPath),
661
+ cert: fs.readFileSync(this.config.tls.certPath),
662
+ };
663
+
664
+ if (this.config.tls.mutualTls) {
665
+ tlsOptions.requestCert = true;
666
+ tlsOptions.rejectUnauthorized = true;
667
+ if (this.config.tls.caPath) {
668
+ tlsOptions.ca = [fs.readFileSync(this.config.tls.caPath)];
669
+ }
670
+ }
671
+
672
+ this.server = tls.createServer(tlsOptions, this.handleConnection.bind(this));
673
+ console.log('[daemon] TLS enabled');
674
+ } else {
675
+ this.server = net.createServer(this.handleConnection.bind(this));
676
+ }
677
+ }
678
+ ```
679
+
680
+ **Client implementation** (`wrapper/client.ts`):
681
+
682
+ ```typescript
683
+ export interface ClientTlsConfig {
684
+ enabled: boolean;
685
+ caPath?: string; // CA to verify server
686
+ clientCertPath?: string; // For mTLS
687
+ clientKeyPath?: string; // For mTLS
688
+ rejectUnauthorized?: boolean;
689
+ }
690
+
691
+ export interface ClientConfig {
692
+ // ... existing
693
+ tls?: ClientTlsConfig;
694
+ }
695
+
696
+ connect(): Promise<void> {
697
+ // ...
698
+
699
+ const connectCallback = () => {
700
+ this.setState('HANDSHAKING');
701
+ this.sendHello();
702
+ };
703
+
704
+ if (this.config.tls?.enabled) {
705
+ const tlsOptions: tls.ConnectionOptions = {
706
+ rejectUnauthorized: this.config.tls.rejectUnauthorized ?? true,
707
+ };
708
+
709
+ if (this.config.tls.caPath) {
710
+ tlsOptions.ca = [fs.readFileSync(this.config.tls.caPath)];
711
+ }
712
+
713
+ if (this.config.tls.clientCertPath && this.config.tls.clientKeyPath) {
714
+ tlsOptions.cert = fs.readFileSync(this.config.tls.clientCertPath);
715
+ tlsOptions.key = fs.readFileSync(this.config.tls.clientKeyPath);
716
+ }
717
+
718
+ this.socket = tls.connect(this.config.socketPath, tlsOptions, connectCallback);
719
+ } else {
720
+ this.socket = net.createConnection(this.config.socketPath, connectCallback);
721
+ }
722
+
723
+ // ...
724
+ }
725
+ ```
726
+
727
+ #### Storage Encryption
728
+
729
+ **Using SQLCipher or better-sqlite3 encryption**:
730
+
731
+ ```typescript
732
+ // storage/sqlite-adapter.ts
733
+
734
+ export interface SqliteAdapterOptions {
735
+ dbPath: string;
736
+ messageRetentionMs?: number;
737
+ cleanupIntervalMs?: number;
738
+
739
+ // NEW
740
+ encryptionKey?: string; // Pro tier
741
+ }
742
+
743
+ private async openDatabase(driver: SqliteDriverName): Promise<SqliteDatabase> {
744
+ if (driver === 'better-sqlite3') {
745
+ const mod = await import('better-sqlite3');
746
+ const DatabaseCtor: any = (mod as any).default ?? mod;
747
+ const db: any = new DatabaseCtor(this.dbPath);
748
+
749
+ // Enable encryption if key provided
750
+ if (this.encryptionKey) {
751
+ if (!hasFeature(this.licenseTier, 'storage_encryption')) {
752
+ throw new Error('Storage encryption requires Pro tier');
753
+ }
754
+
755
+ // SQLCipher compatible
756
+ db.pragma(`key = '${this.encryptionKey}'`);
757
+
758
+ // Verify encryption is working
759
+ try {
760
+ db.pragma('cipher_version');
761
+ } catch (e) {
762
+ throw new Error('SQLCipher not available. Install better-sqlite3 with SQLCipher support.');
763
+ }
764
+ }
765
+
766
+ db.pragma('journal_mode = WAL');
767
+ return db;
768
+ }
769
+
770
+ // ... node:sqlite fallback
771
+ }
772
+ ```
773
+
774
+ **CLI integration**:
775
+
776
+ ```bash
777
+ # Generate encryption key
778
+ agent-relay keygen --output ~/.agent-relay/db.key
779
+
780
+ # Start with encryption
781
+ agent-relay up --db-key ~/.agent-relay/db.key
782
+
783
+ # Or via environment
784
+ export AGENT_RELAY_DB_KEY=$(cat ~/.agent-relay/db.key)
785
+ agent-relay up
786
+ ```
787
+
788
+ ---
789
+
790
+ ### 3.4 Scale Optimizations
791
+
792
+ #### Overview
793
+
794
+ Pro tier enables optimizations for high-throughput scenarios:
795
+ - Batched message persistence
796
+ - Connection pooling
797
+ - Worker thread offloading
798
+
799
+ #### Batched Persistence
800
+
801
+ **Current problem**: Each message is persisted synchronously, blocking the router.
802
+
803
+ **Solution**: Batch writes with configurable flush interval.
804
+
805
+ ```typescript
806
+ // router.ts
807
+
808
+ export interface RouterOptions {
809
+ storage?: StorageAdapter;
810
+ registry?: AgentRegistry;
811
+ delivery?: Partial<DeliveryReliabilityOptions>;
812
+
813
+ // NEW: Pro tier optimizations
814
+ batchPersistence?: {
815
+ enabled: boolean;
816
+ flushIntervalMs: number; // Default: 50ms
817
+ maxBatchSize: number; // Default: 100
818
+ };
819
+ }
820
+
821
+ export class Router {
822
+ private pendingPersist: DeliverEnvelope[] = [];
823
+ private persistTimer?: NodeJS.Timeout;
824
+ private batchConfig: Required<RouterOptions['batchPersistence']>;
825
+
826
+ constructor(options: RouterOptions = {}) {
827
+ // ...
828
+
829
+ this.batchConfig = {
830
+ enabled: options.batchPersistence?.enabled ?? false,
831
+ flushIntervalMs: options.batchPersistence?.flushIntervalMs ?? 50,
832
+ maxBatchSize: options.batchPersistence?.maxBatchSize ?? 100,
833
+ };
834
+ }
835
+
836
+ private persistDeliverEnvelope(envelope: DeliverEnvelope): void {
837
+ if (!this.storage) return;
838
+
839
+ if (this.batchConfig.enabled) {
840
+ this.queuePersist(envelope);
841
+ } else {
842
+ // Original sync behavior
843
+ this.storage.saveMessage({...}).catch(console.error);
844
+ }
845
+ }
846
+
847
+ private queuePersist(envelope: DeliverEnvelope): void {
848
+ this.pendingPersist.push(envelope);
849
+
850
+ // Flush immediately if batch is full
851
+ if (this.pendingPersist.length >= this.batchConfig.maxBatchSize) {
852
+ this.flushPersist();
853
+ return;
854
+ }
855
+
856
+ // Schedule flush
857
+ if (!this.persistTimer) {
858
+ this.persistTimer = setTimeout(() => {
859
+ this.flushPersist();
860
+ }, this.batchConfig.flushIntervalMs);
861
+ }
862
+ }
863
+
864
+ private async flushPersist(): Promise<void> {
865
+ if (this.persistTimer) {
866
+ clearTimeout(this.persistTimer);
867
+ this.persistTimer = undefined;
868
+ }
869
+
870
+ const batch = this.pendingPersist;
871
+ this.pendingPersist = [];
872
+
873
+ if (batch.length === 0) return;
874
+
875
+ const messages = batch.map(e => ({
876
+ id: e.id,
877
+ ts: e.ts,
878
+ from: e.from ?? 'unknown',
879
+ to: e.to ?? 'unknown',
880
+ topic: e.topic,
881
+ kind: e.payload.kind,
882
+ body: e.payload.body,
883
+ data: e.payload.data,
884
+ thread: e.payload.thread,
885
+ deliverySeq: e.delivery.seq,
886
+ deliverySessionId: e.delivery.session_id,
887
+ sessionId: e.delivery.session_id,
888
+ status: 'unread' as const,
889
+ is_urgent: false,
890
+ }));
891
+
892
+ try {
893
+ await this.storage!.saveMessageBatch(messages);
894
+ } catch (err) {
895
+ console.error('[router] Batch persist failed:', err);
896
+ // Re-queue failed batch (with limit to prevent infinite growth)
897
+ if (this.pendingPersist.length < 1000) {
898
+ this.pendingPersist.unshift(...batch);
899
+ }
900
+ }
901
+ }
902
+ }
903
+ ```
904
+
905
+ **SQLite batch insert**:
906
+
907
+ ```typescript
908
+ // storage/sqlite-adapter.ts
909
+
910
+ async saveMessageBatch(messages: StoredMessage[]): Promise<void> {
911
+ if (!this.db) throw new Error('Not initialized');
912
+
913
+ const stmt = this.db.prepare(`
914
+ INSERT OR REPLACE INTO messages
915
+ (id, ts, sender, recipient, topic, kind, body, data, thread, delivery_seq, delivery_session_id, session_id, status, is_urgent)
916
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
917
+ `);
918
+
919
+ // Use transaction for atomicity and performance
920
+ this.db.exec('BEGIN TRANSACTION');
921
+ try {
922
+ for (const msg of messages) {
923
+ stmt.run(
924
+ msg.id, msg.ts, msg.from, msg.to, msg.topic ?? null,
925
+ msg.kind, msg.body, msg.data ? JSON.stringify(msg.data) : null,
926
+ msg.thread ?? null, msg.deliverySeq ?? null, msg.deliverySessionId ?? null,
927
+ msg.sessionId ?? null, msg.status, msg.is_urgent ? 1 : 0
928
+ );
929
+ }
930
+ this.db.exec('COMMIT');
931
+ } catch (err) {
932
+ this.db.exec('ROLLBACK');
933
+ throw err;
934
+ }
935
+ }
936
+ ```
937
+
938
+ #### Rate Limiting
939
+
940
+ **Per-connection rate limiting**:
941
+
942
+ ```typescript
943
+ // daemon/connection.ts
944
+
945
+ interface RateLimitState {
946
+ tokens: number;
947
+ lastRefill: number;
948
+ }
949
+
950
+ private rateLimit: RateLimitState = {
951
+ tokens: 100, // Messages per second
952
+ lastRefill: Date.now(),
953
+ };
954
+
955
+ private checkRateLimit(): boolean {
956
+ const now = Date.now();
957
+ const elapsed = now - this.rateLimit.lastRefill;
958
+
959
+ // Refill tokens (1 token per 10ms = 100/sec)
960
+ const refill = Math.floor(elapsed / 10);
961
+ if (refill > 0) {
962
+ this.rateLimit.tokens = Math.min(100, this.rateLimit.tokens + refill);
963
+ this.rateLimit.lastRefill = now;
964
+ }
965
+
966
+ if (this.rateLimit.tokens <= 0) {
967
+ return false;
968
+ }
969
+
970
+ this.rateLimit.tokens--;
971
+ return true;
972
+ }
973
+
974
+ private handleSend(envelope: Envelope<SendPayload>): void {
975
+ if (this._state !== 'ACTIVE') {
976
+ this.sendError('BAD_REQUEST', 'Not in ACTIVE state', false);
977
+ return;
978
+ }
979
+
980
+ // Check rate limit
981
+ if (!this.checkRateLimit()) {
982
+ this.send({
983
+ v: PROTOCOL_VERSION,
984
+ type: 'BUSY',
985
+ id: uuid(),
986
+ ts: Date.now(),
987
+ payload: {
988
+ retry_after_ms: 100,
989
+ queue_depth: 0,
990
+ },
991
+ });
992
+ return;
993
+ }
994
+
995
+ // Forward to router
996
+ if (this.onMessage) {
997
+ this.onMessage(envelope);
998
+ }
999
+ }
1000
+ ```
1001
+
1002
+ ---
1003
+
1004
+ ### 3.5 Message Retention
1005
+
1006
+ #### Overview
1007
+
1008
+ Configurable retention periods by tier:
1009
+ - Community: 7 days (fixed)
1010
+ - Pro: Up to 90 days
1011
+ - Team: Up to 365 days
1012
+ - Enterprise: Unlimited
1013
+
1014
+ #### Implementation
1015
+
1016
+ **Configuration**:
1017
+
1018
+ ```typescript
1019
+ // storage/sqlite-adapter.ts
1020
+
1021
+ const RETENTION_LIMITS = {
1022
+ community: 7 * 24 * 60 * 60 * 1000, // 7 days
1023
+ pro: 90 * 24 * 60 * 60 * 1000, // 90 days
1024
+ team: 365 * 24 * 60 * 60 * 1000, // 365 days
1025
+ enterprise: Infinity, // Unlimited
1026
+ };
1027
+
1028
+ export interface SqliteAdapterOptions {
1029
+ dbPath: string;
1030
+ messageRetentionMs?: number;
1031
+ cleanupIntervalMs?: number;
1032
+ licenseTier?: string;
1033
+ }
1034
+
1035
+ constructor(options: SqliteAdapterOptions) {
1036
+ this.dbPath = options.dbPath;
1037
+
1038
+ const tier = options.licenseTier || 'community';
1039
+ const maxRetention = RETENTION_LIMITS[tier] || RETENTION_LIMITS.community;
1040
+
1041
+ // User can set lower retention, but not higher than tier allows
1042
+ this.retentionMs = Math.min(
1043
+ options.messageRetentionMs ?? RETENTION_LIMITS.community,
1044
+ maxRetention
1045
+ );
1046
+
1047
+ this.cleanupIntervalMs = options.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
1048
+ }
1049
+ ```
1050
+
1051
+ **CLI options**:
1052
+
1053
+ ```bash
1054
+ # Set retention (Pro tier example)
1055
+ agent-relay up --retention 30d
1056
+
1057
+ # Query current retention
1058
+ agent-relay status --verbose
1059
+ # Output: Retention: 30 days (Pro tier max: 90 days)
1060
+ ```
1061
+
1062
+ ---
1063
+
1064
+ ### 3.6 Webhooks
1065
+
1066
+ #### Overview
1067
+
1068
+ Pro tier enables HTTP webhooks for external integrations:
1069
+ - Message events (sent, delivered, failed)
1070
+ - Agent events (connected, disconnected)
1071
+ - System events (daemon started, stopped)
1072
+
1073
+ #### Architecture
1074
+
1075
+ ```
1076
+ src/
1077
+ webhooks/
1078
+ index.ts # Public API
1079
+ manager.ts # Registration and dispatch
1080
+ types.ts # Event definitions
1081
+ queue.ts # Async dispatch with retry
1082
+ ```
1083
+
1084
+ #### Event Types
1085
+
1086
+ ```typescript
1087
+ // webhooks/types.ts
1088
+
1089
+ export type WebhookEvent =
1090
+ // Message events
1091
+ | 'message.sent'
1092
+ | 'message.delivered'
1093
+ | 'message.failed'
1094
+
1095
+ // Agent events
1096
+ | 'agent.connected'
1097
+ | 'agent.disconnected'
1098
+
1099
+ // System events
1100
+ | 'system.started'
1101
+ | 'system.stopped';
1102
+
1103
+ export interface WebhookPayload {
1104
+ event: WebhookEvent;
1105
+ timestamp: string;
1106
+ data: unknown;
1107
+ }
1108
+
1109
+ export interface MessageSentPayload {
1110
+ id: string;
1111
+ from: string;
1112
+ to: string;
1113
+ preview: string; // First 100 chars
1114
+ timestamp: string;
1115
+ }
1116
+
1117
+ export interface AgentConnectedPayload {
1118
+ name: string;
1119
+ cli?: string;
1120
+ sessionId: string;
1121
+ timestamp: string;
1122
+ }
1123
+ ```
1124
+
1125
+ #### Database Schema
1126
+
1127
+ ```sql
1128
+ CREATE TABLE webhooks (
1129
+ id TEXT PRIMARY KEY,
1130
+ url TEXT NOT NULL,
1131
+ events TEXT NOT NULL, -- JSON array of event types
1132
+ secret TEXT, -- For HMAC signature
1133
+ description TEXT,
1134
+
1135
+ -- Status
1136
+ enabled INTEGER DEFAULT 1,
1137
+ failure_count INTEGER DEFAULT 0,
1138
+ last_failure TEXT, -- Error message
1139
+ last_success_at INTEGER,
1140
+
1141
+ -- Metadata
1142
+ created_at INTEGER NOT NULL,
1143
+ updated_at INTEGER NOT NULL
1144
+ );
1145
+
1146
+ CREATE TABLE webhook_deliveries (
1147
+ id TEXT PRIMARY KEY,
1148
+ webhook_id TEXT NOT NULL,
1149
+ event TEXT NOT NULL,
1150
+ payload TEXT NOT NULL,
1151
+
1152
+ -- Delivery status
1153
+ status TEXT NOT NULL, -- 'pending', 'success', 'failed'
1154
+ attempts INTEGER DEFAULT 0,
1155
+ last_attempt_at INTEGER,
1156
+ response_status INTEGER,
1157
+ response_body TEXT,
1158
+ error TEXT,
1159
+
1160
+ created_at INTEGER NOT NULL,
1161
+
1162
+ FOREIGN KEY (webhook_id) REFERENCES webhooks (id)
1163
+ );
1164
+
1165
+ CREATE INDEX idx_webhook_deliveries_status ON webhook_deliveries (status);
1166
+ CREATE INDEX idx_webhook_deliveries_webhook ON webhook_deliveries (webhook_id);
1167
+ ```
1168
+
1169
+ #### Webhook Manager
1170
+
1171
+ ```typescript
1172
+ // webhooks/manager.ts
1173
+
1174
+ import crypto from 'crypto';
1175
+
1176
+ export interface WebhookConfig {
1177
+ url: string;
1178
+ events: WebhookEvent[];
1179
+ secret?: string;
1180
+ description?: string;
1181
+ }
1182
+
1183
+ export interface WebhookDelivery {
1184
+ id: string;
1185
+ webhookId: string;
1186
+ event: WebhookEvent;
1187
+ payload: unknown;
1188
+ status: 'pending' | 'success' | 'failed';
1189
+ attempts: number;
1190
+ }
1191
+
1192
+ export class WebhookManager {
1193
+ private storage: StorageAdapter;
1194
+ private queue: AsyncQueue<WebhookDelivery>;
1195
+
1196
+ constructor(storage: StorageAdapter) {
1197
+ this.storage = storage;
1198
+ this.queue = new AsyncQueue(this.deliverWebhook.bind(this), {
1199
+ concurrency: 5,
1200
+ retryAttempts: 3,
1201
+ retryDelayMs: 1000,
1202
+ });
1203
+ }
1204
+
1205
+ async register(config: WebhookConfig): Promise<string> {
1206
+ const id = crypto.randomUUID();
1207
+ const now = Date.now();
1208
+
1209
+ await this.storage.exec(`
1210
+ INSERT INTO webhooks (id, url, events, secret, description, created_at, updated_at)
1211
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1212
+ `, [
1213
+ id,
1214
+ config.url,
1215
+ JSON.stringify(config.events),
1216
+ config.secret || null,
1217
+ config.description || null,
1218
+ now,
1219
+ now,
1220
+ ]);
1221
+
1222
+ return id;
1223
+ }
1224
+
1225
+ async dispatch(event: WebhookEvent, data: unknown): Promise<void> {
1226
+ // Find webhooks subscribed to this event
1227
+ const webhooks = await this.storage.all(`
1228
+ SELECT * FROM webhooks
1229
+ WHERE enabled = 1 AND events LIKE ?
1230
+ `, [`%"${event}"%`]);
1231
+
1232
+ for (const webhook of webhooks) {
1233
+ const deliveryId = crypto.randomUUID();
1234
+ const payload = {
1235
+ event,
1236
+ timestamp: new Date().toISOString(),
1237
+ data,
1238
+ };
1239
+
1240
+ // Record delivery attempt
1241
+ await this.storage.exec(`
1242
+ INSERT INTO webhook_deliveries (id, webhook_id, event, payload, status, created_at)
1243
+ VALUES (?, ?, ?, ?, 'pending', ?)
1244
+ `, [deliveryId, webhook.id, event, JSON.stringify(payload), Date.now()]);
1245
+
1246
+ // Queue for async delivery
1247
+ this.queue.push({
1248
+ id: deliveryId,
1249
+ webhookId: webhook.id,
1250
+ event,
1251
+ payload,
1252
+ status: 'pending',
1253
+ attempts: 0,
1254
+ });
1255
+ }
1256
+ }
1257
+
1258
+ private async deliverWebhook(delivery: WebhookDelivery): Promise<void> {
1259
+ const webhook = await this.storage.get(`SELECT * FROM webhooks WHERE id = ?`, [delivery.webhookId]);
1260
+ if (!webhook) return;
1261
+
1262
+ const body = JSON.stringify(delivery.payload);
1263
+ const headers: Record<string, string> = {
1264
+ 'Content-Type': 'application/json',
1265
+ 'X-Webhook-Event': delivery.event,
1266
+ 'X-Webhook-Delivery': delivery.id,
1267
+ };
1268
+
1269
+ // Add HMAC signature if secret is configured
1270
+ if (webhook.secret) {
1271
+ const signature = crypto
1272
+ .createHmac('sha256', webhook.secret)
1273
+ .update(body)
1274
+ .digest('hex');
1275
+ headers['X-Webhook-Signature'] = `sha256=${signature}`;
1276
+ }
1277
+
1278
+ try {
1279
+ const response = await fetch(webhook.url, {
1280
+ method: 'POST',
1281
+ headers,
1282
+ body,
1283
+ signal: AbortSignal.timeout(10000), // 10s timeout
1284
+ });
1285
+
1286
+ await this.storage.exec(`
1287
+ UPDATE webhook_deliveries
1288
+ SET status = ?, attempts = attempts + 1, last_attempt_at = ?, response_status = ?
1289
+ WHERE id = ?
1290
+ `, [
1291
+ response.ok ? 'success' : 'failed',
1292
+ Date.now(),
1293
+ response.status,
1294
+ delivery.id,
1295
+ ]);
1296
+
1297
+ if (response.ok) {
1298
+ await this.storage.exec(`
1299
+ UPDATE webhooks SET failure_count = 0, last_success_at = ? WHERE id = ?
1300
+ `, [Date.now(), webhook.id]);
1301
+ } else {
1302
+ throw new Error(`HTTP ${response.status}`);
1303
+ }
1304
+ } catch (err) {
1305
+ await this.storage.exec(`
1306
+ UPDATE webhooks SET failure_count = failure_count + 1, last_failure = ? WHERE id = ?
1307
+ `, [err.message, webhook.id]);
1308
+
1309
+ await this.storage.exec(`
1310
+ UPDATE webhook_deliveries SET status = 'failed', error = ?, attempts = attempts + 1 WHERE id = ?
1311
+ `, [err.message, delivery.id]);
1312
+
1313
+ throw err; // Let queue handle retry
1314
+ }
1315
+ }
1316
+ }
1317
+ ```
1318
+
1319
+ #### Integration Points
1320
+
1321
+ **Router integration** (`router.ts`):
1322
+
1323
+ ```typescript
1324
+ export class Router {
1325
+ private webhookManager?: WebhookManager;
1326
+
1327
+ constructor(options: RouterOptions = {}) {
1328
+ // ...
1329
+ if (options.webhookManager) {
1330
+ this.webhookManager = options.webhookManager;
1331
+ }
1332
+ }
1333
+
1334
+ private sendDirect(from: string, to: string, envelope: SendEnvelope): boolean {
1335
+ // ... existing delivery logic
1336
+
1337
+ // Dispatch webhook
1338
+ if (this.webhookManager && sent) {
1339
+ this.webhookManager.dispatch('message.delivered', {
1340
+ id: deliver.id,
1341
+ from,
1342
+ to,
1343
+ preview: envelope.payload.body.substring(0, 100),
1344
+ timestamp: new Date(deliver.ts).toISOString(),
1345
+ });
1346
+ }
1347
+
1348
+ return sent;
1349
+ }
1350
+ }
1351
+ ```
1352
+
1353
+ **Server integration** (`daemon/server.ts`):
1354
+
1355
+ ```typescript
1356
+ connection.onActive = () => {
1357
+ // ... existing registration logic
1358
+
1359
+ // Dispatch webhook
1360
+ if (this.webhookManager && connection.agentName) {
1361
+ this.webhookManager.dispatch('agent.connected', {
1362
+ name: connection.agentName,
1363
+ cli: connection.cli,
1364
+ sessionId: connection.sessionId,
1365
+ timestamp: new Date().toISOString(),
1366
+ });
1367
+ }
1368
+ };
1369
+
1370
+ connection.onClose = () => {
1371
+ // ... existing cleanup logic
1372
+
1373
+ // Dispatch webhook
1374
+ if (this.webhookManager && connection.agentName) {
1375
+ this.webhookManager.dispatch('agent.disconnected', {
1376
+ name: connection.agentName,
1377
+ sessionId: connection.sessionId,
1378
+ timestamp: new Date().toISOString(),
1379
+ });
1380
+ }
1381
+ };
1382
+ ```
1383
+
1384
+ #### Dashboard API
1385
+
1386
+ ```typescript
1387
+ // dashboard/server.ts
1388
+
1389
+ // List webhooks
1390
+ app.get('/api/webhooks', requirePro, async (req, res) => {
1391
+ const webhooks = await storage.all(`
1392
+ SELECT id, url, events, description, enabled, failure_count, last_success_at, created_at
1393
+ FROM webhooks
1394
+ ORDER BY created_at DESC
1395
+ `);
1396
+
1397
+ res.json({
1398
+ webhooks: webhooks.map(w => ({
1399
+ id: w.id,
1400
+ url: w.url,
1401
+ events: JSON.parse(w.events),
1402
+ description: w.description,
1403
+ enabled: !!w.enabled,
1404
+ failureCount: w.failure_count,
1405
+ lastSuccessAt: w.last_success_at ? new Date(w.last_success_at).toISOString() : null,
1406
+ createdAt: new Date(w.created_at).toISOString(),
1407
+ })),
1408
+ });
1409
+ });
1410
+
1411
+ // Create webhook
1412
+ app.post('/api/webhooks', requirePro, async (req, res) => {
1413
+ const { url, events, secret, description } = req.body;
1414
+
1415
+ if (!url || !events?.length) {
1416
+ return res.status(400).json({ error: 'URL and events are required' });
1417
+ }
1418
+
1419
+ // Validate URL
1420
+ try {
1421
+ new URL(url);
1422
+ } catch {
1423
+ return res.status(400).json({ error: 'Invalid URL' });
1424
+ }
1425
+
1426
+ // Validate events
1427
+ const validEvents = ['message.sent', 'message.delivered', 'agent.connected', 'agent.disconnected'];
1428
+ if (!events.every(e => validEvents.includes(e))) {
1429
+ return res.status(400).json({ error: 'Invalid event type' });
1430
+ }
1431
+
1432
+ const id = await webhookManager.register({ url, events, secret, description });
1433
+
1434
+ res.json({ id });
1435
+ });
1436
+
1437
+ // Test webhook
1438
+ app.post('/api/webhooks/:id/test', requirePro, async (req, res) => {
1439
+ const { id } = req.params;
1440
+
1441
+ await webhookManager.dispatch('test', {
1442
+ message: 'This is a test webhook from Agent Relay',
1443
+ webhookId: id,
1444
+ });
1445
+
1446
+ res.json({ success: true, message: 'Test webhook dispatched' });
1447
+ });
1448
+
1449
+ // Delete webhook
1450
+ app.delete('/api/webhooks/:id', requirePro, async (req, res) => {
1451
+ const { id } = req.params;
1452
+
1453
+ await storage.exec(`DELETE FROM webhooks WHERE id = ?`, [id]);
1454
+
1455
+ res.json({ success: true });
1456
+ });
1457
+ ```
1458
+
1459
+ ---
1460
+
1461
+ ## 4. Implementation Roadmap
1462
+
1463
+ ### Phase 1: Foundation (Week 1-2)
1464
+
1465
+ | Task | Priority | Effort | Dependencies |
1466
+ |------|----------|--------|--------------|
1467
+ | Licensing system | P0 | 2 days | None |
1468
+ | Agent limits enforcement | P0 | 1 day | Licensing |
1469
+ | Feature flag system | P0 | 1 day | Licensing |
1470
+ | Dashboard license display | P1 | 1 day | Licensing |
1471
+
1472
+ **Milestone**: Free tier has enforced limits, upgrade prompts appear.
1473
+
1474
+ ### Phase 2: Pro Features (Week 3-4)
1475
+
1476
+ | Task | Priority | Effort | Dependencies |
1477
+ |------|----------|--------|--------------|
1478
+ | API key management | P0 | 3 days | Licensing |
1479
+ | Connection authentication | P0 | 2 days | API keys |
1480
+ | Message retention config | P1 | 1 day | Licensing |
1481
+ | Webhooks | P1 | 3 days | API keys |
1482
+ | TLS encryption | P2 | 3 days | None |
1483
+ | Storage encryption | P2 | 2 days | TLS |
1484
+
1485
+ **Milestone**: Pro tier fully functional, customers can upgrade.
1486
+
1487
+ ### Phase 3: Scale & Polish (Week 5-6)
1488
+
1489
+ | Task | Priority | Effort | Dependencies |
1490
+ |------|----------|--------|--------------|
1491
+ | Batched persistence | P1 | 2 days | None |
1492
+ | Rate limiting | P1 | 1 day | None |
1493
+ | Dashboard API key UI | P1 | 2 days | API keys |
1494
+ | Dashboard webhook UI | P1 | 2 days | Webhooks |
1495
+ | Documentation | P0 | 3 days | All |
1496
+
1497
+ **Milestone**: Production-ready Pro tier with full documentation.
1498
+
1499
+ ### Phase 4: Team Tier (Week 7-10)
1500
+
1501
+ | Task | Priority | Effort | Dependencies |
1502
+ |------|----------|--------|--------------|
1503
+ | TCP transport | P0 | 5 days | TLS |
1504
+ | Multi-machine routing | P0 | 5 days | TCP |
1505
+ | SSO (SAML/OIDC) | P1 | 5 days | Auth |
1506
+ | Audit logs | P1 | 3 days | Storage |
1507
+ | Team dashboard | P1 | 5 days | Multi-machine |
1508
+
1509
+ **Milestone**: Team tier available for multi-machine deployments.
1510
+
1511
+ ---
1512
+
1513
+ ## 5. Pricing Justification
1514
+
1515
+ ### Cost Analysis
1516
+
1517
+ | Component | Monthly Cost | Notes |
1518
+ |-----------|-------------|-------|
1519
+ | License server | $20 | Fly.io, minimal traffic |
1520
+ | Support (Pro) | $100/customer | ~2 emails/month |
1521
+ | Support (Team) | $300/customer | ~5 emails/month |
1522
+ | Development | $10,000/month | 1 FTE amortized |
1523
+
1524
+ ### Break-even Analysis
1525
+
1526
+ | Tier | Price | Customers Needed | Notes |
1527
+ |------|-------|------------------|-------|
1528
+ | Pro @ $29/mo | $29 | 350 | Cover dev costs |
1529
+ | Pro @ $49/mo | $49 | 210 | Cover dev costs |
1530
+ | Team @ $149/mo | $149 | 70 | Cover dev costs |
1531
+
1532
+ ### Competitive Pricing
1533
+
1534
+ | Competitor | Comparable Tier | Our Price | Delta |
1535
+ |-----------|-----------------|-----------|-------|
1536
+ | GitLab Premium | $29/user/mo | $49/machine/mo | -40% |
1537
+ | Sidekiq Pro | $99/app/mo | $49/machine/mo | -50% |
1538
+ | Redis Enterprise | $100+/mo | $49/machine/mo | -50% |
1539
+
1540
+ **Recommendation**: Start at $49/mo for Pro, $149/mo for Team. Lower than competitors establishes value.
1541
+
1542
+ ---
1543
+
1544
+ ## 6. Risks & Mitigations
1545
+
1546
+ ### Technical Risks
1547
+
1548
+ | Risk | Probability | Impact | Mitigation |
1549
+ |------|-------------|--------|------------|
1550
+ | SQLCipher compatibility | Medium | High | Test on all supported platforms |
1551
+ | TLS performance overhead | Low | Medium | Benchmark before release |
1552
+ | Webhook delivery failures | Medium | Low | Retry queue with exponential backoff |
1553
+ | License server downtime | Low | High | Offline license for Enterprise |
1554
+
1555
+ ### Business Risks
1556
+
1557
+ | Risk | Probability | Impact | Mitigation |
1558
+ |------|-------------|--------|------------|
1559
+ | Open source fork with Pro features | Medium | High | Move fast, focus on support value |
1560
+ | Pricing too high | Medium | Medium | Start low, increase with value |
1561
+ | Pricing too low | Low | Low | Easy to increase, hard to decrease |
1562
+ | Support burden | High | Medium | Good docs, FAQ, community forums |
1563
+
1564
+ ### Legal Considerations
1565
+
1566
+ 1. **License clarity**: MIT for core, proprietary for Pro features
1567
+ 2. **Terms of service**: Required for commercial tiers
1568
+ 3. **Privacy policy**: Required if collecting telemetry
1569
+ 4. **GDPR compliance**: If serving EU customers
1570
+
1571
+ ---
1572
+
1573
+ ## Appendix A: CLI Commands
1574
+
1575
+ ```bash
1576
+ # License management
1577
+ agent-relay license show
1578
+ agent-relay license activate <key>
1579
+ agent-relay license deactivate
1580
+
1581
+ # API key management (Pro)
1582
+ agent-relay keys list
1583
+ agent-relay keys create --name "CI Pipeline" --scopes send,receive
1584
+ agent-relay keys revoke <key-id>
1585
+
1586
+ # Webhook management (Pro)
1587
+ agent-relay webhooks list
1588
+ agent-relay webhooks add --url https://example.com/hook --events message.delivered
1589
+ agent-relay webhooks test <webhook-id>
1590
+ agent-relay webhooks delete <webhook-id>
1591
+
1592
+ # TLS (Pro)
1593
+ agent-relay up --tls-cert ./cert.pem --tls-key ./key.pem
1594
+ agent-relay up --tls-mutual --tls-ca ./ca.pem
1595
+
1596
+ # Retention (Pro)
1597
+ agent-relay up --retention 30d
1598
+
1599
+ # Storage encryption (Pro)
1600
+ agent-relay keygen --output ./db.key
1601
+ agent-relay up --db-key ./db.key
1602
+ ```
1603
+
1604
+ ---
1605
+
1606
+ ## Appendix B: Environment Variables
1607
+
1608
+ ```bash
1609
+ # License
1610
+ AGENT_RELAY_LICENSE_KEY=ar_pro_xxxx
1611
+
1612
+ # Authentication
1613
+ AGENT_RELAY_REQUIRE_AUTH=true
1614
+ AGENT_RELAY_API_KEY=ar_key_xxxx # For clients
1615
+
1616
+ # TLS
1617
+ AGENT_RELAY_TLS_CERT=/path/to/cert.pem
1618
+ AGENT_RELAY_TLS_KEY=/path/to/key.pem
1619
+ AGENT_RELAY_TLS_CA=/path/to/ca.pem
1620
+ AGENT_RELAY_TLS_MUTUAL=true
1621
+
1622
+ # Storage
1623
+ AGENT_RELAY_DB_KEY=<encryption-key>
1624
+ AGENT_RELAY_RETENTION_DAYS=30
1625
+
1626
+ # Telemetry (opt-in)
1627
+ AGENT_RELAY_TELEMETRY=true
1628
+ ```
1629
+
1630
+ ---
1631
+
1632
+ ## Appendix C: Dashboard Mockups
1633
+
1634
+ ### Pro Features Panel
1635
+
1636
+ ```
1637
+ ┌─────────────────────────────────────────────────────────────┐
1638
+ │ License: Pro Expires: 2026-01-15 │
1639
+ ├─────────────────────────────────────────────────────────────┤
1640
+ │ Agents: 47/100 Messages today: 12,847 │
1641
+ │ Retention: 30 days Webhooks: 3 active │
1642
+ └─────────────────────────────────────────────────────────────┘
1643
+ ```
1644
+
1645
+ ### API Keys Management
1646
+
1647
+ ```
1648
+ ┌─────────────────────────────────────────────────────────────┐
1649
+ │ API Keys [+ Create] │
1650
+ ├─────────────────────────────────────────────────────────────┤
1651
+ │ Name Key Last Used Actions │
1652
+ │ ───────────── ───────────────── ───────────── ───────── │
1653
+ │ CI Pipeline ar_key_a1b2... 2 hours ago [Revoke] │
1654
+ │ Dev Machine ar_key_x9y8... 5 mins ago [Revoke] │
1655
+ │ Staging ar_key_m3n4... Never [Revoke] │
1656
+ └─────────────────────────────────────────────────────────────┘
1657
+ ```
1658
+
1659
+ ### Webhooks Management
1660
+
1661
+ ```
1662
+ ┌─────────────────────────────────────────────────────────────┐
1663
+ │ Webhooks [+ Create] │
1664
+ ├─────────────────────────────────────────────────────────────┤
1665
+ │ URL Events Status │
1666
+ │ ───────────────────────────── ───────────── ───────── │
1667
+ │ https://api.slack.com/hook agent.* ✓ Active │
1668
+ │ https://my-app.com/webhook message.* ✓ Active │
1669
+ │ https://old-service.com/hook all ✗ Failing │
1670
+ └─────────────────────────────────────────────────────────────┘
1671
+ ```
1672
+
1673
+ ---
1674
+
1675
+ ## Changelog
1676
+
1677
+ | Version | Date | Changes |
1678
+ |---------|------|---------|
1679
+ | 0.1 | 2025-12-26 | Initial draft |