agent-relay 1.0.21 → 1.1.0

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 (283) hide show
  1. package/dist/bridge/shadow-cli.d.ts +17 -0
  2. package/dist/bridge/shadow-cli.d.ts.map +1 -0
  3. package/dist/bridge/shadow-cli.js +75 -0
  4. package/dist/bridge/shadow-cli.js.map +1 -0
  5. package/dist/bridge/shadow-config.d.ts +87 -0
  6. package/dist/bridge/shadow-config.d.ts.map +1 -0
  7. package/dist/bridge/shadow-config.js +134 -0
  8. package/dist/bridge/shadow-config.js.map +1 -0
  9. package/dist/bridge/spawner.d.ts +15 -1
  10. package/dist/bridge/spawner.d.ts.map +1 -1
  11. package/dist/bridge/spawner.js +164 -4
  12. package/dist/bridge/spawner.js.map +1 -1
  13. package/dist/bridge/types.d.ts +55 -0
  14. package/dist/bridge/types.d.ts.map +1 -1
  15. package/dist/cli/index.js +796 -11
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cloud/api/auth.d.ts +19 -0
  18. package/dist/cloud/api/auth.d.ts.map +1 -0
  19. package/dist/cloud/api/auth.js +216 -0
  20. package/dist/cloud/api/auth.js.map +1 -0
  21. package/dist/cloud/api/billing.d.ts +17 -0
  22. package/dist/cloud/api/billing.d.ts.map +1 -0
  23. package/dist/cloud/api/billing.js +353 -0
  24. package/dist/cloud/api/billing.js.map +1 -0
  25. package/dist/cloud/api/coordinators.d.ts +8 -0
  26. package/dist/cloud/api/coordinators.d.ts.map +1 -0
  27. package/dist/cloud/api/coordinators.js +347 -0
  28. package/dist/cloud/api/coordinators.js.map +1 -0
  29. package/dist/cloud/api/daemons.d.ts +12 -0
  30. package/dist/cloud/api/daemons.d.ts.map +1 -0
  31. package/dist/cloud/api/daemons.js +320 -0
  32. package/dist/cloud/api/daemons.js.map +1 -0
  33. package/dist/cloud/api/middleware/planLimits.d.ts +36 -0
  34. package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
  35. package/dist/cloud/api/middleware/planLimits.js +164 -0
  36. package/dist/cloud/api/middleware/planLimits.js.map +1 -0
  37. package/dist/cloud/api/onboarding.d.ts +8 -0
  38. package/dist/cloud/api/onboarding.d.ts.map +1 -0
  39. package/dist/cloud/api/onboarding.js +407 -0
  40. package/dist/cloud/api/onboarding.js.map +1 -0
  41. package/dist/cloud/api/providers.d.ts +7 -0
  42. package/dist/cloud/api/providers.d.ts.map +1 -0
  43. package/dist/cloud/api/providers.js +435 -0
  44. package/dist/cloud/api/providers.js.map +1 -0
  45. package/dist/cloud/api/repos.d.ts +7 -0
  46. package/dist/cloud/api/repos.d.ts.map +1 -0
  47. package/dist/cloud/api/repos.js +314 -0
  48. package/dist/cloud/api/repos.js.map +1 -0
  49. package/dist/cloud/api/teams.d.ts +7 -0
  50. package/dist/cloud/api/teams.d.ts.map +1 -0
  51. package/dist/cloud/api/teams.js +279 -0
  52. package/dist/cloud/api/teams.js.map +1 -0
  53. package/dist/cloud/api/usage.d.ts +7 -0
  54. package/dist/cloud/api/usage.d.ts.map +1 -0
  55. package/dist/cloud/api/usage.js +98 -0
  56. package/dist/cloud/api/usage.js.map +1 -0
  57. package/dist/cloud/api/workspaces.d.ts +7 -0
  58. package/dist/cloud/api/workspaces.d.ts.map +1 -0
  59. package/dist/cloud/api/workspaces.js +510 -0
  60. package/dist/cloud/api/workspaces.js.map +1 -0
  61. package/dist/cloud/billing/index.d.ts +9 -0
  62. package/dist/cloud/billing/index.d.ts.map +1 -0
  63. package/dist/cloud/billing/index.js +9 -0
  64. package/dist/cloud/billing/index.js.map +1 -0
  65. package/dist/cloud/billing/plans.d.ts +39 -0
  66. package/dist/cloud/billing/plans.d.ts.map +1 -0
  67. package/dist/cloud/billing/plans.js +232 -0
  68. package/dist/cloud/billing/plans.js.map +1 -0
  69. package/dist/cloud/billing/service.d.ts +80 -0
  70. package/dist/cloud/billing/service.d.ts.map +1 -0
  71. package/dist/cloud/billing/service.js +388 -0
  72. package/dist/cloud/billing/service.js.map +1 -0
  73. package/dist/cloud/billing/types.d.ts +135 -0
  74. package/dist/cloud/billing/types.d.ts.map +1 -0
  75. package/dist/cloud/billing/types.js +7 -0
  76. package/dist/cloud/billing/types.js.map +1 -0
  77. package/dist/cloud/config.d.ts +59 -0
  78. package/dist/cloud/config.d.ts.map +1 -0
  79. package/dist/cloud/config.js +83 -0
  80. package/dist/cloud/config.js.map +1 -0
  81. package/dist/cloud/db/drizzle.d.ts +132 -0
  82. package/dist/cloud/db/drizzle.d.ts.map +1 -0
  83. package/dist/cloud/db/drizzle.js +613 -0
  84. package/dist/cloud/db/drizzle.js.map +1 -0
  85. package/dist/cloud/db/index.d.ts +30 -0
  86. package/dist/cloud/db/index.d.ts.map +1 -0
  87. package/dist/cloud/db/index.js +44 -0
  88. package/dist/cloud/db/index.js.map +1 -0
  89. package/dist/cloud/db/schema.d.ts +1792 -0
  90. package/dist/cloud/db/schema.d.ts.map +1 -0
  91. package/dist/cloud/db/schema.js +234 -0
  92. package/dist/cloud/db/schema.js.map +1 -0
  93. package/dist/cloud/index.d.ts +11 -0
  94. package/dist/cloud/index.d.ts.map +1 -0
  95. package/dist/cloud/index.js +37 -0
  96. package/dist/cloud/index.js.map +1 -0
  97. package/dist/cloud/provisioner/index.d.ts +51 -0
  98. package/dist/cloud/provisioner/index.d.ts.map +1 -0
  99. package/dist/cloud/provisioner/index.js +676 -0
  100. package/dist/cloud/provisioner/index.js.map +1 -0
  101. package/dist/cloud/server.d.ts +16 -0
  102. package/dist/cloud/server.d.ts.map +1 -0
  103. package/dist/cloud/server.js +190 -0
  104. package/dist/cloud/server.js.map +1 -0
  105. package/dist/cloud/services/coordinator.d.ts +62 -0
  106. package/dist/cloud/services/coordinator.d.ts.map +1 -0
  107. package/dist/cloud/services/coordinator.js +389 -0
  108. package/dist/cloud/services/coordinator.js.map +1 -0
  109. package/dist/cloud/services/planLimits.d.ts +110 -0
  110. package/dist/cloud/services/planLimits.d.ts.map +1 -0
  111. package/dist/cloud/services/planLimits.js +254 -0
  112. package/dist/cloud/services/planLimits.js.map +1 -0
  113. package/dist/cloud/vault/index.d.ts +76 -0
  114. package/dist/cloud/vault/index.d.ts.map +1 -0
  115. package/dist/cloud/vault/index.js +219 -0
  116. package/dist/cloud/vault/index.js.map +1 -0
  117. package/dist/daemon/agent-manager.d.ts +87 -0
  118. package/dist/daemon/agent-manager.d.ts.map +1 -0
  119. package/dist/daemon/agent-manager.js +412 -0
  120. package/dist/daemon/agent-manager.js.map +1 -0
  121. package/dist/daemon/agent-registry.d.ts +2 -0
  122. package/dist/daemon/agent-registry.d.ts.map +1 -1
  123. package/dist/daemon/agent-registry.js +3 -0
  124. package/dist/daemon/agent-registry.js.map +1 -1
  125. package/dist/daemon/api.d.ts +69 -0
  126. package/dist/daemon/api.d.ts.map +1 -0
  127. package/dist/daemon/api.js +425 -0
  128. package/dist/daemon/api.js.map +1 -0
  129. package/dist/daemon/cloud-sync.d.ts +101 -0
  130. package/dist/daemon/cloud-sync.d.ts.map +1 -0
  131. package/dist/daemon/cloud-sync.js +261 -0
  132. package/dist/daemon/cloud-sync.js.map +1 -0
  133. package/dist/daemon/index.d.ts +4 -0
  134. package/dist/daemon/index.d.ts.map +1 -1
  135. package/dist/daemon/index.js +6 -0
  136. package/dist/daemon/index.js.map +1 -1
  137. package/dist/daemon/orchestrator.d.ts +155 -0
  138. package/dist/daemon/orchestrator.d.ts.map +1 -0
  139. package/dist/daemon/orchestrator.js +736 -0
  140. package/dist/daemon/orchestrator.js.map +1 -0
  141. package/dist/daemon/router.d.ts +24 -0
  142. package/dist/daemon/router.d.ts.map +1 -1
  143. package/dist/daemon/router.js +71 -1
  144. package/dist/daemon/router.js.map +1 -1
  145. package/dist/daemon/server.d.ts +37 -0
  146. package/dist/daemon/server.d.ts.map +1 -1
  147. package/dist/daemon/server.js +191 -16
  148. package/dist/daemon/server.js.map +1 -1
  149. package/dist/daemon/types.d.ts +127 -0
  150. package/dist/daemon/types.d.ts.map +1 -0
  151. package/dist/daemon/types.js +6 -0
  152. package/dist/daemon/types.js.map +1 -0
  153. package/dist/daemon/workspace-manager.d.ts +75 -0
  154. package/dist/daemon/workspace-manager.d.ts.map +1 -0
  155. package/dist/daemon/workspace-manager.js +289 -0
  156. package/dist/daemon/workspace-manager.js.map +1 -0
  157. package/dist/dashboard/out/404.html +1 -1
  158. package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +1 -0
  159. package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +1 -0
  160. package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +1 -0
  161. package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +1 -0
  162. package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +1 -0
  163. package/dist/dashboard/out/_next/static/chunks/app/history/page-b6edd4dde8d08194.js +1 -0
  164. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
  165. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +1 -0
  166. package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +1 -0
  167. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-d80e03a5297f95b6.js +1 -0
  168. package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
  169. package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-c2f423b9c9f4591b.js} +1 -1
  170. package/dist/dashboard/out/_next/static/chunks/{webpack-c81f7fd28659d64f.js → webpack-a5acc2831d094776.js} +1 -1
  171. package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +1 -0
  172. package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +1 -0
  173. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  174. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  175. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  176. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  177. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  178. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
  179. package/dist/dashboard/out/alt-logos/logo.svg +38 -0
  180. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  181. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  182. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  183. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  184. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  185. package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
  186. package/dist/dashboard/out/app.html +14 -0
  187. package/dist/dashboard/out/app.txt +7 -0
  188. package/dist/dashboard/out/history.html +1 -0
  189. package/dist/dashboard/out/history.txt +7 -0
  190. package/dist/dashboard/out/index.html +1 -1
  191. package/dist/dashboard/out/index.txt +2 -2
  192. package/dist/dashboard/out/metrics.html +1 -515
  193. package/dist/dashboard/out/metrics.txt +2 -2
  194. package/dist/dashboard/out/pricing.html +13 -0
  195. package/dist/dashboard/out/pricing.txt +7 -0
  196. package/dist/dashboard-server/metrics.d.ts.map +1 -1
  197. package/dist/dashboard-server/metrics.js +3 -2
  198. package/dist/dashboard-server/metrics.js.map +1 -1
  199. package/dist/dashboard-server/server.d.ts.map +1 -1
  200. package/dist/dashboard-server/server.js +1279 -56
  201. package/dist/dashboard-server/server.js.map +1 -1
  202. package/dist/protocol/types.d.ts +10 -1
  203. package/dist/protocol/types.d.ts.map +1 -1
  204. package/dist/resiliency/context-persistence.d.ts +140 -0
  205. package/dist/resiliency/context-persistence.d.ts.map +1 -0
  206. package/dist/resiliency/context-persistence.js +397 -0
  207. package/dist/resiliency/context-persistence.js.map +1 -0
  208. package/dist/resiliency/health-monitor.d.ts +97 -0
  209. package/dist/resiliency/health-monitor.d.ts.map +1 -0
  210. package/dist/resiliency/health-monitor.js +291 -0
  211. package/dist/resiliency/health-monitor.js.map +1 -0
  212. package/dist/resiliency/index.d.ts +63 -0
  213. package/dist/resiliency/index.d.ts.map +1 -0
  214. package/dist/resiliency/index.js +63 -0
  215. package/dist/resiliency/index.js.map +1 -0
  216. package/dist/resiliency/logger.d.ts +114 -0
  217. package/dist/resiliency/logger.d.ts.map +1 -0
  218. package/dist/resiliency/logger.js +250 -0
  219. package/dist/resiliency/logger.js.map +1 -0
  220. package/dist/resiliency/metrics.d.ts +115 -0
  221. package/dist/resiliency/metrics.d.ts.map +1 -0
  222. package/dist/resiliency/metrics.js +239 -0
  223. package/dist/resiliency/metrics.js.map +1 -0
  224. package/dist/resiliency/provider-context.d.ts +100 -0
  225. package/dist/resiliency/provider-context.d.ts.map +1 -0
  226. package/dist/resiliency/provider-context.js +360 -0
  227. package/dist/resiliency/provider-context.js.map +1 -0
  228. package/dist/resiliency/supervisor.d.ts +109 -0
  229. package/dist/resiliency/supervisor.d.ts.map +1 -0
  230. package/dist/resiliency/supervisor.js +337 -0
  231. package/dist/resiliency/supervisor.js.map +1 -0
  232. package/dist/storage/adapter.d.ts +2 -0
  233. package/dist/storage/adapter.d.ts.map +1 -1
  234. package/dist/storage/adapter.js +12 -2
  235. package/dist/storage/adapter.js.map +1 -1
  236. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  237. package/dist/storage/sqlite-adapter.js +18 -14
  238. package/dist/storage/sqlite-adapter.js.map +1 -1
  239. package/dist/utils/index.d.ts +1 -0
  240. package/dist/utils/index.d.ts.map +1 -1
  241. package/dist/utils/index.js +1 -0
  242. package/dist/utils/index.js.map +1 -1
  243. package/dist/utils/logger.d.ts +40 -0
  244. package/dist/utils/logger.d.ts.map +1 -0
  245. package/dist/utils/logger.js +84 -0
  246. package/dist/utils/logger.js.map +1 -0
  247. package/dist/wrapper/client.d.ts +16 -1
  248. package/dist/wrapper/client.d.ts.map +1 -1
  249. package/dist/wrapper/client.js +32 -1
  250. package/dist/wrapper/client.js.map +1 -1
  251. package/dist/wrapper/parser.d.ts +3 -0
  252. package/dist/wrapper/parser.d.ts.map +1 -1
  253. package/dist/wrapper/parser.js +121 -18
  254. package/dist/wrapper/parser.js.map +1 -1
  255. package/dist/wrapper/pty-wrapper.d.ts +28 -1
  256. package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
  257. package/dist/wrapper/pty-wrapper.js +166 -30
  258. package/dist/wrapper/pty-wrapper.js.map +1 -1
  259. package/dist/wrapper/tmux-wrapper.d.ts +5 -0
  260. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  261. package/dist/wrapper/tmux-wrapper.js +58 -18
  262. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  263. package/docs/CLOUD-ARCHITECTURE.md +652 -0
  264. package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
  265. package/docs/TESTING_PRESENCE_FEATURES.md +327 -0
  266. package/docs/agent-relay-snippet.md +107 -4
  267. package/docs/guides/CLOUD.md +236 -0
  268. package/docs/guides/LOCAL.md +535 -0
  269. package/docs/guides/SELF-HOSTED.md +494 -0
  270. package/docs/proposals/shadow-as-subagent.md +765 -0
  271. package/docs/proposals/slack-bot-integration.md +1457 -0
  272. package/package.json +33 -4
  273. package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
  274. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
  275. package/dist/dashboard/out/_next/static/chunks/app/page-49055e5d2b5e34ec.js +0 -1
  276. package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
  277. package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
  278. /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_buildManifest.js +0 -0
  279. /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_ssgManifest.js +0 -0
  280. /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b2cd8d6485aacf2b.js} +0 -0
  281. /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-8f3f26864ce515e5.js} +0 -0
  282. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-0b990dbb71d72a98.js} +0 -0
  283. /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
@@ -0,0 +1,1983 @@
1
+ # Agent Relay Cloud - Onboarding Design
2
+
3
+ ## Overview
4
+
5
+ Agent Relay supports two deployment models. **Authentication is always handled by Agent Relay Cloud** - users don't self-host the auth system.
6
+
7
+ ### Deployment Models
8
+
9
+ **Model 1: Cloud Hosted** - We run everything
10
+
11
+ ```
12
+ ┌────────────────────────────────────────────────────────────────┐
13
+ │ AGENT RELAY CLOUD │
14
+ │ │
15
+ │ User connects accounts → We handle everything else │
16
+ │ │
17
+ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
18
+ │ │ Provider │ │ Agent │ │ Dashboard │ │
19
+ │ │ Auth │ │ Execution │ │ & Logs │ │
20
+ │ └────────────┘ └────────────┘ └────────────┘ │
21
+ │ ▲ │
22
+ │ │ │
23
+ │ ┌────────────┐ ┌─────┴──────┐ │
24
+ │ │ GitHub │──│ Cloned │ │
25
+ │ │ Webhooks │ │ Repos │ │
26
+ │ └────────────┘ └────────────┘ │
27
+ │ │
28
+ └────────────────────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ **Model 2: Self-Hosted** - User brings their own servers, auth via cloud
32
+
33
+ ```
34
+ ┌────────────────────────────────────────────────────────────────┐
35
+ │ AGENT RELAY CLOUD │
36
+ │ │
37
+ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
38
+ │ │ Provider │ │ Dashboard │ │ Team │ │
39
+ │ │ Auth/Vault │ │ & Logs │ │ Management │ │
40
+ │ └─────┬──────┘ └─────▲──────┘ └────────────┘ │
41
+ └─────────┼───────────────┼──────────────────────────────────────┘
42
+ │ Credentials │ Sync
43
+ ▼ │
44
+ ┌─────────────────────────┴──────────────────────────────────────┐
45
+ │ USER'S INFRASTRUCTURE │
46
+ │ │
47
+ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
48
+ │ │agent-relay │ │ Agents │ │ Repos │ │
49
+ │ │ daemon │ │ (claude, │ │ (local) │ │
50
+ │ │ │ │ codex) │ │ │ │
51
+ │ └────────────┘ └────────────┘ └────────────┘ │
52
+ │ │
53
+ │ User's VMs, Kubernetes, containers, or bare metal │
54
+ └────────────────────────────────────────────────────────────────┘
55
+ ```
56
+
57
+ ### Feature Comparison
58
+
59
+ | Feature | Cloud Hosted | Self-Hosted |
60
+ |---------|--------------|-------------|
61
+ | **Provider auth** | ✅ Cloud (device flow) | ✅ Cloud (device flow) |
62
+ | **Credential vault** | ✅ Cloud | ✅ Cloud → synced to user's infra |
63
+ | **Dashboard** | ✅ Cloud | ✅ Cloud (synced from user's infra) |
64
+ | **Team features** | ✅ Full | ✅ Full |
65
+ | **Agent execution** | Our servers | User's servers |
66
+ | **Repos** | Cloned to cloud | User clones locally |
67
+ | **Compute costs** | Included in plan | User pays own infra |
68
+ | **Data locality** | Our cloud regions | User's choice |
69
+
70
+ ### When to Use Each
71
+
72
+ **Cloud Hosted** - Best for:
73
+ - Teams wanting zero infrastructure management
74
+ - Quick start - no servers to provision
75
+ - Smaller teams without dedicated DevOps
76
+ - CI/CD integration with GitHub Actions
77
+
78
+ **Self-Hosted** - Best for:
79
+ - Enterprises requiring compute in specific regions/clouds
80
+ - Teams with existing Kubernetes/container infrastructure
81
+ - Cost optimization for high-volume usage
82
+ - Custom security requirements (VPC, firewall rules)
83
+ - GPU workloads on specialized hardware
84
+
85
+ ---
86
+
87
+ ## Cloud Hosted Mode
88
+
89
+ Agent Relay Cloud (fully hosted) provides:
90
+ - Automatic server provisioning with supervisor
91
+ - GitHub repository integration
92
+ - Multi-provider agent authentication via device flow
93
+ - Team management and collaboration
94
+ - Centralized dashboard and logs
95
+
96
+ ---
97
+
98
+ ## Self-Hosted Mode
99
+
100
+ For users running agent-relay on their own infrastructure. **Authentication happens via Agent Relay Cloud servers** - users connect to our infrastructure to run the browser-based auth flows.
101
+
102
+ ### Why Self-Hosted Has More Friction (Intentional)
103
+
104
+ Self-hosted requires extra steps compared to cloud-hosted:
105
+
106
+ | Step | Cloud Hosted | Self-Hosted |
107
+ |------|--------------|-------------|
108
+ | 1. Sign up | GitHub OAuth | GitHub OAuth |
109
+ | 2. Connect providers | Click "Login with X" | Connect to cloud server, then "Login with X" |
110
+ | 3. Select repos | Select from list | Clone repos locally |
111
+ | 4. Start agents | Automatic | Install agent-relay, configure, start |
112
+ | 5. View dashboard | Just visit URL | Logs sync to cloud dashboard |
113
+
114
+ **This is intentional** - cloud-hosted should be the path of least resistance.
115
+
116
+ ### Self-Hosted Auth Flow
117
+
118
+ Since Claude Code and Codex require browser-based OAuth, self-hosted users must connect to our cloud to authenticate:
119
+
120
+ ```
121
+ ┌─────────────────────────────────────────────────────────────────┐
122
+ │ SELF-HOSTED AUTHENTICATION │
123
+ │ │
124
+ │ User's Server Agent Relay Cloud │
125
+ │ ───────────── ───────────────── │
126
+ │ │
127
+ │ 1. agent-relay cloud login │
128
+ │ │ │
129
+ │ │ ──────────────────────────────────────────────────> │
130
+ │ │ Connect to cloud auth service │
131
+ │ │ │
132
+ │ │ 2. Cloud opens browser │
133
+ │ │ to provider (Anthropic) │
134
+ │ │ │ │
135
+ │ │ ▼ │
136
+ │ │ 3. User logs in, │
137
+ │ │ authorizes │
138
+ │ │ │ │
139
+ │ │ ▼ │
140
+ │ │ 4. Tokens stored │
141
+ │ │ in cloud vault │
142
+ │ │ │
143
+ │ │ <────────────────────────────────────────────────── │
144
+ │ │ Sync encrypted credentials │
145
+ │ ▼ │
146
+ │ 5. Credentials cached locally │
147
+ │ (encrypted, auto-refresh via cloud) │
148
+ │ │
149
+ └─────────────────────────────────────────────────────────────────┘
150
+ ```
151
+
152
+ ### Setup Flow
153
+
154
+ ```bash
155
+ # On user's server
156
+ npm install -g agent-relay
157
+
158
+ # Connect to cloud - opens browser on YOUR machine (not server)
159
+ # via a temporary secure tunnel
160
+ agent-relay cloud login
161
+ # → Opens: https://relay.cloud/auth/remote?session=abc123
162
+ # → You authenticate in your browser
163
+ # → Credentials sync to your server
164
+
165
+ # Connect providers (each opens browser via cloud)
166
+ agent-relay cloud connect anthropic
167
+ agent-relay cloud connect openai
168
+
169
+ # Start daemon - credentials synced from cloud
170
+ agent-relay up
171
+ ```
172
+
173
+ ### CLI Experience
174
+
175
+ ```
176
+ $ agent-relay cloud login
177
+
178
+ To authenticate, open this URL in your browser:
179
+
180
+ ┌────────────────────────────────────────────────────────────┐
181
+ │ https://relay.cloud/auth/remote?session=xK9mPq2R │
182
+ └────────────────────────────────────────────────────────────┘
183
+
184
+ Or scan this QR code:
185
+ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
186
+ █ ▄▄▄▄▄ █ █ ▄▄█
187
+ █ █ █ █▄▄ ▀█
188
+ █ █▄▄▄█ █ ▄▀▀▄█
189
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
190
+
191
+ ⏳ Waiting for authentication...
192
+ Session expires in 9:45
193
+
194
+ ✅ Authenticated as user@example.com
195
+
196
+ Connected providers:
197
+ • Anthropic (Claude) ❌ Not connected
198
+ • OpenAI (Codex) ❌ Not connected
199
+
200
+ To connect providers:
201
+ agent-relay cloud connect anthropic
202
+
203
+ $ agent-relay cloud connect anthropic
204
+
205
+ To connect Anthropic, open this URL in your browser:
206
+
207
+ ┌────────────────────────────────────────────────────────────┐
208
+ │ https://relay.cloud/connect/anthropic?session=yL8nQr3S │
209
+ └────────────────────────────────────────────────────────────┘
210
+
211
+ ⏳ Waiting for authorization...
212
+
213
+ ✅ Anthropic connected!
214
+ Logged in as: claude-user@example.com
215
+ Plan: Claude Pro
216
+
217
+ Credentials synced to this server.
218
+ Run 'agent-relay up' to start.
219
+ ```
220
+
221
+ ### Why This Approach
222
+
223
+ 1. **Auth happens on cloud** - Browser-based OAuth works because it runs on our servers
224
+ 2. **Credentials sync down** - Encrypted tokens pushed to user's server
225
+ 3. **More steps than cloud** - Intentional friction encourages cloud adoption
226
+ 4. **Still requires cloud** - Users can't fully disconnect from our service
227
+ 5. **Refresh via cloud** - Token refresh happens through our service, maintaining dependency
228
+
229
+ ### Credential Sync Architecture
230
+
231
+ ```
232
+ ┌────────────────────────────────────────────────────────────────┐
233
+ │ AGENT RELAY CLOUD │
234
+ │ │
235
+ │ ┌──────────────────────────────────────────────────────┐ │
236
+ │ │ Credential Vault (encrypted) │ │
237
+ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
238
+ │ │ │Anthropic │ │ OpenAI │ │ Google │ │ │
239
+ │ │ │ tokens │ │ tokens │ │ tokens │ │ │
240
+ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
241
+ │ └───────┼─────────────┼─────────────┼─────────────────┘ │
242
+ │ └─────────────┼─────────────┘ │
243
+ │ │ │
244
+ │ ▼ Encrypted sync (TLS + E2E) │
245
+ └─────────────────────────┼───────────────────────────────────────┘
246
+
247
+ ┌─────────────────────────▼───────────────────────────────────────┐
248
+ │ USER'S INFRASTRUCTURE │
249
+ │ │
250
+ │ ┌──────────────────────────────────────────────────────────┐ │
251
+ │ │ agent-relay daemon │ │
252
+ │ │ │ │
253
+ │ │ ┌────────────────────────────────────────────────────┐ │ │
254
+ │ │ │ Local credential cache (encrypted, auto-refresh) │ │ │
255
+ │ │ └────────────────────────────────────────────────────┘ │ │
256
+ │ │ │ │ │
257
+ │ │ ┌──────────┼──────────┐ │ │
258
+ │ │ ▼ ▼ ▼ │ │
259
+ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
260
+ │ │ │ claude │ │ codex │ │ gemini │ │ │
261
+ │ │ │ agent │ │ agent │ │ agent │ │ │
262
+ │ │ └────────┘ └────────┘ └────────┘ │ │
263
+ │ └──────────────────────────────────────────────────────────┘ │
264
+ │ │
265
+ │ Logs synced back to cloud dashboard │
266
+ └──────────────────────────────────────────────────────────────────┘
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Provider Authentication Architecture
272
+
273
+ ### Design Principle: Login-Only Authentication
274
+
275
+ **No API keys in the UI** - Users authenticate via login flows, not by pasting keys. This provides:
276
+
277
+ - **Better security**: No keys to leak, rotate, or manage
278
+ - **Consistent UX**: Always "Login with X" buttons
279
+ - **Account linking**: Users authenticate with their existing provider accounts
280
+ - **Automatic token refresh**: Where supported
281
+
282
+ ### Provider Authentication Reality Check
283
+
284
+ Different providers have different OAuth maturity levels. Notably, **both Claude Code and
285
+ OpenAI Codex** use browser-based OAuth that doesn't support headless/cloud environments well:
286
+
287
+ | Provider | Auth Flow | Status | Notes |
288
+ |----------|-----------|--------|-------|
289
+ | Claude/Anthropic | Browser OAuth | ⚠️ Device Flow | Opens browser, no redirect URI support |
290
+ | OpenAI/Codex | Browser OAuth | ⚠️ Device Flow | Opens browser for ChatGPT login |
291
+ | Google/Gemini | OAuth 2.0 | ✅ Redirect | Standard Google OAuth with redirect |
292
+ | GitHub Copilot | OAuth 2.0 | ✅ Redirect | Via GitHub OAuth (auto from signup) |
293
+ | Azure OpenAI | OAuth 2.0 | ✅ Redirect | Via Microsoft Entra ID |
294
+ | Local/Self-hosted | None | ✅ N/A | Just endpoint URL |
295
+
296
+ **Key insight**: The two most popular coding agents (Claude Code and Codex) both require
297
+ device flow or similar workarounds for cloud/headless environments. Both have open GitHub
298
+ issues requesting proper headless auth support:
299
+ - [anthropics/claude-code#7100](https://github.com/anthropics/claude-code/issues/7100)
300
+ - [openai/codex#2798](https://github.com/openai/codex/issues/2798)
301
+
302
+ ### Device Flow Authentication Strategy (Claude + Codex)
303
+
304
+ Both Claude Code and OpenAI Codex use browser-based OAuth that stores tokens locally:
305
+ - **Claude Code**: `claude /login` → opens browser → stores in `~/.claude/.credentials.json`
306
+ - **Codex**: `codex` → opens browser for ChatGPT → stores in `~/.codex/`
307
+
308
+ For a cloud environment, we need a **device authorization flow** (RFC 8628):
309
+
310
+ ```
311
+ ┌─────────────────────────────────────────────────────────────────┐
312
+ │ Agent Relay Cloud - Claude Code Auth Flow │
313
+ ├─────────────────────────────────────────────────────────────────┤
314
+ │ │
315
+ │ Step 1: User clicks "Login with Anthropic" in our dashboard │
316
+ │ ↓ │
317
+ │ Step 2: Opens popup/redirect to Anthropic's OAuth │
318
+ │ (same flow as `claude /login`) │
319
+ │ ↓ │
320
+ │ Step 3: User authenticates with Anthropic │
321
+ │ ↓ │
322
+ │ Step 4: Anthropic redirects back with auth token │
323
+ │ ↓ │
324
+ │ Step 5: We store encrypted token in credential vault │
325
+ │ ↓ │
326
+ │ Step 6: When spawning agents, inject token via: │
327
+ │ - ANTHROPIC_AUTH_TOKEN env var, or │
328
+ │ - Mount equivalent of ~/.claude/.credentials.json │
329
+ │ │
330
+ └─────────────────────────────────────────────────────────────────┘
331
+ ```
332
+
333
+ **Note**: This requires Anthropic to support redirect-based OAuth (vs device-only flow). If not available, fallback options:
334
+
335
+ 1. **Device Authorization Flow**: Display code, user enters at anthropic.com
336
+ 2. **Credential File Upload**: User runs `/login` locally, uploads credential file
337
+ 3. **API Key (hidden)**: Accept API key but label it as "Access Token" for consistent UX
338
+
339
+ ### Proposed Solution: Provider Credentials Vault
340
+
341
+ ```
342
+ ┌─────────────────────────────────────────────────────────────────┐
343
+ │ Agent Relay Cloud │
344
+ ├─────────────────────────────────────────────────────────────────┤
345
+ │ │
346
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
347
+ │ │ GitHub │ │ Provider │ │ Secrets │ │
348
+ │ │ OAuth │ │ Connector │ │ Vault │ │
349
+ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
350
+ │ │ │ │ │
351
+ │ ▼ ▼ ▼ │
352
+ │ ┌─────────────────────────────────────────────────────────┐ │
353
+ │ │ Onboarding Flow │ │
354
+ │ │ 1. Sign up (GitHub OAuth) │ │
355
+ │ │ 2. Connect repositories │ │
356
+ │ │ 3. Add agent providers │ │
357
+ │ │ 4. Configure teams │ │
358
+ │ └─────────────────────────────────────────────────────────┘ │
359
+ │ │
360
+ └─────────────────────────────────────────────────────────────────┘
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Onboarding Flow Design
366
+
367
+ ### Step 1: Sign Up via GitHub OAuth
368
+
369
+ ```
370
+ ┌─────────────────────────────────────────────┐
371
+ │ Welcome to Agent Relay Cloud │
372
+ │ │
373
+ │ Orchestrate AI agents across your repos │
374
+ │ │
375
+ │ ┌─────────────────────────────────────┐ │
376
+ │ │ 🔗 Continue with GitHub │ │
377
+ │ └─────────────────────────────────────┘ │
378
+ │ │
379
+ │ By signing up, you agree to our Terms │
380
+ └─────────────────────────────────────────────┘
381
+ ```
382
+
383
+ **Why GitHub first?**
384
+ - Natural auth for developers
385
+ - Immediate access to repo list
386
+ - Repository permissions already defined
387
+ - GitHub Apps for webhook integration
388
+
389
+ ### Step 2: Connect Repositories
390
+
391
+ ```
392
+ ┌─────────────────────────────────────────────────────────────┐
393
+ │ Select Repositories │
394
+ │ │
395
+ │ Which repositories should Agent Relay manage? │
396
+ │ │
397
+ │ ┌────────────────────────────────────────────────────────┐ │
398
+ │ │ 🔍 Search repositories... │ │
399
+ │ └────────────────────────────────────────────────────────┘ │
400
+ │ │
401
+ │ ☑️ acme/frontend ⭐ 234 Updated 2 hours ago │
402
+ │ ☑️ acme/backend-api ⭐ 156 Updated 1 day ago │
403
+ │ ☐ acme/docs ⭐ 45 Updated 3 days ago │
404
+ │ ☐ acme/mobile-app ⭐ 89 Updated 1 week ago │
405
+ │ │
406
+ │ ┌──────────────┐ ┌──────────────────────────────────────┐ │
407
+ │ │ Back │ │ Continue with 2 repositories → │ │
408
+ │ └──────────────┘ └──────────────────────────────────────┘ │
409
+ └─────────────────────────────────────────────────────────────┘
410
+ ```
411
+
412
+ **What happens behind the scenes:**
413
+ - Install GitHub App on selected repos
414
+ - Clone repos to cloud workspace
415
+ - Detect existing `.claude/agents/` or `teams.json` configs
416
+ - Set up webhooks for PR/issue events
417
+
418
+ ### Step 3: Add Agent Providers (The Key Step)
419
+
420
+ ```
421
+ ┌─────────────────────────────────────────────────────────────────┐
422
+ │ Connect Your AI Providers │
423
+ │ │
424
+ │ Agent Relay works with multiple AI providers. Connect the │
425
+ │ ones you want to use: │
426
+ │ │
427
+ │ ┌─────────────────────────────────────────────────────────────┐│
428
+ │ │ ANTHROPIC ││
429
+ │ │ Claude Code ││
430
+ │ │ ⚡ Recommended for code tasks ││
431
+ │ │ ││
432
+ │ │ ┌─────────────────────────────────────────────────────┐ ││
433
+ │ │ │ 🔐 Login with Anthropic │ ││
434
+ │ │ └─────────────────────────────────────────────────────┘ ││
435
+ │ └─────────────────────────────────────────────────────────────┘│
436
+ │ │
437
+ │ ┌─────────────────────────────────────────────────────────────┐│
438
+ │ │ OPENAI ││
439
+ │ │ Codex, ChatGPT ││
440
+ │ │ Good for diverse tasks ││
441
+ │ │ ││
442
+ │ │ ┌─────────────────────────────────────────────────────┐ ││
443
+ │ │ │ 🔐 Login with OpenAI │ ││
444
+ │ │ └─────────────────────────────────────────────────────┘ ││
445
+ │ └─────────────────────────────────────────────────────────────┘│
446
+ │ │
447
+ │ ┌─────────────────────────────────────────────────────────────┐│
448
+ │ │ GOOGLE ││
449
+ │ │ Gemini ││
450
+ │ │ Multi-modal capabilities ││
451
+ │ │ ││
452
+ │ │ ┌─────────────────────────────────────────────────────┐ ││
453
+ │ │ │ 🔐 Login with Google │ ││
454
+ │ │ └─────────────────────────────────────────────────────┘ ││
455
+ │ └─────────────────────────────────────────────────────────────┘│
456
+ │ │
457
+ │ ┌─────────────────────────────────────────────────────────────┐│
458
+ │ │ GITHUB COPILOT ✓ Connected ││
459
+ │ │ Already connected via your GitHub account ││
460
+ │ └─────────────────────────────────────────────────────────────┘│
461
+ │ │
462
+ │ ┌─────────────────────────────────────────────────────────────┐│
463
+ │ │ + Add Self-Hosted Provider ││
464
+ │ │ Ollama, LM Studio, or other local tools ││
465
+ │ └─────────────────────────────────────────────────────────────┘│
466
+ │ │
467
+ │ You can always add more providers later in Settings │
468
+ │ │
469
+ │ ┌──────────────┐ ┌──────────────────────────────────────────┐ │
470
+ │ │ Skip │ │ Continue → │ │
471
+ │ └──────────────┘ └──────────────────────────────────────────┘ │
472
+ └─────────────────────────────────────────────────────────────────┘
473
+ ```
474
+
475
+ #### OAuth Login Flow
476
+
477
+ When user clicks "Login with [Provider]":
478
+
479
+ ```
480
+ ┌─────────────────────────────────────────────────────────────┐
481
+ │ │
482
+ │ ┌─────────────────────────────────────────┐ │
483
+ │ │ provider-name.com │ │
484
+ │ ├─────────────────────────────────────────┤ │
485
+ │ │ │ │
486
+ │ │ Sign in to Anthropic │ │
487
+ │ │ │ │
488
+ │ │ ┌───────────────────────────────────┐ │ │
489
+ │ │ │ email@example.com │ │ │
490
+ │ │ └───────────────────────────────────┘ │ │
491
+ │ │ ┌───────────────────────────────────┐ │ │
492
+ │ │ │ •••••••••••• │ │ │
493
+ │ │ └───────────────────────────────────┘ │ │
494
+ │ │ │ │
495
+ │ │ ┌───────────────────────────────────┐ │ │
496
+ │ │ │ Sign In │ │ │
497
+ │ │ └───────────────────────────────────┘ │ │
498
+ │ │ │ │
499
+ │ │ Or continue with: │ │
500
+ │ │ [Google] [GitHub] [SSO] │ │
501
+ │ │ │ │
502
+ │ └─────────────────────────────────────────┘ │
503
+ │ │
504
+ └─────────────────────────────────────────────────────────────┘
505
+ ```
506
+
507
+ After login, authorization consent:
508
+
509
+ ```
510
+ ┌─────────────────────────────────────────────────────────────┐
511
+ │ │
512
+ │ ┌─────────────────────────────────────────┐ │
513
+ │ │ │ │
514
+ │ │ Authorize Agent Relay Cloud │ │
515
+ │ │ │ │
516
+ │ │ Agent Relay Cloud wants to: │ │
517
+ │ │ │ │
518
+ │ │ ✓ Run AI agents on your behalf │ │
519
+ │ │ ✓ Access your usage quota │ │
520
+ │ │ ✓ View your account info │ │
521
+ │ │ │ │
522
+ │ │ Signed in as: user@example.com │ │
523
+ │ │ │ │
524
+ │ │ ┌─────────────┐ ┌─────────────────┐ │ │
525
+ │ │ │ Cancel │ │ Authorize │ │ │
526
+ │ │ └─────────────┘ └─────────────────┘ │ │
527
+ │ │ │ │
528
+ │ └─────────────────────────────────────────┘ │
529
+ │ │
530
+ └─────────────────────────────────────────────────────────────┘
531
+ ```
532
+
533
+ After authorization, redirect back with success:
534
+
535
+ ```
536
+ ┌─────────────────────────────────────────────────────────────────┐
537
+ │ Connect Your AI Providers │
538
+ │ │
539
+ │ ┌─────────────────────────────────────────────────────────────┐│
540
+ │ │ ANTHROPIC ✓ Connected ││
541
+ │ │ Claude Code ││
542
+ │ │ Logged in as claude-user@example.com ││
543
+ │ │ [Disconnect] ││
544
+ │ └─────────────────────────────────────────────────────────────┘│
545
+ │ ... │
546
+ └─────────────────────────────────────────────────────────────────┘
547
+ ```
548
+
549
+ ---
550
+
551
+ ## Device Authorization Flow (RFC 8628)
552
+
553
+ This is the **primary authentication method** for Claude Code and OpenAI Codex, since neither
554
+ supports standard OAuth redirect flows for third-party cloud applications.
555
+
556
+ ### How Device Flow Works
557
+
558
+ ```
559
+ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
560
+ │ Agent Relay │ │ Provider │ │ User's │
561
+ │ Cloud │ │ Auth Server │ │ Browser │
562
+ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
563
+ │ │ │
564
+ │ 1. POST /device/code │ │
565
+ │ {client_id, scope} │ │
566
+ │ ───────────────────────>│ │
567
+ │ │ │
568
+ │ {device_code, │ │
569
+ │ user_code: "ABCD-1234"│ │
570
+ │ verification_uri, │ │
571
+ │ expires_in: 900} │ │
572
+ │ <───────────────────────│ │
573
+ │ │ │
574
+ │ Display code to user │ │
575
+ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│
576
+ │ │ │
577
+ │ │ 2. User visits URL │
578
+ │ │ & enters user_code │
579
+ │ │<────────────────────────│
580
+ │ │ │
581
+ │ │ 3. User authenticates │
582
+ │ │ & authorizes app │
583
+ │ │<────────────────────────│
584
+ │ │ │
585
+ │ 4. POST /token │ │
586
+ │ {device_code} │ │
587
+ │ (polling every 5s) │ │
588
+ │ ───────────────────────>│ │
589
+ │ │ │
590
+ │ "authorization_pending"│ │
591
+ │ <───────────────────────│ (keep polling...) │
592
+ │ │ │
593
+ │ 5. POST /token │ │
594
+ │ ───────────────────────>│ │
595
+ │ │ │
596
+ │ {access_token, │ │
597
+ │ refresh_token} │ │
598
+ │ <───────────────────────│ │
599
+ │ │ │
600
+ ```
601
+
602
+ ### Provider-Specific Device Flow URLs
603
+
604
+ | Provider | Device Code URL | Token URL | Verification URL |
605
+ |----------|-----------------|-----------|------------------|
606
+ | Anthropic | `api.anthropic.com/oauth/device/code` | `api.anthropic.com/oauth/token` | `console.anthropic.com/device` |
607
+ | OpenAI | `auth.openai.com/device/code` | `auth.openai.com/oauth/token` | `auth.openai.com/device` |
608
+ | Google | `oauth2.googleapis.com/device/code` | `oauth2.googleapis.com/token` | `google.com/device` |
609
+ | GitHub | `github.com/login/device/code` | `github.com/login/oauth/access_token` | `github.com/login/device` |
610
+
611
+ *Note: Anthropic and OpenAI URLs are hypothetical - these providers would need to implement
612
+ RFC 8628 device authorization. Currently, they only support browser-based OAuth.*
613
+
614
+ ### UI Flow
615
+
616
+ **Step 1: User clicks "Login with [Provider]"**
617
+
618
+ ```
619
+ ┌─────────────────────────────────────────────────────────────────┐
620
+ │ Connect Claude Code │
621
+ │ │
622
+ │ To connect your Anthropic account: │
623
+ │ │
624
+ │ ┌─────────────────────────────────────────────────────────────┐│
625
+ │ │ 1. Click below to open Anthropic in a new tab ││
626
+ │ │ 2. Sign in with your Anthropic account ││
627
+ │ │ 3. Enter the code shown here when prompted ││
628
+ │ └─────────────────────────────────────────────────────────────┘│
629
+ │ │
630
+ │ ┌─────────────────────────────────────────────────────────────┐│
631
+ │ │ 🔐 Open Anthropic → ││
632
+ │ └─────────────────────────────────────────────────────────────┘│
633
+ │ │
634
+ └─────────────────────────────────────────────────────────────────┘
635
+ ```
636
+
637
+ **Step 2: Show code, user enters at provider**
638
+
639
+ ```
640
+ ┌─────────────────────────────────────────────────────────────────┐
641
+ │ Connect Claude Code │
642
+ │ │
643
+ │ Enter this code at Anthropic: │
644
+ │ │
645
+ │ ┌─────────────────────────────────────────────────────────────┐│
646
+ │ │ ││
647
+ │ │ WDJB-MJPV ││
648
+ │ │ ││
649
+ │ └─────────────────────────────────────────────────────────────┘│
650
+ │ [Copy] │
651
+ │ │
652
+ │ A browser tab should have opened to console.anthropic.com │
653
+ │ Didn't open? Click here → │
654
+ │ │
655
+ │ ───────────────────────────────────────────────────────────────│
656
+ │ │
657
+ │ ⏳ Waiting for you to authorize... │
658
+ │ Code expires in 14:32 │
659
+ │ │
660
+ │ ┌──────────────┐ │
661
+ │ │ Cancel │ │
662
+ │ └──────────────┘ │
663
+ └─────────────────────────────────────────────────────────────────┘
664
+ ```
665
+
666
+ **Same flow for Codex:**
667
+
668
+ ```
669
+ ┌─────────────────────────────────────────────────────────────────┐
670
+ │ Connect OpenAI Codex │
671
+ │ │
672
+ │ Enter this code at OpenAI: │
673
+ │ │
674
+ │ ┌─────────────────────────────────────────────────────────────┐│
675
+ │ │ ││
676
+ │ │ XKCD-4815 ││
677
+ │ │ ││
678
+ │ └─────────────────────────────────────────────────────────────┘│
679
+ │ [Copy] │
680
+ │ │
681
+ │ A browser tab should have opened to auth.openai.com │
682
+ │ Didn't open? Click here → │
683
+ │ │
684
+ │ ───────────────────────────────────────────────────────────────│
685
+ │ │
686
+ │ ⏳ Waiting for you to authorize... │
687
+ │ Code expires in 14:47 │
688
+ │ │
689
+ │ ┌──────────────┐ │
690
+ │ │ Cancel │ │
691
+ │ └──────────────┘ │
692
+ └─────────────────────────────────────────────────────────────────┘
693
+ ```
694
+
695
+ **Step 3: Success**
696
+
697
+ ```
698
+ ┌─────────────────────────────────────────────────────────────────┐
699
+ │ │
700
+ │ ✅ Connected! │
701
+ │ │
702
+ │ Your Anthropic account is now linked. │
703
+ │ │
704
+ │ ┌─────────────────────────────────────────────────────────────┐│
705
+ │ │ Account: user@example.com ││
706
+ │ │ Plan: Claude Pro ││
707
+ │ └─────────────────────────────────────────────────────────────┘│
708
+ │ │
709
+ │ ┌─────────────────────────────────────────────────────────────┐│
710
+ │ │ Continue → ││
711
+ │ └─────────────────────────────────────────────────────────────┘│
712
+ │ │
713
+ └─────────────────────────────────────────────────────────────────┘
714
+ ```
715
+
716
+ **Error States:**
717
+
718
+ ```
719
+ Code Expired:
720
+ ┌─────────────────────────────────────────────────────────────────┐
721
+ │ ⚠️ Code Expired │
722
+ │ │
723
+ │ The authorization code has expired. This happens if you │
724
+ │ don't complete the sign-in within 15 minutes. │
725
+ │ │
726
+ │ ┌───────────────────┐ ┌───────────────────────────────────┐ │
727
+ │ │ Cancel │ │ Get New Code │ │
728
+ │ └───────────────────┘ └───────────────────────────────────┘ │
729
+ └─────────────────────────────────────────────────────────────────┘
730
+
731
+ Access Denied:
732
+ ┌─────────────────────────────────────────────────────────────────┐
733
+ │ ❌ Access Denied │
734
+ │ │
735
+ │ You denied the authorization request at Anthropic. │
736
+ │ │
737
+ │ ┌───────────────────┐ ┌───────────────────────────────────┐ │
738
+ │ │ Cancel │ │ Try Again │ │
739
+ │ └───────────────────┘ └───────────────────────────────────┘ │
740
+ └─────────────────────────────────────────────────────────────────┘
741
+ ```
742
+
743
+ ---
744
+
745
+ ### Device Flow Implementation
746
+
747
+ ```typescript
748
+ // src/cloud/auth/device-flow.ts
749
+
750
+ interface DeviceCodeResponse {
751
+ device_code: string; // Secret - we use this for polling
752
+ user_code: string; // Display to user: "WDJB-MJPV"
753
+ verification_uri: string; // Where user goes: console.anthropic.com/device
754
+ verification_uri_complete?: string; // URL with code pre-filled (optional)
755
+ expires_in: number; // Seconds until codes expire (typically 900)
756
+ interval: number; // Min seconds between poll requests (typically 5)
757
+ }
758
+
759
+ interface DeviceFlowConfig {
760
+ provider: string;
761
+ deviceCodeUrl: string;
762
+ tokenUrl: string;
763
+ clientId: string;
764
+ scopes: string[];
765
+ }
766
+
767
+ const DEVICE_FLOW_CONFIGS: Record<string, DeviceFlowConfig> = {
768
+ anthropic: {
769
+ provider: 'anthropic',
770
+ deviceCodeUrl: 'https://api.anthropic.com/oauth/device/code',
771
+ tokenUrl: 'https://api.anthropic.com/oauth/token',
772
+ clientId: process.env.ANTHROPIC_CLIENT_ID!,
773
+ scopes: ['claude-code:execute', 'user:read']
774
+ },
775
+ openai: {
776
+ provider: 'openai',
777
+ deviceCodeUrl: 'https://auth.openai.com/device/code',
778
+ tokenUrl: 'https://auth.openai.com/oauth/token',
779
+ clientId: process.env.OPENAI_CLIENT_ID!,
780
+ scopes: ['openid', 'profile', 'email', 'codex:execute']
781
+ }
782
+ };
783
+
784
+ class DeviceFlowAuth {
785
+ private config: DeviceFlowConfig;
786
+
787
+ constructor(provider: string) {
788
+ this.config = DEVICE_FLOW_CONFIGS[provider];
789
+ if (!this.config) {
790
+ throw new Error(`No device flow config for provider: ${provider}`);
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Step 1: Request device and user codes from provider
796
+ */
797
+ async requestCodes(): Promise<DeviceCodeResponse> {
798
+ const response = await fetch(this.config.deviceCodeUrl, {
799
+ method: 'POST',
800
+ headers: {
801
+ 'Content-Type': 'application/x-www-form-urlencoded'
802
+ },
803
+ body: new URLSearchParams({
804
+ client_id: this.config.clientId,
805
+ scope: this.config.scopes.join(' ')
806
+ })
807
+ });
808
+
809
+ if (!response.ok) {
810
+ const error = await response.text();
811
+ throw new Error(`Failed to get device code: ${error}`);
812
+ }
813
+
814
+ return response.json();
815
+ }
816
+
817
+ /**
818
+ * Step 2: Poll for tokens (called repeatedly until success/failure)
819
+ */
820
+ async pollForToken(deviceCode: string): Promise<PollResult> {
821
+ const response = await fetch(this.config.tokenUrl, {
822
+ method: 'POST',
823
+ headers: {
824
+ 'Content-Type': 'application/x-www-form-urlencoded'
825
+ },
826
+ body: new URLSearchParams({
827
+ client_id: this.config.clientId,
828
+ device_code: deviceCode,
829
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
830
+ })
831
+ });
832
+
833
+ const data = await response.json();
834
+
835
+ // RFC 8628 error codes
836
+ if (data.error) {
837
+ switch (data.error) {
838
+ case 'authorization_pending':
839
+ // User hasn't completed authorization yet
840
+ return { status: 'pending' };
841
+
842
+ case 'slow_down':
843
+ // Polling too fast - increase interval
844
+ return {
845
+ status: 'slow_down',
846
+ retryAfter: data.interval || 10
847
+ };
848
+
849
+ case 'expired_token':
850
+ // Device code expired
851
+ return { status: 'expired' };
852
+
853
+ case 'access_denied':
854
+ // User denied authorization
855
+ return { status: 'denied' };
856
+
857
+ default:
858
+ return {
859
+ status: 'error',
860
+ error: data.error_description || data.error
861
+ };
862
+ }
863
+ }
864
+
865
+ // Success!
866
+ return {
867
+ status: 'success',
868
+ tokens: {
869
+ accessToken: data.access_token,
870
+ refreshToken: data.refresh_token,
871
+ expiresIn: data.expires_in,
872
+ scope: data.scope
873
+ }
874
+ };
875
+ }
876
+ }
877
+
878
+ type PollResult =
879
+ | { status: 'pending' }
880
+ | { status: 'slow_down'; retryAfter: number }
881
+ | { status: 'expired' }
882
+ | { status: 'denied' }
883
+ | { status: 'error'; error: string }
884
+ | { status: 'success'; tokens: TokenSet };
885
+
886
+ interface TokenSet {
887
+ accessToken: string;
888
+ refreshToken: string;
889
+ expiresIn: number;
890
+ scope: string;
891
+ }
892
+ ```
893
+
894
+ ### Device Flow API Routes
895
+
896
+ ```typescript
897
+ // src/cloud/api/device-flow.ts
898
+
899
+ const router = Router();
900
+
901
+ // Store for active device flows (use Redis in production)
902
+ const activeFlows = new Map<string, ActiveFlow>();
903
+
904
+ interface ActiveFlow {
905
+ userId: string;
906
+ provider: string;
907
+ deviceCode: string;
908
+ userCode: string;
909
+ verificationUri: string;
910
+ verificationUriComplete?: string;
911
+ expiresAt: Date;
912
+ pollInterval: number;
913
+ status: 'pending' | 'success' | 'expired' | 'denied' | 'error';
914
+ tokens?: TokenSet;
915
+ error?: string;
916
+ }
917
+
918
+ /**
919
+ * POST /api/device-flow/:provider/start
920
+ * Initiates device flow, returns user code to display
921
+ */
922
+ router.post('/:provider/start', async (req, res) => {
923
+ const { provider } = req.params;
924
+ const userId = req.session.userId;
925
+
926
+ try {
927
+ const auth = new DeviceFlowAuth(provider);
928
+ const codes = await auth.requestCodes();
929
+
930
+ const flowId = crypto.randomUUID();
931
+
932
+ activeFlows.set(flowId, {
933
+ userId,
934
+ provider,
935
+ deviceCode: codes.device_code,
936
+ userCode: codes.user_code,
937
+ verificationUri: codes.verification_uri,
938
+ verificationUriComplete: codes.verification_uri_complete,
939
+ expiresAt: new Date(Date.now() + codes.expires_in * 1000),
940
+ pollInterval: codes.interval,
941
+ status: 'pending'
942
+ });
943
+
944
+ // Start background polling
945
+ pollInBackground(flowId, auth);
946
+
947
+ res.json({
948
+ flowId,
949
+ userCode: codes.user_code,
950
+ verificationUri: codes.verification_uri,
951
+ verificationUriComplete: codes.verification_uri_complete,
952
+ expiresIn: codes.expires_in
953
+ });
954
+ } catch (error) {
955
+ res.status(500).json({ error: error.message });
956
+ }
957
+ });
958
+
959
+ /**
960
+ * GET /api/device-flow/:provider/status/:flowId
961
+ * Check status of device flow (client polls this)
962
+ */
963
+ router.get('/:provider/status/:flowId', async (req, res) => {
964
+ const { flowId } = req.params;
965
+ const flow = activeFlows.get(flowId);
966
+
967
+ if (!flow) {
968
+ return res.status(404).json({ error: 'Flow not found' });
969
+ }
970
+
971
+ if (flow.userId !== req.session.userId) {
972
+ return res.status(403).json({ error: 'Unauthorized' });
973
+ }
974
+
975
+ const timeLeft = Math.max(0, Math.floor(
976
+ (flow.expiresAt.getTime() - Date.now()) / 1000
977
+ ));
978
+
979
+ res.json({
980
+ status: flow.status,
981
+ expiresIn: timeLeft,
982
+ error: flow.error
983
+ });
984
+ });
985
+
986
+ /**
987
+ * DELETE /api/device-flow/:provider/:flowId
988
+ * Cancel device flow
989
+ */
990
+ router.delete('/:provider/:flowId', async (req, res) => {
991
+ const { flowId } = req.params;
992
+ const flow = activeFlows.get(flowId);
993
+
994
+ if (flow?.userId === req.session.userId) {
995
+ activeFlows.delete(flowId);
996
+ }
997
+
998
+ res.json({ success: true });
999
+ });
1000
+
1001
+ /**
1002
+ * Background polling for device authorization
1003
+ */
1004
+ async function pollInBackground(flowId: string, auth: DeviceFlowAuth) {
1005
+ const flow = activeFlows.get(flowId);
1006
+ if (!flow) return;
1007
+
1008
+ let interval = flow.pollInterval * 1000;
1009
+
1010
+ const poll = async () => {
1011
+ const current = activeFlows.get(flowId);
1012
+ if (!current || current.status !== 'pending') return;
1013
+
1014
+ // Check expiry
1015
+ if (Date.now() > current.expiresAt.getTime()) {
1016
+ current.status = 'expired';
1017
+ return;
1018
+ }
1019
+
1020
+ try {
1021
+ const result = await auth.pollForToken(current.deviceCode);
1022
+
1023
+ switch (result.status) {
1024
+ case 'pending':
1025
+ setTimeout(poll, interval);
1026
+ break;
1027
+
1028
+ case 'slow_down':
1029
+ interval = result.retryAfter * 1000;
1030
+ setTimeout(poll, interval);
1031
+ break;
1032
+
1033
+ case 'success':
1034
+ // Store tokens
1035
+ await storeProviderTokens(current.userId, current.provider, result.tokens);
1036
+ current.status = 'success';
1037
+ current.tokens = result.tokens;
1038
+ // Clean up after 60s
1039
+ setTimeout(() => activeFlows.delete(flowId), 60000);
1040
+ break;
1041
+
1042
+ case 'expired':
1043
+ case 'denied':
1044
+ current.status = result.status;
1045
+ break;
1046
+
1047
+ case 'error':
1048
+ current.status = 'error';
1049
+ current.error = result.error;
1050
+ break;
1051
+ }
1052
+ } catch (error) {
1053
+ console.error('Poll error:', error);
1054
+ // Retry with backoff
1055
+ setTimeout(poll, interval * 2);
1056
+ }
1057
+ };
1058
+
1059
+ // Start after initial interval
1060
+ setTimeout(poll, interval);
1061
+ }
1062
+
1063
+ /**
1064
+ * Store tokens after successful device flow
1065
+ */
1066
+ async function storeProviderTokens(
1067
+ userId: string,
1068
+ provider: string,
1069
+ tokens: TokenSet
1070
+ ) {
1071
+ // Get user info from provider
1072
+ const userInfo = await fetchProviderUserInfo(provider, tokens.accessToken);
1073
+
1074
+ await credentialVault.store({
1075
+ userId,
1076
+ provider,
1077
+ accessToken: tokens.accessToken,
1078
+ refreshToken: tokens.refreshToken,
1079
+ tokenExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),
1080
+ scopes: tokens.scope.split(' '),
1081
+ providerAccountId: userInfo.id,
1082
+ providerAccountEmail: userInfo.email,
1083
+ providerAccountName: userInfo.name,
1084
+ connectedAt: new Date(),
1085
+ isValid: true
1086
+ });
1087
+ }
1088
+ ```
1089
+
1090
+ ### Frontend Component
1091
+
1092
+ ```tsx
1093
+ // src/cloud/components/DeviceFlowAuth.tsx
1094
+
1095
+ import { useState, useEffect, useCallback } from 'react';
1096
+
1097
+ interface Props {
1098
+ provider: 'anthropic' | 'openai';
1099
+ providerName: string; // "Anthropic" or "OpenAI"
1100
+ onSuccess: () => void;
1101
+ onCancel: () => void;
1102
+ }
1103
+
1104
+ type State =
1105
+ | { step: 'ready' }
1106
+ | { step: 'loading' }
1107
+ | { step: 'showing_code'; flowId: string; userCode: string;
1108
+ verificationUri: string; expiresAt: Date }
1109
+ | { step: 'success' }
1110
+ | { step: 'error'; message: string; canRetry: boolean };
1111
+
1112
+ export function DeviceFlowAuth({ provider, providerName, onSuccess, onCancel }: Props) {
1113
+ const [state, setState] = useState<State>({ step: 'ready' });
1114
+ const [timeLeft, setTimeLeft] = useState(0);
1115
+
1116
+ // Start the device flow
1117
+ const startFlow = useCallback(async () => {
1118
+ setState({ step: 'loading' });
1119
+
1120
+ try {
1121
+ const res = await fetch(`/api/device-flow/${provider}/start`, {
1122
+ method: 'POST',
1123
+ credentials: 'include'
1124
+ });
1125
+ const data = await res.json();
1126
+
1127
+ if (!res.ok) throw new Error(data.error);
1128
+
1129
+ setState({
1130
+ step: 'showing_code',
1131
+ flowId: data.flowId,
1132
+ userCode: data.userCode,
1133
+ verificationUri: data.verificationUri,
1134
+ expiresAt: new Date(Date.now() + data.expiresIn * 1000)
1135
+ });
1136
+
1137
+ // Open provider auth page
1138
+ window.open(
1139
+ data.verificationUriComplete || data.verificationUri,
1140
+ '_blank',
1141
+ 'noopener'
1142
+ );
1143
+ } catch (error) {
1144
+ setState({
1145
+ step: 'error',
1146
+ message: error.message,
1147
+ canRetry: true
1148
+ });
1149
+ }
1150
+ }, [provider]);
1151
+
1152
+ // Poll for status
1153
+ useEffect(() => {
1154
+ if (state.step !== 'showing_code') return;
1155
+
1156
+ const checkStatus = async () => {
1157
+ try {
1158
+ const res = await fetch(
1159
+ `/api/device-flow/${provider}/status/${state.flowId}`,
1160
+ { credentials: 'include' }
1161
+ );
1162
+ const data = await res.json();
1163
+
1164
+ switch (data.status) {
1165
+ case 'success':
1166
+ setState({ step: 'success' });
1167
+ setTimeout(onSuccess, 1500);
1168
+ break;
1169
+ case 'expired':
1170
+ setState({
1171
+ step: 'error',
1172
+ message: 'Code expired. Please try again.',
1173
+ canRetry: true
1174
+ });
1175
+ break;
1176
+ case 'denied':
1177
+ setState({
1178
+ step: 'error',
1179
+ message: 'Authorization was denied.',
1180
+ canRetry: true
1181
+ });
1182
+ break;
1183
+ case 'error':
1184
+ setState({
1185
+ step: 'error',
1186
+ message: data.error || 'An error occurred.',
1187
+ canRetry: true
1188
+ });
1189
+ break;
1190
+ // 'pending' - keep polling
1191
+ }
1192
+ } catch (error) {
1193
+ console.error('Status check failed:', error);
1194
+ }
1195
+ };
1196
+
1197
+ const interval = setInterval(checkStatus, 2000);
1198
+ return () => clearInterval(interval);
1199
+ }, [state, provider, onSuccess]);
1200
+
1201
+ // Countdown timer
1202
+ useEffect(() => {
1203
+ if (state.step !== 'showing_code') return;
1204
+
1205
+ const tick = () => {
1206
+ const remaining = Math.floor(
1207
+ (state.expiresAt.getTime() - Date.now()) / 1000
1208
+ );
1209
+ setTimeLeft(Math.max(0, remaining));
1210
+ };
1211
+
1212
+ tick();
1213
+ const interval = setInterval(tick, 1000);
1214
+ return () => clearInterval(interval);
1215
+ }, [state]);
1216
+
1217
+ const formatTime = (s: number) =>
1218
+ `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, '0')}`;
1219
+
1220
+ const copyCode = () => {
1221
+ if (state.step === 'showing_code') {
1222
+ navigator.clipboard.writeText(state.userCode);
1223
+ }
1224
+ };
1225
+
1226
+ // Render
1227
+ switch (state.step) {
1228
+ case 'ready':
1229
+ return (
1230
+ <div className="device-flow">
1231
+ <h2>Connect {providerName}</h2>
1232
+ <p>
1233
+ Click below to sign in with your {providerName} account.
1234
+ You'll enter a code to link your account.
1235
+ </p>
1236
+ <div className="actions">
1237
+ <button onClick={startFlow} className="primary">
1238
+ Open {providerName} →
1239
+ </button>
1240
+ <button onClick={onCancel} className="secondary">
1241
+ Cancel
1242
+ </button>
1243
+ </div>
1244
+ </div>
1245
+ );
1246
+
1247
+ case 'loading':
1248
+ return (
1249
+ <div className="device-flow loading">
1250
+ <div className="spinner" />
1251
+ <p>Preparing authorization...</p>
1252
+ </div>
1253
+ );
1254
+
1255
+ case 'showing_code':
1256
+ return (
1257
+ <div className="device-flow">
1258
+ <h2>Enter this code at {providerName}</h2>
1259
+
1260
+ <div className="code-box">
1261
+ <code className="user-code">{state.userCode}</code>
1262
+ <button onClick={copyCode} className="copy-btn">
1263
+ Copy
1264
+ </button>
1265
+ </div>
1266
+
1267
+ <p className="hint">
1268
+ A browser tab opened to{' '}
1269
+ <a href={state.verificationUri} target="_blank" rel="noopener">
1270
+ {new URL(state.verificationUri).hostname}
1271
+ </a>
1272
+ </p>
1273
+
1274
+ <div className="status">
1275
+ <span className="spinner small" />
1276
+ <span>Waiting for authorization...</span>
1277
+ </div>
1278
+
1279
+ <div className="timer">
1280
+ Code expires in {formatTime(timeLeft)}
1281
+ </div>
1282
+
1283
+ <button onClick={onCancel} className="secondary">
1284
+ Cancel
1285
+ </button>
1286
+ </div>
1287
+ );
1288
+
1289
+ case 'success':
1290
+ return (
1291
+ <div className="device-flow success">
1292
+ <div className="icon">✅</div>
1293
+ <h2>Connected!</h2>
1294
+ <p>Your {providerName} account is now linked.</p>
1295
+ </div>
1296
+ );
1297
+
1298
+ case 'error':
1299
+ return (
1300
+ <div className="device-flow error">
1301
+ <div className="icon">❌</div>
1302
+ <h2>Connection Failed</h2>
1303
+ <p>{state.message}</p>
1304
+ <div className="actions">
1305
+ {state.canRetry && (
1306
+ <button onClick={startFlow} className="primary">
1307
+ Try Again
1308
+ </button>
1309
+ )}
1310
+ <button onClick={onCancel} className="secondary">
1311
+ Cancel
1312
+ </button>
1313
+ </div>
1314
+ </div>
1315
+ );
1316
+ }
1317
+ }
1318
+ ```
1319
+
1320
+ ---
1321
+
1322
+ #### Credential Import (Alternative for Claude)
1323
+
1324
+ For users who prefer to authenticate locally first:
1325
+
1326
+ ```
1327
+ ┌─────────────────────────────────────────────────────────────────┐
1328
+ │ Connect Claude Code │
1329
+ │ │
1330
+ │ Choose how to connect: │
1331
+ │ │
1332
+ │ ┌─────────────────────────────────────────────────────────────┐│
1333
+ │ │ 🔐 Login with Anthropic ││
1334
+ │ │ Authenticate in browser (recommended) ││
1335
+ │ └─────────────────────────────────────────────────────────────┘│
1336
+ │ │
1337
+ │ ┌─────────────────────────────────────────────────────────────┐│
1338
+ │ │ 📁 Import from Local Claude ││
1339
+ │ │ Already have Claude Code installed? Import credentials ││
1340
+ │ └─────────────────────────────────────────────────────────────┘│
1341
+ │ │
1342
+ └─────────────────────────────────────────────────────────────────┘
1343
+ ```
1344
+
1345
+ Import flow:
1346
+
1347
+ ```
1348
+ ┌─────────────────────────────────────────────────────────────────┐
1349
+ │ Import Claude Credentials │
1350
+ │ │
1351
+ │ Run this command on your local machine: │
1352
+ │ │
1353
+ │ ┌─────────────────────────────────────────────────────────────┐│
1354
+ │ │ npx agent-relay-cloud export-credentials ││
1355
+ │ └─────────────────────────────────────────────────────────────┘│
1356
+ │ [Copy] │
1357
+ │ │
1358
+ │ This will: │
1359
+ │ • Read your Claude credentials from ~/.claude/ │
1360
+ │ • Encrypt them with a one-time code │
1361
+ │ • Upload securely to Agent Relay Cloud │
1362
+ │ │
1363
+ │ Your credentials never leave your machine unencrypted. │
1364
+ │ │
1365
+ │ ⏳ Waiting for import... │
1366
+ │ │
1367
+ └─────────────────────────────────────────────────────────────────┘
1368
+ ```
1369
+
1370
+ ### Step 4: Configure Your First Team (Optional)
1371
+
1372
+ ```
1373
+ ┌─────────────────────────────────────────────────────────────────┐
1374
+ │ Create Your First Agent Team │
1375
+ │ │
1376
+ │ Teams are groups of AI agents that work together on tasks. │
1377
+ │ │
1378
+ │ ┌──────────────────────────────────────────────────────────┐ │
1379
+ │ │ 🚀 Quick Start Templates │ │
1380
+ │ ├──────────────────────────────────────────────────────────┤ │
1381
+ │ │ │ │
1382
+ │ │ ┌────────────────────────────────────────────────────┐ │ │
1383
+ │ │ │ 👥 Code Review Team │ │ │
1384
+ │ │ │ Architect + Reviewer + Security Auditor │ │ │
1385
+ │ │ │ Auto-reviews PRs and suggests improvements │ │ │
1386
+ │ │ └────────────────────────────────────────────────────┘ │ │
1387
+ │ │ │ │
1388
+ │ │ ┌────────────────────────────────────────────────────┐ │ │
1389
+ │ │ │ 🛠️ Feature Development Team │ │ │
1390
+ │ │ │ Lead + Frontend + Backend + Tester │ │ │
1391
+ │ │ │ Coordinates multi-agent feature builds │ │ │
1392
+ │ │ └────────────────────────────────────────────────────┘ │ │
1393
+ │ │ │ │
1394
+ │ │ ┌────────────────────────────────────────────────────┐ │ │
1395
+ │ │ │ 📝 Custom Team │ │ │
1396
+ │ │ │ Configure your own agent composition │ │ │
1397
+ │ │ └────────────────────────────────────────────────────┘ │ │
1398
+ │ │ │ │
1399
+ │ └──────────────────────────────────────────────────────────┘ │
1400
+ │ │
1401
+ │ ┌──────────────┐ ┌──────────────────────────────────────────┐ │
1402
+ │ │ Skip for now │ │ Select template → │ │
1403
+ │ └──────────────┘ └──────────────────────────────────────────┘ │
1404
+ └─────────────────────────────────────────────────────────────────┘
1405
+ ```
1406
+
1407
+ ### Step 5: Ready to Go!
1408
+
1409
+ ```
1410
+ ┌─────────────────────────────────────────────────────────────────┐
1411
+ │ 🎉 You're all set! │
1412
+ │ │
1413
+ │ Your Agent Relay Cloud workspace is ready: │
1414
+ │ │
1415
+ │ ┌──────────────────────────────────────────────────────────┐ │
1416
+ │ │ 📂 Repositories 2 connected │ │
1417
+ │ │ 🤖 Agent Providers Claude, Codex │ │
1418
+ │ │ 👥 Teams Code Review Team │ │
1419
+ │ │ 🌐 Dashboard relay.yourdomain.cloud │ │
1420
+ │ └──────────────────────────────────────────────────────────┘ │
1421
+ │ │
1422
+ │ What's next? │
1423
+ │ │
1424
+ │ • Open a PR to trigger automatic code review │
1425
+ │ • Use @agent-relay in PR comments to chat with agents │
1426
+ │ • Visit your dashboard to monitor agent activity │
1427
+ │ │
1428
+ │ ┌──────────────────────────────────────────────────────────┐ │
1429
+ │ │ Open Dashboard → │ │
1430
+ │ └──────────────────────────────────────────────────────────┘ │
1431
+ └─────────────────────────────────────────────────────────────────┘
1432
+ ```
1433
+
1434
+ ---
1435
+
1436
+ ## Technical Implementation
1437
+
1438
+ ### Provider Credentials Storage
1439
+
1440
+ ```typescript
1441
+ // src/cloud/providers/types.ts
1442
+
1443
+ interface ProviderCredential {
1444
+ id: string;
1445
+ userId: string;
1446
+ provider: ProviderType;
1447
+
1448
+ // OAuth tokens (encrypted at rest)
1449
+ accessToken: string;
1450
+ refreshToken: string;
1451
+ tokenExpiresAt: Date;
1452
+ scopes: string[];
1453
+
1454
+ // Account info from provider
1455
+ providerAccountId: string; // Provider's user ID
1456
+ providerAccountEmail: string; // For display: "user@example.com"
1457
+ providerAccountName?: string; // Display name if available
1458
+
1459
+ // Metadata
1460
+ connectedAt: Date;
1461
+ lastUsedAt?: Date;
1462
+ lastRefreshedAt?: Date;
1463
+ isValid: boolean;
1464
+ }
1465
+
1466
+ type ProviderType =
1467
+ | 'anthropic' // Claude Code
1468
+ | 'openai' // Codex, ChatGPT
1469
+ | 'google' // Gemini
1470
+ | 'github' // Copilot (auto-connected via signup)
1471
+ | 'microsoft' // Azure OpenAI
1472
+ | 'self-hosted'; // Ollama, LM Studio (no auth needed)
1473
+ ```
1474
+
1475
+ ### Provider Registry
1476
+
1477
+ ```typescript
1478
+ // src/cloud/providers/registry.ts
1479
+
1480
+ interface OAuthConfig {
1481
+ authorizationUrl: string;
1482
+ tokenUrl: string;
1483
+ scopes: string[];
1484
+ userInfoUrl?: string; // To fetch account email/name after auth
1485
+ }
1486
+
1487
+ interface ProviderConfig {
1488
+ id: ProviderType;
1489
+ name: string;
1490
+ displayName: string; // "Login with {displayName}"
1491
+ description: string;
1492
+ cliCommand: string;
1493
+ cliArgs?: string[];
1494
+ oauthConfig: OAuthConfig;
1495
+ icon: string;
1496
+ color: string; // Brand color for button
1497
+ }
1498
+
1499
+ const PROVIDER_REGISTRY: ProviderConfig[] = [
1500
+ {
1501
+ id: 'anthropic',
1502
+ name: 'Anthropic',
1503
+ displayName: 'Anthropic',
1504
+ description: 'Claude Code - recommended for code tasks',
1505
+ cliCommand: 'claude',
1506
+ cliArgs: ['--dangerously-skip-permissions'],
1507
+ oauthConfig: {
1508
+ authorizationUrl: 'https://console.anthropic.com/oauth/authorize',
1509
+ tokenUrl: 'https://api.anthropic.com/oauth/token',
1510
+ scopes: ['claude-code:execute', 'user:read'],
1511
+ userInfoUrl: 'https://api.anthropic.com/v1/user'
1512
+ },
1513
+ icon: 'anthropic-logo.svg',
1514
+ color: '#D97757'
1515
+ },
1516
+ {
1517
+ id: 'openai',
1518
+ name: 'OpenAI',
1519
+ displayName: 'OpenAI',
1520
+ description: 'Codex and ChatGPT models',
1521
+ cliCommand: 'codex',
1522
+ cliArgs: ['--dangerously-bypass-approvals-and-sandbox'],
1523
+ oauthConfig: {
1524
+ authorizationUrl: 'https://auth.openai.com/authorize',
1525
+ tokenUrl: 'https://auth.openai.com/oauth/token',
1526
+ scopes: ['openid', 'profile', 'email', 'model.read', 'model.request'],
1527
+ userInfoUrl: 'https://api.openai.com/v1/user'
1528
+ },
1529
+ icon: 'openai-logo.svg',
1530
+ color: '#10A37F'
1531
+ },
1532
+ {
1533
+ id: 'google',
1534
+ name: 'Google',
1535
+ displayName: 'Google',
1536
+ description: 'Gemini - multi-modal capabilities',
1537
+ cliCommand: 'gemini',
1538
+ oauthConfig: {
1539
+ authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
1540
+ tokenUrl: 'https://oauth2.googleapis.com/token',
1541
+ scopes: [
1542
+ 'openid',
1543
+ 'email',
1544
+ 'profile',
1545
+ 'https://www.googleapis.com/auth/generative-language'
1546
+ ],
1547
+ userInfoUrl: 'https://www.googleapis.com/oauth2/v2/userinfo'
1548
+ },
1549
+ icon: 'google-logo.svg',
1550
+ color: '#4285F4'
1551
+ },
1552
+ {
1553
+ id: 'github',
1554
+ name: 'GitHub',
1555
+ displayName: 'GitHub',
1556
+ description: 'Copilot - auto-connected via signup',
1557
+ cliCommand: 'gh-copilot',
1558
+ oauthConfig: {
1559
+ // Uses same OAuth from signup - just needs Copilot scope
1560
+ authorizationUrl: 'https://github.com/login/oauth/authorize',
1561
+ tokenUrl: 'https://github.com/login/oauth/access_token',
1562
+ scopes: ['copilot', 'read:user', 'user:email'],
1563
+ userInfoUrl: 'https://api.github.com/user'
1564
+ },
1565
+ icon: 'github-logo.svg',
1566
+ color: '#24292F'
1567
+ },
1568
+ {
1569
+ id: 'microsoft',
1570
+ name: 'Microsoft',
1571
+ displayName: 'Microsoft',
1572
+ description: 'Azure OpenAI - enterprise deployments',
1573
+ cliCommand: 'azure-openai',
1574
+ oauthConfig: {
1575
+ authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
1576
+ tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
1577
+ scopes: [
1578
+ 'openid',
1579
+ 'profile',
1580
+ 'email',
1581
+ 'https://cognitiveservices.azure.com/.default'
1582
+ ],
1583
+ userInfoUrl: 'https://graph.microsoft.com/v1.0/me'
1584
+ },
1585
+ icon: 'microsoft-logo.svg',
1586
+ color: '#00A4EF'
1587
+ }
1588
+ ];
1589
+
1590
+ // Self-hosted providers don't need OAuth
1591
+ interface SelfHostedProvider {
1592
+ id: 'self-hosted';
1593
+ name: string;
1594
+ endpoint: string; // e.g., "http://localhost:11434" for Ollama
1595
+ cliCommand: string;
1596
+ }
1597
+ ```
1598
+
1599
+ ### Spawner Integration
1600
+
1601
+ ```typescript
1602
+ // src/cloud/spawner-cloud.ts
1603
+
1604
+ class CloudAgentSpawner extends AgentSpawner {
1605
+ private credentialVault: CredentialVault;
1606
+ private tokenRefresher: TokenRefresher;
1607
+
1608
+ async spawn(request: CloudSpawnRequest): Promise<SpawnedAgent> {
1609
+ const { userId, provider, agentName, task } = request;
1610
+
1611
+ // Get credentials for this provider
1612
+ const credential = await this.credentialVault.get(userId, provider);
1613
+ if (!credential) {
1614
+ throw new ProviderNotConnectedError(provider);
1615
+ }
1616
+
1617
+ // Refresh token if expired or expiring soon (within 5 min)
1618
+ const validCredential = await this.ensureValidToken(credential);
1619
+
1620
+ // Build environment with OAuth token
1621
+ const env = this.buildProviderEnv(validCredential);
1622
+
1623
+ // Get provider config
1624
+ const providerConfig = PROVIDER_REGISTRY.find(p => p.id === provider);
1625
+
1626
+ // Spawn agent with credentials injected
1627
+ return super.spawn({
1628
+ name: agentName,
1629
+ cli: providerConfig.cliCommand,
1630
+ args: providerConfig.cliArgs,
1631
+ env,
1632
+ task
1633
+ });
1634
+ }
1635
+
1636
+ private async ensureValidToken(credential: ProviderCredential): Promise<ProviderCredential> {
1637
+ const expiresIn = credential.tokenExpiresAt.getTime() - Date.now();
1638
+ const FIVE_MINUTES = 5 * 60 * 1000;
1639
+
1640
+ if (expiresIn < FIVE_MINUTES) {
1641
+ // Refresh the token
1642
+ const provider = PROVIDER_REGISTRY.find(p => p.id === credential.provider);
1643
+ const newTokens = await this.tokenRefresher.refresh(
1644
+ provider.oauthConfig,
1645
+ credential.refreshToken
1646
+ );
1647
+
1648
+ // Update stored credential
1649
+ const updated = await this.credentialVault.update(credential.id, {
1650
+ accessToken: newTokens.accessToken,
1651
+ refreshToken: newTokens.refreshToken ?? credential.refreshToken,
1652
+ tokenExpiresAt: newTokens.expiresAt,
1653
+ lastRefreshedAt: new Date()
1654
+ });
1655
+
1656
+ return updated;
1657
+ }
1658
+
1659
+ return credential;
1660
+ }
1661
+
1662
+ private buildProviderEnv(credential: ProviderCredential): Record<string, string> {
1663
+ // Each provider expects OAuth token in different env vars
1664
+ const envMapping: Record<ProviderType, string> = {
1665
+ 'anthropic': 'ANTHROPIC_AUTH_TOKEN',
1666
+ 'openai': 'OPENAI_AUTH_TOKEN',
1667
+ 'google': 'GOOGLE_AUTH_TOKEN',
1668
+ 'github': 'GITHUB_TOKEN',
1669
+ 'microsoft': 'AZURE_AUTH_TOKEN',
1670
+ 'self-hosted': '' // No auth needed
1671
+ };
1672
+
1673
+ const envVar = envMapping[credential.provider];
1674
+ if (!envVar) return {};
1675
+
1676
+ return {
1677
+ [envVar]: credential.accessToken,
1678
+ // Some CLIs also want the account info
1679
+ 'PROVIDER_ACCOUNT_EMAIL': credential.providerAccountEmail
1680
+ };
1681
+ }
1682
+ }
1683
+
1684
+ class ProviderNotConnectedError extends Error {
1685
+ constructor(provider: ProviderType) {
1686
+ super(`Provider "${provider}" is not connected. Please connect it in Settings.`);
1687
+ this.name = 'ProviderNotConnectedError';
1688
+ }
1689
+ }
1690
+ ```
1691
+
1692
+ ### Onboarding API
1693
+
1694
+ ```typescript
1695
+ // src/cloud/api/onboarding.ts
1696
+
1697
+ const onboardingRouter = Router();
1698
+
1699
+ // Step 1: GitHub OAuth callback (primary signup/login)
1700
+ onboardingRouter.get('/auth/github/callback', async (req, res) => {
1701
+ const { code } = req.query;
1702
+ const tokens = await exchangeGitHubCode(code);
1703
+ const user = await createOrUpdateUser(tokens);
1704
+
1705
+ // Store GitHub credential (also gives us Copilot access)
1706
+ await credentialVault.store({
1707
+ userId: user.id,
1708
+ provider: 'github',
1709
+ accessToken: tokens.accessToken,
1710
+ refreshToken: tokens.refreshToken,
1711
+ tokenExpiresAt: tokens.expiresAt,
1712
+ scopes: tokens.scopes,
1713
+ providerAccountId: tokens.user.id,
1714
+ providerAccountEmail: tokens.user.email,
1715
+ providerAccountName: tokens.user.name,
1716
+ connectedAt: new Date(),
1717
+ isValid: true
1718
+ });
1719
+
1720
+ // Set session and redirect to repo selection
1721
+ req.session.userId = user.id;
1722
+ res.redirect('/onboarding/repositories');
1723
+ });
1724
+
1725
+ // Step 2: Get user's repositories
1726
+ onboardingRouter.get('/repositories', async (req, res) => {
1727
+ const repos = await github.listUserRepos(req.session.accessToken);
1728
+ res.json({ repos });
1729
+ });
1730
+
1731
+ // Step 2: Connect selected repositories
1732
+ onboardingRouter.post('/repositories', async (req, res) => {
1733
+ const { repoIds } = req.body;
1734
+ await Promise.all(repoIds.map(id =>
1735
+ connectRepository(req.session.userId, id)
1736
+ ));
1737
+ res.json({ success: true });
1738
+ });
1739
+
1740
+ // Step 3: List available providers with connection status
1741
+ onboardingRouter.get('/providers', async (req, res) => {
1742
+ const connected = await getConnectedProviders(req.session.userId);
1743
+
1744
+ // Map registry with connection status
1745
+ const providers = PROVIDER_REGISTRY.map(p => ({
1746
+ ...p,
1747
+ isConnected: connected.some(c => c.provider === p.id),
1748
+ connectedAs: connected.find(c => c.provider === p.id)?.providerAccountEmail
1749
+ }));
1750
+
1751
+ res.json({ providers });
1752
+ });
1753
+
1754
+ // Step 3: Initiate OAuth login for a provider
1755
+ onboardingRouter.get('/providers/:provider/login', async (req, res) => {
1756
+ const { provider } = req.params;
1757
+ const config = PROVIDER_REGISTRY.find(p => p.id === provider);
1758
+
1759
+ if (!config) {
1760
+ return res.status(404).json({ error: 'Unknown provider' });
1761
+ }
1762
+
1763
+ // Generate state token for CSRF protection
1764
+ const state = await generateOAuthState({
1765
+ userId: req.session.userId,
1766
+ provider,
1767
+ returnTo: req.query.returnTo || '/onboarding/providers'
1768
+ });
1769
+
1770
+ // Build OAuth authorization URL
1771
+ const authUrl = new URL(config.oauthConfig.authorizationUrl);
1772
+ authUrl.searchParams.set('client_id', getClientId(provider));
1773
+ authUrl.searchParams.set('redirect_uri', `${BASE_URL}/providers/${provider}/callback`);
1774
+ authUrl.searchParams.set('scope', config.oauthConfig.scopes.join(' '));
1775
+ authUrl.searchParams.set('state', state);
1776
+ authUrl.searchParams.set('response_type', 'code');
1777
+
1778
+ res.redirect(authUrl.toString());
1779
+ });
1780
+
1781
+ // Step 3: OAuth callback after user authorizes
1782
+ onboardingRouter.get('/providers/:provider/callback', async (req, res) => {
1783
+ const { code, state, error } = req.query;
1784
+
1785
+ if (error) {
1786
+ return res.redirect(`/onboarding/providers?error=${error}`);
1787
+ }
1788
+
1789
+ // Verify state token
1790
+ const stateData = await verifyOAuthState(state);
1791
+ if (!stateData) {
1792
+ return res.status(400).json({ error: 'Invalid state' });
1793
+ }
1794
+
1795
+ const { userId, provider, returnTo } = stateData;
1796
+ const config = PROVIDER_REGISTRY.find(p => p.id === provider);
1797
+
1798
+ // Exchange code for tokens
1799
+ const tokens = await exchangeOAuthCode({
1800
+ tokenUrl: config.oauthConfig.tokenUrl,
1801
+ code,
1802
+ clientId: getClientId(provider),
1803
+ clientSecret: getClientSecret(provider),
1804
+ redirectUri: `${BASE_URL}/providers/${provider}/callback`
1805
+ });
1806
+
1807
+ // Fetch user info from provider
1808
+ const userInfo = await fetchProviderUserInfo(
1809
+ config.oauthConfig.userInfoUrl,
1810
+ tokens.accessToken
1811
+ );
1812
+
1813
+ // Store credential
1814
+ await credentialVault.store({
1815
+ userId,
1816
+ provider,
1817
+ accessToken: tokens.accessToken,
1818
+ refreshToken: tokens.refreshToken,
1819
+ tokenExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),
1820
+ scopes: tokens.scope.split(' '),
1821
+ providerAccountId: userInfo.id,
1822
+ providerAccountEmail: userInfo.email,
1823
+ providerAccountName: userInfo.name,
1824
+ connectedAt: new Date(),
1825
+ isValid: true
1826
+ });
1827
+
1828
+ res.redirect(`${returnTo}?connected=${provider}`);
1829
+ });
1830
+
1831
+ // Step 3: Disconnect a provider
1832
+ onboardingRouter.delete('/providers/:provider', async (req, res) => {
1833
+ const { provider } = req.params;
1834
+
1835
+ await credentialVault.delete(req.session.userId, provider);
1836
+
1837
+ res.json({ success: true });
1838
+ });
1839
+
1840
+ // Step 4: Create team from template
1841
+ onboardingRouter.post('/teams/from-template', async (req, res) => {
1842
+ const { templateId, repoIds, defaultProvider } = req.body;
1843
+ const template = TEAM_TEMPLATES[templateId];
1844
+
1845
+ // Verify user has the default provider connected
1846
+ const hasProvider = await credentialVault.exists(
1847
+ req.session.userId,
1848
+ defaultProvider || 'anthropic'
1849
+ );
1850
+
1851
+ if (!hasProvider) {
1852
+ return res.status(400).json({
1853
+ error: 'Provider not connected',
1854
+ message: `Please connect ${defaultProvider || 'Anthropic'} first`
1855
+ });
1856
+ }
1857
+
1858
+ const team = await createTeam({
1859
+ userId: req.session.userId,
1860
+ name: template.name,
1861
+ agents: template.agents.map(a => ({
1862
+ ...a,
1863
+ provider: defaultProvider || 'anthropic'
1864
+ })),
1865
+ repoIds
1866
+ });
1867
+
1868
+ res.json({ team });
1869
+ });
1870
+
1871
+ // Complete onboarding
1872
+ onboardingRouter.post('/complete', async (req, res) => {
1873
+ await markOnboardingComplete(req.session.userId);
1874
+
1875
+ // Provision workspace
1876
+ const workspace = await provisionWorkspace(req.session.userId);
1877
+
1878
+ res.json({
1879
+ dashboardUrl: workspace.dashboardUrl,
1880
+ webhookUrl: workspace.webhookUrl
1881
+ });
1882
+ });
1883
+ ```
1884
+
1885
+ ---
1886
+
1887
+ ## Security Considerations
1888
+
1889
+ ### OAuth Token Security
1890
+
1891
+ 1. **Encryption at rest**: All tokens encrypted with AES-256-GCM
1892
+ 2. **Key derivation**: Per-user encryption keys derived from master key + user ID
1893
+ 3. **No plaintext logging**: Tokens never logged, even in debug mode
1894
+ 4. **Short-lived access tokens**: Rely on refresh tokens for long sessions
1895
+
1896
+ ### Token Lifecycle Management
1897
+
1898
+ 1. **Automatic refresh**: Tokens refreshed 5 minutes before expiry
1899
+ 2. **Refresh token rotation**: Use rotating refresh tokens where supported
1900
+ 3. **Revocation detection**: Check token validity on spawn, prompt re-auth if revoked
1901
+ 4. **Graceful degradation**: Queue tasks if token refresh fails temporarily
1902
+
1903
+ ### Scope Management
1904
+
1905
+ 1. **Minimum scopes**: Request only scopes needed for agent execution
1906
+ 2. **Scope display**: Show users exactly what permissions we request
1907
+ 3. **No scope creep**: Never silently request additional scopes
1908
+
1909
+ ### Access Control
1910
+
1911
+ 1. **User isolation**: Credentials tied to user ID, never shared
1912
+ 2. **Team permissions**: Team admins can enable provider access for team members
1913
+ 3. **Audit logging**: All credential access and agent spawns logged
1914
+ 4. **Rate limiting**: Provider usage rate-limited per user/team
1915
+
1916
+ ---
1917
+
1918
+ ## Future Enhancements
1919
+
1920
+ ### 1. Credential Sharing for Teams
1921
+
1922
+ Allow team admins to share provider credentials with team members:
1923
+
1924
+ ```typescript
1925
+ interface SharedCredential {
1926
+ credentialId: string;
1927
+ teamId: string;
1928
+ sharedBy: string;
1929
+ permissions: 'read' | 'use'; // 'use' allows spawning agents
1930
+ }
1931
+ ```
1932
+
1933
+ ### 2. Usage Tracking & Billing
1934
+
1935
+ Track provider usage per user/team for billing:
1936
+
1937
+ ```typescript
1938
+ interface UsageRecord {
1939
+ userId: string;
1940
+ teamId?: string;
1941
+ provider: ProviderType;
1942
+ agentName: string;
1943
+ tokensUsed: number;
1944
+ duration: number;
1945
+ timestamp: Date;
1946
+ }
1947
+ ```
1948
+
1949
+ ### 3. Provider Health Monitoring
1950
+
1951
+ Monitor provider availability and quota:
1952
+
1953
+ ```typescript
1954
+ interface ProviderHealth {
1955
+ provider: ProviderType;
1956
+ status: 'healthy' | 'degraded' | 'down';
1957
+ quotaRemaining?: number;
1958
+ lastChecked: Date;
1959
+ }
1960
+ ```
1961
+
1962
+ ### 4. Bring Your Own Cloud
1963
+
1964
+ Let users connect their own cloud accounts for compute:
1965
+
1966
+ - AWS credentials for EC2 instances
1967
+ - GCP credentials for Cloud Run
1968
+ - Azure credentials for Container Instances
1969
+
1970
+ ---
1971
+
1972
+ ## Summary
1973
+
1974
+ The onboarding flow prioritizes:
1975
+
1976
+ 1. **Low friction**: GitHub OAuth gets users started immediately
1977
+ 2. **Consistent UX**: All providers use "Login with X" - no API keys to manage
1978
+ 3. **Security**: OAuth tokens with automatic refresh, encrypted at rest
1979
+ 4. **Account linking**: Users log in with their existing provider accounts
1980
+ 5. **Progressive disclosure**: Optional team setup, can skip and add later
1981
+ 6. **Graceful recovery**: Re-auth prompts when tokens expire or get revoked
1982
+
1983
+ Users can connect all their AI providers during onboarding with simple login buttons, or add them later from Settings. GitHub Copilot is auto-connected via the initial signup flow.