agent-relay 1.1.0 → 1.2.3

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 (567) hide show
  1. package/.gitattributes +3 -0
  2. package/.nvmrc +1 -0
  3. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.json +65 -0
  4. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.md +37 -0
  5. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.json +65 -0
  6. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.md +37 -0
  7. package/.trajectories/completed/2026-01/traj_1rp3rges5811.json +49 -0
  8. package/.trajectories/completed/2026-01/traj_1rp3rges5811.md +31 -0
  9. package/.trajectories/completed/2026-01/traj_22bhyulruouw.json +113 -0
  10. package/.trajectories/completed/2026-01/traj_22bhyulruouw.md +57 -0
  11. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.json +53 -0
  12. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.md +32 -0
  13. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.json +26 -0
  14. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.md +6 -0
  15. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.json +47 -0
  16. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.md +32 -0
  17. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.json +53 -0
  18. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.md +32 -0
  19. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.json +59 -0
  20. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.md +33 -0
  21. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.json +53 -0
  22. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.md +32 -0
  23. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.json +48 -0
  24. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.md +24 -0
  25. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.json +209 -0
  26. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.md +97 -0
  27. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.json +48 -0
  28. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.md +24 -0
  29. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.json +49 -0
  30. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.md +23 -0
  31. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.json +53 -0
  32. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.md +32 -0
  33. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.json +49 -0
  34. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.md +31 -0
  35. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.json +26 -0
  36. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.md +6 -0
  37. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.json +59 -0
  38. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.md +37 -0
  39. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.json +77 -0
  40. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.md +42 -0
  41. package/.trajectories/completed/2026-01/traj_gjdre5voouod.json +53 -0
  42. package/.trajectories/completed/2026-01/traj_gjdre5voouod.md +32 -0
  43. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.json +25 -0
  44. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.md +15 -0
  45. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.json +101 -0
  46. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.md +44 -0
  47. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json +22 -0
  48. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md +5 -0
  49. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.json +53 -0
  50. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.md +32 -0
  51. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.json +25 -0
  52. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.md +15 -0
  53. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.json +53 -0
  54. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.md +32 -0
  55. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.json +53 -0
  56. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.md +32 -0
  57. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.json +48 -0
  58. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.md +24 -0
  59. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.json +53 -0
  60. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.md +32 -0
  61. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.json +77 -0
  62. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.md +42 -0
  63. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.json +53 -0
  64. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.md +32 -0
  65. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.json +83 -0
  66. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.md +47 -0
  67. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.json +59 -0
  68. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.md +37 -0
  69. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.json +48 -0
  70. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.md +16 -0
  71. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.json +59 -0
  72. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.md +37 -0
  73. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.json +53 -0
  74. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.md +32 -0
  75. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.json +84 -0
  76. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.md +109 -0
  77. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.json +53 -0
  78. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.md +32 -0
  79. package/.trajectories/completed/2026-01/traj_v87hypnongqx.json +71 -0
  80. package/.trajectories/completed/2026-01/traj_v87hypnongqx.md +42 -0
  81. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.json +53 -0
  82. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.md +32 -0
  83. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.json +20 -0
  84. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.md +6 -0
  85. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.json +175 -0
  86. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.md +82 -0
  87. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.json +47 -0
  88. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.md +32 -0
  89. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.json +59 -0
  90. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.md +37 -0
  91. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.json +53 -0
  92. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.md +32 -0
  93. package/.trajectories/index.json +314 -0
  94. package/ARCHITECTURE.md +1245 -0
  95. package/README.md +1 -1
  96. package/TESTING.md +278 -0
  97. package/deploy/init-db.sql +5 -0
  98. package/deploy/scripts/setup-fly-workspaces.sh +69 -0
  99. package/deploy/scripts/setup-railway.sh +75 -0
  100. package/deploy/workspace/entrypoint-browser.sh +118 -0
  101. package/deploy/workspace/entrypoint.sh +348 -0
  102. package/deploy/workspace/git-credential-relay +111 -0
  103. package/dist/bridge/spawner.d.ts +53 -0
  104. package/dist/bridge/spawner.js +203 -19
  105. package/dist/bridge/types.d.ts +12 -0
  106. package/dist/cli/index.js +618 -5
  107. package/dist/cloud/api/auth.d.ts +3 -2
  108. package/dist/cloud/api/auth.js +10 -98
  109. package/dist/cloud/api/billing.js +30 -9
  110. package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
  111. package/dist/cloud/api/cli-pty-runner.js +119 -0
  112. package/dist/cloud/api/codex-auth-helper.d.ts +15 -0
  113. package/dist/cloud/api/codex-auth-helper.js +100 -0
  114. package/dist/cloud/api/generic-webhooks.d.ts +8 -0
  115. package/dist/cloud/api/generic-webhooks.js +129 -0
  116. package/dist/cloud/api/git.d.ts +8 -0
  117. package/dist/cloud/api/git.js +152 -0
  118. package/dist/cloud/api/github-app.d.ts +11 -0
  119. package/dist/cloud/api/github-app.js +189 -0
  120. package/dist/cloud/api/middleware/planLimits.d.ts +7 -0
  121. package/dist/cloud/api/middleware/planLimits.js +39 -1
  122. package/dist/cloud/api/monitoring.d.ts +11 -0
  123. package/dist/cloud/api/monitoring.js +578 -0
  124. package/dist/cloud/api/nango-auth.d.ts +9 -0
  125. package/dist/cloud/api/nango-auth.js +377 -0
  126. package/dist/cloud/api/onboarding.d.ts +8 -1
  127. package/dist/cloud/api/onboarding.js +313 -119
  128. package/dist/cloud/api/policy.d.ts +8 -0
  129. package/dist/cloud/api/policy.js +229 -0
  130. package/dist/cloud/api/providers.js +114 -42
  131. package/dist/cloud/api/repos.d.ts +1 -0
  132. package/dist/cloud/api/repos.js +186 -0
  133. package/dist/cloud/api/test-helpers.d.ts +10 -0
  134. package/dist/cloud/api/test-helpers.js +575 -0
  135. package/dist/cloud/api/webhooks.d.ts +8 -0
  136. package/dist/cloud/api/webhooks.js +645 -0
  137. package/dist/cloud/api/workspaces.js +320 -12
  138. package/dist/cloud/billing/plans.js +32 -19
  139. package/dist/cloud/billing/types.d.ts +9 -3
  140. package/dist/cloud/config.d.ts +9 -2
  141. package/dist/cloud/config.js +13 -4
  142. package/dist/cloud/db/drizzle.d.ts +84 -1
  143. package/dist/cloud/db/drizzle.js +470 -0
  144. package/dist/cloud/db/index.d.ts +9 -4
  145. package/dist/cloud/db/index.js +11 -3
  146. package/dist/cloud/db/schema.d.ts +3283 -556
  147. package/dist/cloud/db/schema.js +314 -1
  148. package/dist/cloud/index.d.ts +1 -0
  149. package/dist/cloud/index.js +2 -0
  150. package/dist/cloud/provisioner/index.d.ts +56 -0
  151. package/dist/cloud/provisioner/index.js +676 -34
  152. package/dist/cloud/server.d.ts +1 -0
  153. package/dist/cloud/server.js +362 -13
  154. package/dist/cloud/services/auto-scaler.d.ts +152 -0
  155. package/dist/cloud/services/auto-scaler.js +439 -0
  156. package/dist/cloud/services/capacity-manager.d.ts +148 -0
  157. package/dist/cloud/services/capacity-manager.js +449 -0
  158. package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
  159. package/dist/cloud/services/ci-agent-spawner.js +373 -0
  160. package/dist/cloud/services/index.d.ts +12 -0
  161. package/dist/cloud/services/index.js +15 -0
  162. package/dist/cloud/services/mention-handler.d.ts +65 -0
  163. package/dist/cloud/services/mention-handler.js +405 -0
  164. package/dist/cloud/services/nango.d.ts +186 -0
  165. package/dist/cloud/services/nango.js +344 -0
  166. package/dist/cloud/services/persistence.d.ts +131 -0
  167. package/dist/cloud/services/persistence.js +200 -0
  168. package/dist/cloud/services/planLimits.d.ts +37 -0
  169. package/dist/cloud/services/planLimits.js +86 -5
  170. package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
  171. package/dist/cloud/services/scaling-orchestrator.js +502 -0
  172. package/dist/cloud/services/scaling-policy.d.ts +121 -0
  173. package/dist/cloud/services/scaling-policy.js +415 -0
  174. package/dist/cloud/vault/index.js +1 -1
  175. package/dist/cloud/webhooks/index.d.ts +24 -0
  176. package/dist/cloud/webhooks/index.js +29 -0
  177. package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
  178. package/dist/cloud/webhooks/parsers/github.js +234 -0
  179. package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
  180. package/dist/cloud/webhooks/parsers/index.js +30 -0
  181. package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
  182. package/dist/cloud/webhooks/parsers/linear.js +258 -0
  183. package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
  184. package/dist/cloud/webhooks/parsers/slack.js +214 -0
  185. package/dist/cloud/webhooks/responders/github.d.ts +8 -0
  186. package/dist/cloud/webhooks/responders/github.js +73 -0
  187. package/dist/cloud/webhooks/responders/index.d.ts +23 -0
  188. package/dist/cloud/webhooks/responders/index.js +30 -0
  189. package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
  190. package/dist/cloud/webhooks/responders/linear.js +149 -0
  191. package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
  192. package/dist/cloud/webhooks/responders/slack.js +178 -0
  193. package/dist/cloud/webhooks/router.d.ts +25 -0
  194. package/dist/cloud/webhooks/router.js +504 -0
  195. package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
  196. package/dist/cloud/webhooks/rules-engine.js +287 -0
  197. package/dist/cloud/webhooks/types.d.ts +186 -0
  198. package/dist/cloud/webhooks/types.js +8 -0
  199. package/dist/continuity/formatter.d.ts +51 -0
  200. package/dist/continuity/formatter.js +313 -0
  201. package/dist/continuity/handoff-store.d.ts +67 -0
  202. package/dist/continuity/handoff-store.js +472 -0
  203. package/dist/continuity/index.d.ts +45 -0
  204. package/dist/continuity/index.js +48 -0
  205. package/dist/continuity/ledger-store.d.ts +110 -0
  206. package/dist/continuity/ledger-store.js +500 -0
  207. package/dist/continuity/manager.d.ts +178 -0
  208. package/dist/continuity/manager.js +562 -0
  209. package/dist/continuity/parser.d.ts +76 -0
  210. package/dist/continuity/parser.js +579 -0
  211. package/dist/continuity/types.d.ts +180 -0
  212. package/dist/continuity/types.js +9 -0
  213. package/dist/daemon/agent-manager.d.ts +27 -0
  214. package/dist/daemon/agent-manager.js +107 -6
  215. package/dist/daemon/agent-registry.d.ts +32 -0
  216. package/dist/daemon/agent-registry.js +42 -2
  217. package/dist/daemon/api.d.ts +12 -0
  218. package/dist/daemon/api.js +131 -2
  219. package/dist/daemon/cli-auth.d.ts +67 -0
  220. package/dist/daemon/cli-auth.js +537 -0
  221. package/dist/daemon/cloud-sync.js +9 -7
  222. package/dist/daemon/orchestrator.js +30 -0
  223. package/dist/daemon/router.d.ts +5 -0
  224. package/dist/daemon/router.js +78 -26
  225. package/dist/daemon/server.d.ts +5 -0
  226. package/dist/daemon/server.js +9 -1
  227. package/dist/daemon/services/browser-testing.d.ts +88 -0
  228. package/dist/daemon/services/browser-testing.js +244 -0
  229. package/dist/daemon/services/container-spawner.d.ts +135 -0
  230. package/dist/daemon/services/container-spawner.js +313 -0
  231. package/dist/daemon/types.d.ts +5 -1
  232. package/dist/dashboard/out/404.html +1 -1
  233. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
  234. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  235. package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +1 -0
  236. package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +9 -0
  237. package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +1 -0
  238. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-3fdfa60e53f2810d.js +1 -0
  239. package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +1 -0
  240. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +1 -0
  241. package/dist/dashboard/out/_next/static/chunks/app/history/{page-b6edd4dde8d08194.js → page-abb9ab2d329f56e9.js} +1 -1
  242. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
  243. package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +1 -0
  244. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +1 -0
  245. package/dist/dashboard/out/_next/static/chunks/app/page-77e9c65420a06cfb.js +1 -0
  246. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +1 -0
  247. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +1 -0
  248. package/dist/dashboard/out/_next/static/chunks/app/signup/page-68d34f50baa8ab6b.js +1 -0
  249. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  250. package/dist/dashboard/out/_next/static/chunks/{main-app-5d692157a8eb1fd9.js → main-app-6e8e8d3ef4e0192a.js} +1 -1
  251. package/dist/dashboard/out/_next/static/chunks/{main-c2f423b9c9f4591b.js → main-ed4e1fb6f29c34cf.js} +1 -1
  252. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  253. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
  254. package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +1 -0
  255. package/dist/dashboard/out/app/onboarding.html +1 -0
  256. package/dist/dashboard/out/app/onboarding.txt +7 -0
  257. package/dist/dashboard/out/app.html +1 -14
  258. package/dist/dashboard/out/app.txt +2 -2
  259. package/dist/dashboard/out/connect-repos.html +1 -0
  260. package/dist/dashboard/out/connect-repos.txt +7 -0
  261. package/dist/dashboard/out/history.html +1 -1
  262. package/dist/dashboard/out/history.txt +2 -2
  263. package/dist/dashboard/out/index.html +1 -1
  264. package/dist/dashboard/out/index.txt +2 -2
  265. package/dist/dashboard/out/login.html +6 -0
  266. package/dist/dashboard/out/login.txt +7 -0
  267. package/dist/dashboard/out/metrics.html +1 -1
  268. package/dist/dashboard/out/metrics.txt +2 -2
  269. package/dist/dashboard/out/pricing.html +3 -3
  270. package/dist/dashboard/out/pricing.txt +2 -2
  271. package/dist/dashboard/out/providers.html +1 -0
  272. package/dist/dashboard/out/providers.txt +7 -0
  273. package/dist/dashboard/out/signup.html +6 -0
  274. package/dist/dashboard/out/signup.txt +7 -0
  275. package/dist/dashboard-server/server.js +1308 -8
  276. package/dist/hooks/emitter.d.ts +40 -0
  277. package/dist/hooks/emitter.js +63 -0
  278. package/dist/hooks/index.d.ts +3 -0
  279. package/dist/hooks/index.js +3 -0
  280. package/dist/hooks/registry.d.ts +173 -0
  281. package/dist/hooks/registry.js +476 -0
  282. package/dist/hooks/trajectory-hooks.d.ts +52 -0
  283. package/dist/hooks/trajectory-hooks.js +183 -0
  284. package/dist/hooks/types.d.ts +141 -0
  285. package/dist/index.d.ts +2 -0
  286. package/dist/index.js +3 -0
  287. package/dist/memory/adapters/index.d.ts +8 -0
  288. package/dist/memory/adapters/index.js +8 -0
  289. package/dist/memory/adapters/inmemory.d.ts +59 -0
  290. package/dist/memory/adapters/inmemory.js +195 -0
  291. package/dist/memory/adapters/supermemory.d.ts +71 -0
  292. package/dist/memory/adapters/supermemory.js +338 -0
  293. package/dist/memory/factory.d.ts +48 -0
  294. package/dist/memory/factory.js +143 -0
  295. package/dist/memory/index.d.ts +32 -0
  296. package/dist/memory/index.js +32 -0
  297. package/dist/memory/memory-hooks.d.ts +60 -0
  298. package/dist/memory/memory-hooks.js +313 -0
  299. package/dist/memory/service.d.ts +49 -0
  300. package/dist/memory/service.js +146 -0
  301. package/dist/memory/types.d.ts +195 -0
  302. package/dist/memory/types.js +8 -0
  303. package/dist/policy/agent-policy.d.ts +225 -0
  304. package/dist/policy/agent-policy.js +665 -0
  305. package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
  306. package/dist/policy/cloud-policy-fetcher.js +64 -0
  307. package/dist/resiliency/crash-insights.d.ts +156 -0
  308. package/dist/resiliency/crash-insights.js +492 -0
  309. package/dist/resiliency/gossip-health.d.ts +137 -0
  310. package/dist/resiliency/gossip-health.js +241 -0
  311. package/dist/resiliency/index.d.ts +5 -0
  312. package/dist/resiliency/index.js +5 -0
  313. package/dist/resiliency/leader-watchdog.d.ts +109 -0
  314. package/dist/resiliency/leader-watchdog.js +189 -0
  315. package/dist/resiliency/memory-monitor.d.ts +172 -0
  316. package/dist/resiliency/memory-monitor.js +593 -0
  317. package/dist/resiliency/stateless-lead.d.ts +149 -0
  318. package/dist/resiliency/stateless-lead.js +308 -0
  319. package/dist/resiliency/supervisor.d.ts +38 -0
  320. package/dist/resiliency/supervisor.js +122 -0
  321. package/dist/shared/cli-auth-config.d.ts +91 -0
  322. package/dist/shared/cli-auth-config.js +264 -0
  323. package/dist/storage/adapter.d.ts +1 -1
  324. package/dist/trajectory/config.d.ts +84 -0
  325. package/dist/trajectory/config.js +163 -0
  326. package/dist/trajectory/index.d.ts +8 -0
  327. package/dist/trajectory/index.js +8 -0
  328. package/dist/trajectory/integration.d.ts +292 -0
  329. package/dist/trajectory/integration.js +834 -0
  330. package/dist/utils/logger.js +1 -1
  331. package/dist/utils/project-namespace.d.ts +24 -0
  332. package/dist/utils/project-namespace.js +84 -0
  333. package/dist/wrapper/parser.d.ts +10 -0
  334. package/dist/wrapper/parser.js +100 -33
  335. package/dist/wrapper/pty-wrapper.d.ts +197 -16
  336. package/dist/wrapper/pty-wrapper.js +943 -106
  337. package/dist/wrapper/shared.d.ts +165 -0
  338. package/dist/wrapper/shared.js +270 -0
  339. package/dist/wrapper/tmux-wrapper.d.ts +73 -11
  340. package/dist/wrapper/tmux-wrapper.js +541 -120
  341. package/package.json +16 -16
  342. package/scripts/postinstall.js +60 -0
  343. package/test-push.txt +1 -0
  344. package/bin/tmux +0 -0
  345. package/dist/bridge/config.d.ts.map +0 -1
  346. package/dist/bridge/config.js.map +0 -1
  347. package/dist/bridge/index.d.ts.map +0 -1
  348. package/dist/bridge/index.js.map +0 -1
  349. package/dist/bridge/multi-project-client.d.ts.map +0 -1
  350. package/dist/bridge/multi-project-client.js.map +0 -1
  351. package/dist/bridge/shadow-cli.d.ts.map +0 -1
  352. package/dist/bridge/shadow-cli.js.map +0 -1
  353. package/dist/bridge/shadow-config.d.ts.map +0 -1
  354. package/dist/bridge/shadow-config.js.map +0 -1
  355. package/dist/bridge/spawner.d.ts.map +0 -1
  356. package/dist/bridge/spawner.js.map +0 -1
  357. package/dist/bridge/teams-config.d.ts.map +0 -1
  358. package/dist/bridge/teams-config.js.map +0 -1
  359. package/dist/bridge/types.d.ts.map +0 -1
  360. package/dist/bridge/types.js.map +0 -1
  361. package/dist/bridge/utils.d.ts.map +0 -1
  362. package/dist/bridge/utils.js.map +0 -1
  363. package/dist/cli/index.d.ts.map +0 -1
  364. package/dist/cli/index.js.map +0 -1
  365. package/dist/cloud/api/auth.d.ts.map +0 -1
  366. package/dist/cloud/api/auth.js.map +0 -1
  367. package/dist/cloud/api/billing.d.ts.map +0 -1
  368. package/dist/cloud/api/billing.js.map +0 -1
  369. package/dist/cloud/api/coordinators.d.ts.map +0 -1
  370. package/dist/cloud/api/coordinators.js.map +0 -1
  371. package/dist/cloud/api/daemons.d.ts.map +0 -1
  372. package/dist/cloud/api/daemons.js.map +0 -1
  373. package/dist/cloud/api/middleware/planLimits.d.ts.map +0 -1
  374. package/dist/cloud/api/middleware/planLimits.js.map +0 -1
  375. package/dist/cloud/api/onboarding.d.ts.map +0 -1
  376. package/dist/cloud/api/onboarding.js.map +0 -1
  377. package/dist/cloud/api/providers.d.ts.map +0 -1
  378. package/dist/cloud/api/providers.js.map +0 -1
  379. package/dist/cloud/api/repos.d.ts.map +0 -1
  380. package/dist/cloud/api/repos.js.map +0 -1
  381. package/dist/cloud/api/teams.d.ts.map +0 -1
  382. package/dist/cloud/api/teams.js.map +0 -1
  383. package/dist/cloud/api/usage.d.ts.map +0 -1
  384. package/dist/cloud/api/usage.js.map +0 -1
  385. package/dist/cloud/api/workspaces.d.ts.map +0 -1
  386. package/dist/cloud/api/workspaces.js.map +0 -1
  387. package/dist/cloud/billing/index.d.ts.map +0 -1
  388. package/dist/cloud/billing/index.js.map +0 -1
  389. package/dist/cloud/billing/plans.d.ts.map +0 -1
  390. package/dist/cloud/billing/plans.js.map +0 -1
  391. package/dist/cloud/billing/service.d.ts.map +0 -1
  392. package/dist/cloud/billing/service.js.map +0 -1
  393. package/dist/cloud/billing/types.d.ts.map +0 -1
  394. package/dist/cloud/billing/types.js.map +0 -1
  395. package/dist/cloud/config.d.ts.map +0 -1
  396. package/dist/cloud/config.js.map +0 -1
  397. package/dist/cloud/db/drizzle.d.ts.map +0 -1
  398. package/dist/cloud/db/drizzle.js.map +0 -1
  399. package/dist/cloud/db/index.d.ts.map +0 -1
  400. package/dist/cloud/db/index.js.map +0 -1
  401. package/dist/cloud/db/schema.d.ts.map +0 -1
  402. package/dist/cloud/db/schema.js.map +0 -1
  403. package/dist/cloud/index.d.ts.map +0 -1
  404. package/dist/cloud/index.js.map +0 -1
  405. package/dist/cloud/provisioner/index.d.ts.map +0 -1
  406. package/dist/cloud/provisioner/index.js.map +0 -1
  407. package/dist/cloud/server.d.ts.map +0 -1
  408. package/dist/cloud/server.js.map +0 -1
  409. package/dist/cloud/services/coordinator.d.ts.map +0 -1
  410. package/dist/cloud/services/coordinator.js.map +0 -1
  411. package/dist/cloud/services/planLimits.d.ts.map +0 -1
  412. package/dist/cloud/services/planLimits.js.map +0 -1
  413. package/dist/cloud/vault/index.d.ts.map +0 -1
  414. package/dist/cloud/vault/index.js.map +0 -1
  415. package/dist/daemon/agent-manager.d.ts.map +0 -1
  416. package/dist/daemon/agent-manager.js.map +0 -1
  417. package/dist/daemon/agent-registry.d.ts.map +0 -1
  418. package/dist/daemon/agent-registry.js.map +0 -1
  419. package/dist/daemon/api.d.ts.map +0 -1
  420. package/dist/daemon/api.js.map +0 -1
  421. package/dist/daemon/auth.d.ts.map +0 -1
  422. package/dist/daemon/auth.js.map +0 -1
  423. package/dist/daemon/cloud-sync.d.ts.map +0 -1
  424. package/dist/daemon/cloud-sync.js.map +0 -1
  425. package/dist/daemon/connection.d.ts.map +0 -1
  426. package/dist/daemon/connection.js.map +0 -1
  427. package/dist/daemon/index.d.ts.map +0 -1
  428. package/dist/daemon/index.js.map +0 -1
  429. package/dist/daemon/orchestrator.d.ts.map +0 -1
  430. package/dist/daemon/orchestrator.js.map +0 -1
  431. package/dist/daemon/registry.d.ts.map +0 -1
  432. package/dist/daemon/registry.js.map +0 -1
  433. package/dist/daemon/router.d.ts.map +0 -1
  434. package/dist/daemon/router.js.map +0 -1
  435. package/dist/daemon/server.d.ts.map +0 -1
  436. package/dist/daemon/server.js.map +0 -1
  437. package/dist/daemon/types.d.ts.map +0 -1
  438. package/dist/daemon/types.js.map +0 -1
  439. package/dist/daemon/workspace-manager.d.ts.map +0 -1
  440. package/dist/daemon/workspace-manager.js.map +0 -1
  441. package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +0 -1
  442. package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +0 -1
  443. package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +0 -1
  444. package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +0 -1
  445. package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +0 -1
  446. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
  447. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +0 -1
  448. package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +0 -1
  449. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-d80e03a5297f95b6.js +0 -1
  450. package/dist/dashboard/out/_next/static/chunks/webpack-a5acc2831d094776.js +0 -1
  451. package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +0 -1
  452. package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +0 -1
  453. package/dist/dashboard-server/metrics.d.ts.map +0 -1
  454. package/dist/dashboard-server/metrics.js.map +0 -1
  455. package/dist/dashboard-server/needs-attention.d.ts.map +0 -1
  456. package/dist/dashboard-server/needs-attention.js.map +0 -1
  457. package/dist/dashboard-server/server.d.ts.map +0 -1
  458. package/dist/dashboard-server/server.js.map +0 -1
  459. package/dist/dashboard-server/start.d.ts.map +0 -1
  460. package/dist/dashboard-server/start.js.map +0 -1
  461. package/dist/hooks/inbox-check/hook.d.ts.map +0 -1
  462. package/dist/hooks/inbox-check/hook.js.map +0 -1
  463. package/dist/hooks/inbox-check/index.d.ts.map +0 -1
  464. package/dist/hooks/inbox-check/index.js.map +0 -1
  465. package/dist/hooks/inbox-check/types.d.ts.map +0 -1
  466. package/dist/hooks/inbox-check/types.js.map +0 -1
  467. package/dist/hooks/inbox-check/utils.d.ts.map +0 -1
  468. package/dist/hooks/inbox-check/utils.js.map +0 -1
  469. package/dist/hooks/index.d.ts.map +0 -1
  470. package/dist/hooks/index.js.map +0 -1
  471. package/dist/hooks/types.d.ts.map +0 -1
  472. package/dist/hooks/types.js.map +0 -1
  473. package/dist/index.d.ts.map +0 -1
  474. package/dist/index.js.map +0 -1
  475. package/dist/protocol/framing.d.ts.map +0 -1
  476. package/dist/protocol/framing.js.map +0 -1
  477. package/dist/protocol/index.d.ts.map +0 -1
  478. package/dist/protocol/index.js.map +0 -1
  479. package/dist/protocol/types.d.ts.map +0 -1
  480. package/dist/protocol/types.js.map +0 -1
  481. package/dist/resiliency/context-persistence.d.ts.map +0 -1
  482. package/dist/resiliency/context-persistence.js.map +0 -1
  483. package/dist/resiliency/health-monitor.d.ts.map +0 -1
  484. package/dist/resiliency/health-monitor.js.map +0 -1
  485. package/dist/resiliency/index.d.ts.map +0 -1
  486. package/dist/resiliency/index.js.map +0 -1
  487. package/dist/resiliency/logger.d.ts.map +0 -1
  488. package/dist/resiliency/logger.js.map +0 -1
  489. package/dist/resiliency/metrics.d.ts.map +0 -1
  490. package/dist/resiliency/metrics.js.map +0 -1
  491. package/dist/resiliency/provider-context.d.ts.map +0 -1
  492. package/dist/resiliency/provider-context.js.map +0 -1
  493. package/dist/resiliency/supervisor.d.ts.map +0 -1
  494. package/dist/resiliency/supervisor.js.map +0 -1
  495. package/dist/state/agent-state.d.ts.map +0 -1
  496. package/dist/state/agent-state.js.map +0 -1
  497. package/dist/storage/adapter.d.ts.map +0 -1
  498. package/dist/storage/adapter.js.map +0 -1
  499. package/dist/storage/sqlite-adapter.d.ts.map +0 -1
  500. package/dist/storage/sqlite-adapter.js.map +0 -1
  501. package/dist/utils/agent-config.d.ts.map +0 -1
  502. package/dist/utils/agent-config.js.map +0 -1
  503. package/dist/utils/command-resolver.d.ts.map +0 -1
  504. package/dist/utils/command-resolver.js.map +0 -1
  505. package/dist/utils/index.d.ts.map +0 -1
  506. package/dist/utils/index.js.map +0 -1
  507. package/dist/utils/logger.d.ts.map +0 -1
  508. package/dist/utils/logger.js.map +0 -1
  509. package/dist/utils/name-generator.d.ts.map +0 -1
  510. package/dist/utils/name-generator.js.map +0 -1
  511. package/dist/utils/project-namespace.d.ts.map +0 -1
  512. package/dist/utils/project-namespace.js.map +0 -1
  513. package/dist/utils/tmux-resolver.d.ts.map +0 -1
  514. package/dist/utils/tmux-resolver.js.map +0 -1
  515. package/dist/utils/update-checker.d.ts.map +0 -1
  516. package/dist/utils/update-checker.js.map +0 -1
  517. package/dist/wrapper/client.d.ts.map +0 -1
  518. package/dist/wrapper/client.js.map +0 -1
  519. package/dist/wrapper/inbox.d.ts.map +0 -1
  520. package/dist/wrapper/inbox.js.map +0 -1
  521. package/dist/wrapper/index.d.ts.map +0 -1
  522. package/dist/wrapper/index.js.map +0 -1
  523. package/dist/wrapper/parser.d.ts.map +0 -1
  524. package/dist/wrapper/parser.js.map +0 -1
  525. package/dist/wrapper/pty-wrapper.d.ts.map +0 -1
  526. package/dist/wrapper/pty-wrapper.js.map +0 -1
  527. package/dist/wrapper/tmux-wrapper.d.ts.map +0 -1
  528. package/dist/wrapper/tmux-wrapper.js.map +0 -1
  529. package/docs/AGENTS.md +0 -513
  530. package/docs/ARCHITECTURE_DECISIONS.md +0 -175
  531. package/docs/CHANGELOG.md +0 -11
  532. package/docs/CLI-SIMPLIFICATION-COMPLETE.md +0 -48
  533. package/docs/CLOUD-ARCHITECTURE.md +0 -652
  534. package/docs/CLOUD-ONBOARDING-DESIGN.md +0 -1983
  535. package/docs/COMPETITIVE_ANALYSIS.md +0 -897
  536. package/docs/CONTRIBUTING.md +0 -151
  537. package/docs/DESIGN_BRIDGE_STAFFING.md +0 -878
  538. package/docs/DESIGN_V2.md +0 -1079
  539. package/docs/INTEGRATION-GUIDE.md +0 -926
  540. package/docs/MONETIZATION.md +0 -1679
  541. package/docs/PROPOSAL-trajectories.md +0 -1582
  542. package/docs/PROTOCOL.md +0 -325
  543. package/docs/SCALING_ANALYSIS.md +0 -280
  544. package/docs/TESTING_PRESENCE_FEATURES.md +0 -327
  545. package/docs/TMUX_IMPLEMENTATION_NOTES.md +0 -364
  546. package/docs/TMUX_IMPROVEMENTS.md +0 -968
  547. package/docs/agent-relay-snippet.md +0 -168
  548. package/docs/competitive-analysis-mcp-agent-mail.md +0 -389
  549. package/docs/dashboard-v2-plan.md +0 -179
  550. package/docs/guides/CLOUD.md +0 -236
  551. package/docs/guides/LOCAL.md +0 -535
  552. package/docs/guides/SELF-HOSTED.md +0 -494
  553. package/docs/proposals/shadow-as-subagent.md +0 -765
  554. package/docs/proposals/slack-bot-integration.md +0 -1457
  555. package/docs/removable-code-analysis.md +0 -24
  556. package/scripts/dev/PUBLIC_RELEASE_PLAN.md +0 -88
  557. package/scripts/dev/dev-team-setup.sh +0 -431
  558. package/scripts/e2e-test.sh +0 -119
  559. package/scripts/games/game-protocol.md +0 -79
  560. package/scripts/games/hearts-setup.sh +0 -264
  561. package/scripts/tictactoe-setup.sh +0 -181
  562. /package/dist/dashboard/out/_next/static/chunks/{117-b2cd8d6485aacf2b.js → 117-f7b8ab0809342e77.js} +0 -0
  563. /package/dist/dashboard/out/_next/static/chunks/{648-8f3f26864ce515e5.js → 648-5cc6e1921389a58a.js} +0 -0
  564. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-0b990dbb71d72a98.js → page-53b8a69f76db17d0.js} +0 -0
  565. /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-bf46c09eb57e019c.js → fd9d1056-609918ca7b6280bb.js} +0 -0
  566. /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → wPgKJtcOmTFLpUncDg16A}/_buildManifest.js +0 -0
  567. /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → wPgKJtcOmTFLpUncDg16A}/_ssgManifest.js +0 -0
@@ -3,11 +3,73 @@
3
3
  *
4
4
  * One-click provisioning for compute resources (Fly.io, Railway, Docker).
5
5
  */
6
+ import * as crypto from 'crypto';
6
7
  import { getConfig } from '../config.js';
7
8
  import { db } from '../db/index.js';
8
9
  import { vault } from '../vault/index.js';
10
+ import { nangoService } from '../services/nango.js';
11
+ import { canAutoScale, canScaleToTier, getResourceTierForPlan, } from '../services/planLimits.js';
9
12
  const WORKSPACE_PORT = 3888;
10
13
  const FETCH_TIMEOUT_MS = 10_000;
14
+ const WORKSPACE_IMAGE = process.env.WORKSPACE_IMAGE || 'ghcr.io/agentworkforce/relay-workspace:latest';
15
+ // In-memory tracker for provisioning progress (workspace ID -> progress)
16
+ const provisioningProgress = new Map();
17
+ /**
18
+ * Update the provisioning stage for a workspace
19
+ */
20
+ function updateProvisioningStage(workspaceId, stage) {
21
+ const existing = provisioningProgress.get(workspaceId);
22
+ provisioningProgress.set(workspaceId, {
23
+ stage,
24
+ startedAt: existing?.startedAt ?? Date.now(),
25
+ updatedAt: Date.now(),
26
+ });
27
+ console.log(`[provisioner] Workspace ${workspaceId.substring(0, 8)} stage: ${stage}`);
28
+ }
29
+ /**
30
+ * Get the current provisioning stage for a workspace
31
+ */
32
+ export function getProvisioningStage(workspaceId) {
33
+ return provisioningProgress.get(workspaceId) ?? null;
34
+ }
35
+ /**
36
+ * Clear provisioning progress (call when complete or failed)
37
+ */
38
+ function clearProvisioningProgress(workspaceId) {
39
+ provisioningProgress.delete(workspaceId);
40
+ }
41
+ /**
42
+ * Schedule cleanup of provisioning progress after a delay
43
+ * This gives the frontend time to poll and see the 'complete' stage
44
+ */
45
+ function scheduleProgressCleanup(workspaceId, delayMs = 30_000) {
46
+ setTimeout(() => {
47
+ clearProvisioningProgress(workspaceId);
48
+ console.log(`[provisioner] Cleaned up provisioning progress for ${workspaceId.substring(0, 8)}`);
49
+ }, delayMs);
50
+ }
51
+ /**
52
+ * Get a fresh GitHub App installation token from Nango.
53
+ * Looks up the user's connected repositories to find a valid Nango connection.
54
+ */
55
+ async function getGithubAppTokenForUser(userId) {
56
+ try {
57
+ // Find any repository with a Nango connection for this user
58
+ const repos = await db.repositories.findByUserId(userId);
59
+ const repoWithConnection = repos.find(r => r.nangoConnectionId);
60
+ if (!repoWithConnection?.nangoConnectionId) {
61
+ console.warn(`[provisioner] No Nango GitHub App connection found for user ${userId}`);
62
+ return null;
63
+ }
64
+ // Get fresh installation token from Nango (handles refresh automatically)
65
+ const token = await nangoService.getGithubAppToken(repoWithConnection.nangoConnectionId);
66
+ return token;
67
+ }
68
+ catch (error) {
69
+ console.error(`[provisioner] Failed to get GitHub App token for user ${userId}:`, error);
70
+ return null;
71
+ }
72
+ }
11
73
  async function loadCredentialToken(userId, provider) {
12
74
  try {
13
75
  const cred = await vault.getCredential(userId, provider);
@@ -63,6 +125,96 @@ async function softHealthCheck(url) {
63
125
  console.warn(`[health] Failed to reach ${url}/health`, error);
64
126
  }
65
127
  }
128
+ /**
129
+ * Wait for machine to be in "started" state using Fly.io's /wait endpoint
130
+ * This is more efficient than polling - the API blocks until the state is reached
131
+ * @see https://fly.io/docs/machines/api/machines-resource/#wait-for-a-machine-to-reach-a-specific-state
132
+ */
133
+ async function waitForMachineStarted(apiToken, appName, machineId, timeoutSeconds = 120) {
134
+ console.log(`[provisioner] Waiting for machine ${machineId} to start (timeout: ${timeoutSeconds}s)...`);
135
+ try {
136
+ // Use Fly.io's /wait endpoint - blocks until machine reaches target state
137
+ const res = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines/${machineId}/wait?state=started&timeout=${timeoutSeconds}`, {
138
+ headers: { Authorization: `Bearer ${apiToken}` },
139
+ });
140
+ if (res.ok) {
141
+ console.log(`[provisioner] Machine ${machineId} is now started`);
142
+ return;
143
+ }
144
+ // 408 = timeout, machine didn't reach state in time
145
+ if (res.status === 408) {
146
+ // Get current state for error message
147
+ const stateRes = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines/${machineId}`, { headers: { Authorization: `Bearer ${apiToken}` } });
148
+ const machine = stateRes.ok ? (await stateRes.json()) : { state: 'unknown' };
149
+ throw new Error(`Machine ${machineId} did not start within ${timeoutSeconds}s (last state: ${machine.state})`);
150
+ }
151
+ // Other error
152
+ const errorText = await res.text();
153
+ throw new Error(`Wait for machine failed: ${res.status} ${errorText}`);
154
+ }
155
+ catch (error) {
156
+ if (error instanceof Error && error.message.includes('did not start')) {
157
+ throw error;
158
+ }
159
+ console.warn(`[provisioner] Error waiting for machine:`, error);
160
+ throw new Error(`Failed to wait for machine ${machineId}: ${error.message}`);
161
+ }
162
+ }
163
+ /**
164
+ * Wait for health check to pass (with DNS propagation time)
165
+ * Tries internal Fly network first if available, then falls back to public URL
166
+ */
167
+ async function waitForHealthy(url, appName, maxWaitMs = 90_000) {
168
+ const startTime = Date.now();
169
+ // Build list of URLs to try - internal first (faster, more reliable from inside Fly)
170
+ const urlsToTry = [];
171
+ // If running on Fly and app name provided, try internal network first
172
+ const isOnFly = !!process.env.FLY_APP_NAME;
173
+ if (isOnFly && appName) {
174
+ urlsToTry.push(`http://${appName}.internal:8080/health`);
175
+ }
176
+ // Always add the public URL as fallback
177
+ urlsToTry.push(`${url.replace(/\/$/, '')}/health`);
178
+ console.log(`[provisioner] Waiting for workspace to become healthy (trying: ${urlsToTry.join(', ')})...`);
179
+ while (Date.now() - startTime < maxWaitMs) {
180
+ // Try each URL in order
181
+ for (const healthUrl of urlsToTry) {
182
+ try {
183
+ const controller = new AbortController();
184
+ const timer = setTimeout(() => controller.abort(), 5_000);
185
+ const res = await fetch(healthUrl, {
186
+ method: 'GET',
187
+ signal: controller.signal,
188
+ });
189
+ clearTimeout(timer);
190
+ if (res.ok) {
191
+ console.log(`[provisioner] Health check passed via ${healthUrl}`);
192
+ return;
193
+ }
194
+ console.log(`[provisioner] Health check to ${healthUrl} returned ${res.status}`);
195
+ }
196
+ catch (error) {
197
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
198
+ const errMsg = error.message;
199
+ // Only log detailed error for last URL attempt
200
+ if (healthUrl === urlsToTry[urlsToTry.length - 1]) {
201
+ console.log(`[provisioner] Health check failed (${elapsed}s elapsed): ${errMsg}`);
202
+ }
203
+ }
204
+ }
205
+ await wait(3000);
206
+ }
207
+ // Don't throw - workspace is provisioned, health check is best-effort
208
+ console.warn(`[provisioner] Health check did not pass within ${maxWaitMs}ms, continuing anyway`);
209
+ }
210
+ // Resource tiers sized for Claude Code agents (~1-2GB RAM per agent)
211
+ // cpuKind: 'shared' = cheaper but can be throttled, 'performance' = dedicated
212
+ export const RESOURCE_TIERS = {
213
+ small: { name: 'small', cpuCores: 2, memoryMb: 2048, maxAgents: 2, cpuKind: 'shared' },
214
+ medium: { name: 'medium', cpuCores: 2, memoryMb: 4096, maxAgents: 5, cpuKind: 'shared' },
215
+ large: { name: 'large', cpuCores: 4, memoryMb: 8192, maxAgents: 10, cpuKind: 'performance' },
216
+ xlarge: { name: 'xlarge', cpuCores: 8, memoryMb: 16384, maxAgents: 20, cpuKind: 'performance' },
217
+ };
66
218
  /**
67
219
  * Fly.io provisioner
68
220
  */
@@ -71,6 +223,9 @@ class FlyProvisioner {
71
223
  org;
72
224
  region;
73
225
  workspaceDomain;
226
+ cloudApiUrl;
227
+ sessionSecret;
228
+ registryAuth;
74
229
  constructor() {
75
230
  const config = getConfig();
76
231
  if (!config.compute.fly) {
@@ -80,11 +235,26 @@ class FlyProvisioner {
80
235
  this.org = config.compute.fly.org;
81
236
  this.region = config.compute.fly.region || 'sjc';
82
237
  this.workspaceDomain = config.compute.fly.workspaceDomain;
238
+ this.registryAuth = config.compute.fly.registryAuth;
239
+ this.cloudApiUrl = config.publicUrl;
240
+ this.sessionSecret = config.sessionSecret;
241
+ }
242
+ /**
243
+ * Generate a workspace token for API authentication
244
+ * This is a simple HMAC - in production, consider using JWTs
245
+ */
246
+ generateWorkspaceToken(workspaceId) {
247
+ return crypto
248
+ .createHmac('sha256', this.sessionSecret)
249
+ .update(`workspace:${workspaceId}`)
250
+ .digest('hex');
83
251
  }
84
252
  async provision(workspace, credentials) {
85
253
  const appName = `ar-${workspace.id.substring(0, 8)}`;
254
+ // Stage: Creating workspace
255
+ updateProvisioningStage(workspace.id, 'creating');
86
256
  // Create Fly app
87
- const createResponse = await fetchWithRetry('https://api.machines.dev/v1/apps', {
257
+ await fetchWithRetry('https://api.machines.dev/v1/apps', {
88
258
  method: 'POST',
89
259
  headers: {
90
260
  Authorization: `Bearer ${this.apiToken}`,
@@ -95,19 +265,93 @@ class FlyProvisioner {
95
265
  org_slug: this.org,
96
266
  }),
97
267
  });
98
- // Set secrets (credentials)
268
+ // Stage: Networking
269
+ updateProvisioningStage(workspace.id, 'networking');
270
+ // Allocate IPs for the app (required for public DNS)
271
+ // Must use GraphQL API - Machines REST API doesn't support IP allocation
272
+ // Shared IPv4 is free, IPv6 is free
273
+ console.log(`[fly] Allocating IPs for ${appName}...`);
274
+ const allocateIP = async (type) => {
275
+ try {
276
+ // Map our type to Fly GraphQL enum
277
+ const graphqlType = type === 'shared_v4' ? 'shared_v4' : 'v6';
278
+ const res = await fetchWithRetry('https://api.fly.io/graphql', {
279
+ method: 'POST',
280
+ headers: {
281
+ Authorization: `Bearer ${this.apiToken}`,
282
+ 'Content-Type': 'application/json',
283
+ },
284
+ body: JSON.stringify({
285
+ query: `
286
+ mutation AllocateIPAddress($input: AllocateIPAddressInput!) {
287
+ allocateIpAddress(input: $input) {
288
+ ipAddress {
289
+ id
290
+ address
291
+ type
292
+ }
293
+ }
294
+ }
295
+ `,
296
+ variables: {
297
+ input: {
298
+ appId: appName,
299
+ type: graphqlType,
300
+ },
301
+ },
302
+ }),
303
+ });
304
+ if (!res.ok) {
305
+ const errorText = await res.text();
306
+ console.warn(`[fly] Failed to allocate ${type}: ${res.status} ${errorText}`);
307
+ return false;
308
+ }
309
+ const data = await res.json();
310
+ if (data.errors?.length) {
311
+ // Ignore "already allocated" errors
312
+ const alreadyAllocated = data.errors.some(e => e.message.includes('already') || e.message.includes('exists'));
313
+ if (!alreadyAllocated) {
314
+ console.warn(`[fly] GraphQL error allocating ${type}: ${data.errors[0].message}`);
315
+ return false;
316
+ }
317
+ console.log(`[fly] IP ${type} already allocated`);
318
+ return true;
319
+ }
320
+ const address = data.data?.allocateIpAddress?.ipAddress?.address;
321
+ console.log(`[fly] Allocated ${type}: ${address}`);
322
+ return true;
323
+ }
324
+ catch (err) {
325
+ console.warn(`[fly] Failed to allocate ${type}: ${err.message}`);
326
+ return false;
327
+ }
328
+ };
329
+ const [sharedV4Result, v6Result] = await Promise.all([
330
+ allocateIP('shared_v4'),
331
+ allocateIP('v6'),
332
+ ]);
333
+ console.log(`[fly] IP allocation results: shared_v4=${sharedV4Result}, v6=${v6Result}`);
334
+ // Stage: Secrets
335
+ updateProvisioningStage(workspace.id, 'secrets');
336
+ // Set secrets (provider credentials)
99
337
  const secrets = {};
100
338
  for (const [provider, token] of credentials) {
101
339
  secrets[`${provider.toUpperCase()}_TOKEN`] = token;
340
+ // Also set GH_TOKEN for gh CLI compatibility
341
+ if (provider === 'github') {
342
+ secrets['GH_TOKEN'] = token;
343
+ }
344
+ }
345
+ if (Object.keys(secrets).length > 0) {
346
+ await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/secrets`, {
347
+ method: 'POST',
348
+ headers: {
349
+ Authorization: `Bearer ${this.apiToken}`,
350
+ 'Content-Type': 'application/json',
351
+ },
352
+ body: JSON.stringify(secrets),
353
+ });
102
354
  }
103
- await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/secrets`, {
104
- method: 'POST',
105
- headers: {
106
- Authorization: `Bearer ${this.apiToken}`,
107
- 'Content-Type': 'application/json',
108
- },
109
- body: JSON.stringify(secrets),
110
- });
111
355
  // If custom workspace domain is configured, add certificate
112
356
  const customHostname = this.workspaceDomain
113
357
  ? `${appName}.${this.workspaceDomain}`
@@ -115,6 +359,8 @@ class FlyProvisioner {
115
359
  if (customHostname) {
116
360
  await this.allocateCertificate(appName, customHostname);
117
361
  }
362
+ // Stage: Machine
363
+ updateProvisioningStage(workspace.id, 'machine');
118
364
  // Create machine with auto-stop/start for cost optimization
119
365
  const machineResponse = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines`, {
120
366
  method: 'POST',
@@ -125,7 +371,15 @@ class FlyProvisioner {
125
371
  body: JSON.stringify({
126
372
  region: this.region,
127
373
  config: {
128
- image: 'ghcr.io/khaliqgant/agent-relay-workspace:latest',
374
+ image: WORKSPACE_IMAGE,
375
+ // Registry auth for private ghcr.io images
376
+ ...(this.registryAuth && {
377
+ image_registry_auth: {
378
+ registry: 'ghcr.io',
379
+ username: this.registryAuth.username,
380
+ password: this.registryAuth.password,
381
+ },
382
+ }),
129
383
  env: {
130
384
  WORKSPACE_ID: workspace.id,
131
385
  SUPERVISOR_ENABLED: String(workspace.config.supervisorEnabled ?? false),
@@ -134,25 +388,56 @@ class FlyProvisioner {
134
388
  PROVIDERS: (workspace.config.providers ?? []).join(','),
135
389
  PORT: String(WORKSPACE_PORT),
136
390
  AGENT_RELAY_DASHBOARD_PORT: String(WORKSPACE_PORT),
391
+ // Git gateway configuration
392
+ CLOUD_API_URL: this.cloudApiUrl,
393
+ WORKSPACE_TOKEN: this.generateWorkspaceToken(workspace.id),
137
394
  },
138
395
  services: [
139
396
  {
140
397
  ports: [
141
- { port: 443, handlers: ['tls', 'http'] },
398
+ {
399
+ port: 443,
400
+ handlers: ['tls', 'http'],
401
+ // Force HTTP/1.1 to backend for WebSocket upgrade compatibility
402
+ // HTTP/2 doesn't support traditional WebSocket upgrade mechanism
403
+ http_options: {
404
+ h2_backend: false,
405
+ },
406
+ },
142
407
  { port: 80, handlers: ['http'] },
143
408
  ],
144
409
  protocol: 'tcp',
145
410
  internal_port: WORKSPACE_PORT,
146
- // Auto-stop after 5 minutes of inactivity
147
- auto_stop_machines: true,
411
+ // Auto-stop after inactivity to reduce costs
412
+ // Fly Proxy automatically wakes machines on incoming requests
413
+ auto_stop_machines: 'stop', // stop (not suspend) for faster wake
148
414
  auto_start_machines: true,
149
415
  min_machines_running: 0,
416
+ // Idle timeout before auto-stop (in seconds)
417
+ // Longer timeout = better UX, shorter = lower costs
418
+ concurrency: {
419
+ type: 'requests',
420
+ soft_limit: 25,
421
+ hard_limit: 50,
422
+ },
150
423
  },
151
424
  ],
425
+ checks: {
426
+ health: {
427
+ type: 'http',
428
+ port: WORKSPACE_PORT,
429
+ path: '/health',
430
+ interval: '30s',
431
+ timeout: '5s',
432
+ grace_period: '10s',
433
+ },
434
+ },
435
+ // Start with small tier (shared CPUs) - scales up based on plan
436
+ // Free tier uses shared CPUs for cost efficiency
152
437
  guest: {
153
438
  cpu_kind: 'shared',
154
- cpus: 1,
155
- memory_mb: 512,
439
+ cpus: 2,
440
+ memory_mb: 2048,
156
441
  },
157
442
  },
158
443
  }),
@@ -166,7 +451,19 @@ class FlyProvisioner {
166
451
  const publicUrl = customHostname
167
452
  ? `https://${customHostname}`
168
453
  : `https://${appName}.fly.dev`;
169
- await softHealthCheck(publicUrl);
454
+ // Stage: Booting
455
+ updateProvisioningStage(workspace.id, 'booting');
456
+ // Wait for machine to be in started state
457
+ await waitForMachineStarted(this.apiToken, appName, machine.id);
458
+ // Stage: Health check
459
+ updateProvisioningStage(workspace.id, 'health');
460
+ // Wait for health check to pass (includes DNS propagation time)
461
+ // Pass appName to enable internal Fly network health checks
462
+ await waitForHealthy(publicUrl, appName);
463
+ // Stage: Complete
464
+ updateProvisioningStage(workspace.id, 'complete');
465
+ // Schedule cleanup of provisioning progress after 30s (gives frontend time to see 'complete')
466
+ scheduleProgressCleanup(workspace.id);
170
467
  return {
171
468
  computeId: machine.id,
172
469
  publicUrl,
@@ -236,18 +533,110 @@ class FlyProvisioner {
236
533
  },
237
534
  });
238
535
  }
536
+ /**
537
+ * Resize workspace - vertical scaling via Fly Machines API
538
+ */
539
+ async resize(workspace, tier) {
540
+ if (!workspace.computeId)
541
+ return;
542
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
543
+ // Update machine configuration
544
+ await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}`, {
545
+ method: 'POST',
546
+ headers: {
547
+ Authorization: `Bearer ${this.apiToken}`,
548
+ 'Content-Type': 'application/json',
549
+ },
550
+ body: JSON.stringify({
551
+ config: {
552
+ guest: {
553
+ // Use tier-specific CPU type (shared for cost, performance for power)
554
+ cpu_kind: tier.cpuKind,
555
+ cpus: tier.cpuCores,
556
+ memory_mb: tier.memoryMb,
557
+ },
558
+ env: {
559
+ MAX_AGENTS: String(tier.maxAgents),
560
+ },
561
+ },
562
+ }),
563
+ });
564
+ console.log(`[fly] Resized workspace ${workspace.id} to ${tier.name} (${tier.cpuCores} CPU, ${tier.memoryMb}MB RAM)`);
565
+ }
566
+ /**
567
+ * Update the max agent limit for a workspace
568
+ */
569
+ async updateAgentLimit(workspace, newLimit) {
570
+ if (!workspace.computeId)
571
+ return;
572
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
573
+ // Update environment variable
574
+ await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}`, {
575
+ method: 'POST',
576
+ headers: {
577
+ Authorization: `Bearer ${this.apiToken}`,
578
+ 'Content-Type': 'application/json',
579
+ },
580
+ body: JSON.stringify({
581
+ config: {
582
+ env: {
583
+ MAX_AGENTS: String(newLimit),
584
+ },
585
+ },
586
+ }),
587
+ });
588
+ console.log(`[fly] Updated workspace ${workspace.id} agent limit to ${newLimit}`);
589
+ }
590
+ /**
591
+ * Get current resource tier for a workspace
592
+ */
593
+ async getCurrentTier(workspace) {
594
+ if (!workspace.computeId) {
595
+ return RESOURCE_TIERS.small;
596
+ }
597
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
598
+ const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}`, {
599
+ headers: {
600
+ Authorization: `Bearer ${this.apiToken}`,
601
+ },
602
+ });
603
+ if (!response.ok) {
604
+ return RESOURCE_TIERS.small;
605
+ }
606
+ const machine = await response.json();
607
+ const _cpus = machine.config?.guest?.cpus || 1;
608
+ const memoryMb = machine.config?.guest?.memory_mb || 512;
609
+ // Map to nearest tier
610
+ if (memoryMb >= 4096)
611
+ return RESOURCE_TIERS.xlarge;
612
+ if (memoryMb >= 2048)
613
+ return RESOURCE_TIERS.large;
614
+ if (memoryMb >= 1024)
615
+ return RESOURCE_TIERS.medium;
616
+ return RESOURCE_TIERS.small;
617
+ }
239
618
  }
240
619
  /**
241
620
  * Railway provisioner
242
621
  */
243
622
  class RailwayProvisioner {
244
623
  apiToken;
624
+ cloudApiUrl;
625
+ sessionSecret;
245
626
  constructor() {
246
627
  const config = getConfig();
247
628
  if (!config.compute.railway) {
248
629
  throw new Error('Railway configuration missing');
249
630
  }
250
631
  this.apiToken = config.compute.railway.apiToken;
632
+ this.cloudApiUrl = config.publicUrl;
633
+ this.sessionSecret = config.sessionSecret;
634
+ }
635
+ generateWorkspaceToken(workspaceId) {
636
+ return crypto
637
+ .createHmac('sha256', this.sessionSecret)
638
+ .update(`workspace:${workspaceId}`)
639
+ .digest('hex');
251
640
  }
252
641
  async provision(workspace, credentials) {
253
642
  // Create project
@@ -295,7 +684,7 @@ class RailwayProvisioner {
295
684
  projectId,
296
685
  name: 'workspace',
297
686
  source: {
298
- image: 'ghcr.io/khaliqgant/agent-relay-workspace:latest',
687
+ image: WORKSPACE_IMAGE,
299
688
  },
300
689
  },
301
690
  },
@@ -312,9 +701,15 @@ class RailwayProvisioner {
312
701
  PROVIDERS: (workspace.config.providers ?? []).join(','),
313
702
  PORT: String(WORKSPACE_PORT),
314
703
  AGENT_RELAY_DASHBOARD_PORT: String(WORKSPACE_PORT),
704
+ CLOUD_API_URL: this.cloudApiUrl,
705
+ WORKSPACE_TOKEN: this.generateWorkspaceToken(workspace.id),
315
706
  };
316
707
  for (const [provider, token] of credentials) {
317
708
  envVars[`${provider.toUpperCase()}_TOKEN`] = token;
709
+ // Also set GH_TOKEN for gh CLI compatibility
710
+ if (provider === 'github') {
711
+ envVars['GH_TOKEN'] = token;
712
+ }
318
713
  }
319
714
  await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
320
715
  method: 'POST',
@@ -463,6 +858,56 @@ class RailwayProvisioner {
463
858
  * Local Docker provisioner (for development/self-hosted)
464
859
  */
465
860
  class DockerProvisioner {
861
+ cloudApiUrl;
862
+ cloudApiUrlForContainer;
863
+ sessionSecret;
864
+ constructor() {
865
+ const config = getConfig();
866
+ this.cloudApiUrl = config.publicUrl;
867
+ this.sessionSecret = config.sessionSecret;
868
+ // For Docker containers, localhost won't work - they need to reach the host
869
+ // Convert localhost URLs to host.docker.internal for container access
870
+ if (this.cloudApiUrl.includes('localhost') || this.cloudApiUrl.includes('127.0.0.1')) {
871
+ this.cloudApiUrlForContainer = this.cloudApiUrl
872
+ .replace('localhost', 'host.docker.internal')
873
+ .replace('127.0.0.1', 'host.docker.internal');
874
+ console.log(`[docker] Container API URL: ${this.cloudApiUrlForContainer} (host: ${this.cloudApiUrl})`);
875
+ }
876
+ else {
877
+ this.cloudApiUrlForContainer = this.cloudApiUrl;
878
+ }
879
+ }
880
+ generateWorkspaceToken(workspaceId) {
881
+ return crypto
882
+ .createHmac('sha256', this.sessionSecret)
883
+ .update(`workspace:${workspaceId}`)
884
+ .digest('hex');
885
+ }
886
+ /**
887
+ * Wait for container to be healthy by polling the health endpoint
888
+ */
889
+ async waitForHealthy(publicUrl, timeoutMs = 60_000) {
890
+ const startTime = Date.now();
891
+ const pollInterval = 2000;
892
+ console.log(`[docker] Waiting for container to be healthy at ${publicUrl}...`);
893
+ while (Date.now() - startTime < timeoutMs) {
894
+ try {
895
+ const response = await fetch(`${publicUrl}/health`, {
896
+ method: 'GET',
897
+ signal: AbortSignal.timeout(5000),
898
+ });
899
+ if (response.ok) {
900
+ console.log(`[docker] Container healthy after ${Date.now() - startTime}ms`);
901
+ return;
902
+ }
903
+ }
904
+ catch {
905
+ // Container not ready yet, continue polling
906
+ }
907
+ await wait(pollInterval);
908
+ }
909
+ throw new Error(`Container did not become healthy within ${timeoutMs}ms`);
910
+ }
466
911
  async provision(workspace, credentials) {
467
912
  const containerName = `ar-${workspace.id.substring(0, 8)}`;
468
913
  // Build environment variables
@@ -474,21 +919,54 @@ class DockerProvisioner {
474
919
  `-e PROVIDERS=${(workspace.config.providers ?? []).join(',')}`,
475
920
  `-e PORT=${WORKSPACE_PORT}`,
476
921
  `-e AGENT_RELAY_DASHBOARD_PORT=${WORKSPACE_PORT}`,
922
+ `-e CLOUD_API_URL=${this.cloudApiUrlForContainer}`,
923
+ `-e WORKSPACE_TOKEN=${this.generateWorkspaceToken(workspace.id)}`,
477
924
  ];
478
925
  for (const [provider, token] of credentials) {
479
926
  envArgs.push(`-e ${provider.toUpperCase()}_TOKEN=${token}`);
927
+ // Also set GH_TOKEN for gh CLI compatibility
928
+ if (provider === 'github') {
929
+ envArgs.push(`-e GH_TOKEN=${token}`);
930
+ }
480
931
  }
481
932
  // Run container
482
933
  const { execSync } = await import('child_process');
483
934
  const hostPort = 3000 + Math.floor(Math.random() * 1000);
935
+ // When running in Docker, connect to the same network for container-to-container communication
936
+ const runningInDocker = process.env.RUNNING_IN_DOCKER === 'true';
937
+ const networkArg = runningInDocker ? '--network agent-relay-dev' : '';
938
+ // In development, mount local dist and docs folders for faster iteration
939
+ // Set WORKSPACE_DEV_MOUNT=true to enable
940
+ const devMount = process.env.WORKSPACE_DEV_MOUNT === 'true';
941
+ const volumeArgs = devMount
942
+ ? `-v "${process.cwd()}/dist:/app/dist:ro" -v "${process.cwd()}/docs:/app/docs:ro"`
943
+ : '';
944
+ if (devMount) {
945
+ console.log('[provisioner] Dev mode: mounting local dist/ and docs/ folders into workspace container');
946
+ }
484
947
  try {
485
- execSync(`docker run -d --name ${containerName} -p ${hostPort}:${WORKSPACE_PORT} ${envArgs.join(' ')} ghcr.io/khaliqgant/agent-relay-workspace:latest`, { stdio: 'pipe' });
948
+ execSync(`docker run -d --user root --name ${containerName} ${networkArg} ${volumeArgs} -p ${hostPort}:${WORKSPACE_PORT} ${envArgs.join(' ')} ${WORKSPACE_IMAGE}`, { stdio: 'pipe' });
949
+ const publicUrl = `http://localhost:${hostPort}`;
950
+ // Wait for container to be healthy before returning
951
+ // When running in Docker, use the internal container name for health check
952
+ const healthCheckUrl = runningInDocker
953
+ ? `http://${containerName}:${WORKSPACE_PORT}`
954
+ : publicUrl;
955
+ await this.waitForHealthy(healthCheckUrl);
486
956
  return {
487
957
  computeId: containerName,
488
- publicUrl: `http://localhost:${hostPort}`,
958
+ publicUrl,
489
959
  };
490
960
  }
491
961
  catch (error) {
962
+ // Clean up container if it was created but health check failed
963
+ try {
964
+ const { execSync: execSyncCleanup } = await import('child_process');
965
+ execSyncCleanup(`docker rm -f ${containerName}`, { stdio: 'pipe' });
966
+ }
967
+ catch {
968
+ // Ignore cleanup errors
969
+ }
492
970
  throw new Error(`Failed to start Docker container: ${error}`);
493
971
  }
494
972
  }
@@ -559,6 +1037,7 @@ export class WorkspaceProvisioner {
559
1037
  }
560
1038
  /**
561
1039
  * Provision a new workspace (one-click)
1040
+ * Returns immediately with 'provisioning' status and runs actual provisioning in background
562
1041
  */
563
1042
  async provision(config) {
564
1043
  // Create workspace record
@@ -573,6 +1052,31 @@ export class WorkspaceProvisioner {
573
1052
  maxAgents: config.maxAgents ?? 10,
574
1053
  },
575
1054
  });
1055
+ // Add creator as owner in workspace_members for team collaboration support
1056
+ await db.workspaceMembers.addMember({
1057
+ workspaceId: workspace.id,
1058
+ userId: config.userId,
1059
+ role: 'owner',
1060
+ invitedBy: config.userId, // Self-invited as creator
1061
+ });
1062
+ // Auto-accept the creator's membership
1063
+ await db.workspaceMembers.acceptInvite(workspace.id, config.userId);
1064
+ // Initialize stage tracking immediately
1065
+ updateProvisioningStage(workspace.id, 'creating');
1066
+ // Run provisioning in the background so frontend can poll for stages
1067
+ this.runProvisioningAsync(workspace, config).catch((error) => {
1068
+ console.error(`[provisioner] Background provisioning failed for ${workspace.id}:`, error);
1069
+ });
1070
+ // Return immediately with 'provisioning' status
1071
+ return {
1072
+ workspaceId: workspace.id,
1073
+ status: 'provisioning',
1074
+ };
1075
+ }
1076
+ /**
1077
+ * Run the actual provisioning work asynchronously
1078
+ */
1079
+ async runProvisioningAsync(workspace, config) {
576
1080
  // Get credentials
577
1081
  const credentials = new Map();
578
1082
  for (const provider of config.providers) {
@@ -582,13 +1086,22 @@ export class WorkspaceProvisioner {
582
1086
  }
583
1087
  }
584
1088
  // GitHub token is required for cloning repositories
1089
+ // Use direct token if provided (for testing), otherwise get from Nango
585
1090
  if (config.repositories.length > 0) {
586
- const githubToken = await loadCredentialToken(config.userId, 'github');
587
- if (githubToken) {
588
- credentials.set('github', githubToken);
1091
+ if (config.githubToken) {
1092
+ // Direct token provided (for testing)
1093
+ credentials.set('github', config.githubToken);
1094
+ console.log('[provisioner] Using provided GitHub token');
589
1095
  }
590
1096
  else {
591
- console.warn(`No GitHub token found for user ${config.userId}; repository cloning may fail.`);
1097
+ // Get fresh installation token from Nango GitHub App
1098
+ const githubToken = await getGithubAppTokenForUser(config.userId);
1099
+ if (githubToken) {
1100
+ credentials.set('github', githubToken);
1101
+ }
1102
+ else {
1103
+ console.warn(`[provisioner] No GitHub App token for user ${config.userId}; repository cloning may fail.`);
1104
+ }
592
1105
  }
593
1106
  }
594
1107
  // Provision compute
@@ -598,22 +1111,21 @@ export class WorkspaceProvisioner {
598
1111
  computeId,
599
1112
  publicUrl,
600
1113
  });
601
- return {
602
- workspaceId: workspace.id,
603
- status: 'running',
604
- publicUrl,
605
- };
1114
+ // Schedule cleanup of provisioning progress after 30s (gives frontend time to see 'complete')
1115
+ setTimeout(() => {
1116
+ clearProvisioningProgress(workspace.id);
1117
+ console.log(`[provisioner] Cleaned up provisioning progress for ${workspace.id.substring(0, 8)}`);
1118
+ }, 30_000);
1119
+ console.log(`[provisioner] Workspace ${workspace.id} provisioned successfully at ${publicUrl}`);
606
1120
  }
607
1121
  catch (error) {
608
1122
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
609
1123
  await db.workspaces.updateStatus(workspace.id, 'error', {
610
1124
  errorMessage,
611
1125
  });
612
- return {
613
- workspaceId: workspace.id,
614
- status: 'error',
615
- error: errorMessage,
616
- };
1126
+ // Clear provisioning progress on error
1127
+ clearProvisioningProgress(workspace.id);
1128
+ console.error(`[provisioner] Workspace ${workspace.id} provisioning failed:`, errorMessage);
617
1129
  }
618
1130
  }
619
1131
  /**
@@ -635,6 +1147,11 @@ export class WorkspaceProvisioner {
635
1147
  if (!workspace) {
636
1148
  throw new Error('Workspace not found');
637
1149
  }
1150
+ // During early provisioning, computeId isn't set yet
1151
+ // Return the database status instead of querying the provider
1152
+ if (!workspace.computeId && workspace.status === 'provisioning') {
1153
+ return 'provisioning';
1154
+ }
638
1155
  const status = await this.provisioner.getStatus(workspace);
639
1156
  // Update database if status changed
640
1157
  if (status !== workspace.status) {
@@ -664,6 +1181,131 @@ export class WorkspaceProvisioner {
664
1181
  await this.provisioner.deprovision(workspace);
665
1182
  await db.workspaces.updateStatus(workspaceId, 'stopped');
666
1183
  }
1184
+ /**
1185
+ * Resize a workspace (vertical scaling)
1186
+ */
1187
+ async resize(workspaceId, tier) {
1188
+ const workspace = await db.workspaces.findById(workspaceId);
1189
+ if (!workspace) {
1190
+ throw new Error('Workspace not found');
1191
+ }
1192
+ if (!this.provisioner.resize) {
1193
+ throw new Error('Resize not supported by current compute provider');
1194
+ }
1195
+ await this.provisioner.resize(workspace, tier);
1196
+ // Update workspace config with new limits
1197
+ await db.workspaces.updateConfig(workspaceId, {
1198
+ ...workspace.config,
1199
+ maxAgents: tier.maxAgents,
1200
+ resourceTier: tier.name,
1201
+ });
1202
+ }
1203
+ /**
1204
+ * Update the max agent limit for a workspace
1205
+ */
1206
+ async updateAgentLimit(workspaceId, newLimit) {
1207
+ const workspace = await db.workspaces.findById(workspaceId);
1208
+ if (!workspace) {
1209
+ throw new Error('Workspace not found');
1210
+ }
1211
+ if (this.provisioner.updateAgentLimit) {
1212
+ await this.provisioner.updateAgentLimit(workspace, newLimit);
1213
+ }
1214
+ // Update workspace config
1215
+ await db.workspaces.updateConfig(workspaceId, {
1216
+ ...workspace.config,
1217
+ maxAgents: newLimit,
1218
+ });
1219
+ }
1220
+ /**
1221
+ * Get current resource tier for a workspace
1222
+ */
1223
+ async getCurrentTier(workspaceId) {
1224
+ const workspace = await db.workspaces.findById(workspaceId);
1225
+ if (!workspace) {
1226
+ throw new Error('Workspace not found');
1227
+ }
1228
+ if (this.provisioner.getCurrentTier) {
1229
+ return this.provisioner.getCurrentTier(workspace);
1230
+ }
1231
+ // Fallback: determine from config or default to small
1232
+ const tierName = workspace.config.resourceTier || 'small';
1233
+ return RESOURCE_TIERS[tierName] || RESOURCE_TIERS.small;
1234
+ }
1235
+ /**
1236
+ * Get recommended tier based on agent count
1237
+ * Uses 1.5-2GB per agent as baseline for Claude Code
1238
+ */
1239
+ getRecommendedTier(agentCount) {
1240
+ // Find the smallest tier that supports this agent count
1241
+ const tiers = Object.values(RESOURCE_TIERS).sort((a, b) => a.maxAgents - b.maxAgents);
1242
+ for (const tier of tiers) {
1243
+ if (tier.maxAgents >= agentCount) {
1244
+ return tier;
1245
+ }
1246
+ }
1247
+ // If agent count exceeds all tiers, return the largest
1248
+ return RESOURCE_TIERS.xlarge;
1249
+ }
1250
+ /**
1251
+ * Auto-scale workspace based on current agent count
1252
+ * Respects plan limits - free tier cannot scale, others have max tier limits
1253
+ * Returns { scaled: boolean, reason?: string }
1254
+ */
1255
+ async autoScale(workspaceId, currentAgentCount) {
1256
+ const workspace = await db.workspaces.findById(workspaceId);
1257
+ if (!workspace) {
1258
+ throw new Error('Workspace not found');
1259
+ }
1260
+ // Get user's plan
1261
+ const user = await db.users.findById(workspace.userId);
1262
+ const plan = user?.plan || 'free';
1263
+ // Check if plan allows auto-scaling
1264
+ if (!canAutoScale(plan)) {
1265
+ return {
1266
+ scaled: false,
1267
+ reason: 'Auto-scaling requires Pro plan or higher',
1268
+ };
1269
+ }
1270
+ const currentTier = await this.getCurrentTier(workspaceId);
1271
+ const recommendedTier = this.getRecommendedTier(currentAgentCount);
1272
+ // Only scale UP, never down (to avoid disruption)
1273
+ if (recommendedTier.memoryMb <= currentTier.memoryMb) {
1274
+ return {
1275
+ scaled: false,
1276
+ currentTier: currentTier.name,
1277
+ };
1278
+ }
1279
+ // Check if plan allows scaling to the recommended tier
1280
+ if (!canScaleToTier(plan, recommendedTier.name)) {
1281
+ // Find the max tier allowed for this plan
1282
+ const maxTierName = getResourceTierForPlan(plan);
1283
+ const maxTier = RESOURCE_TIERS[maxTierName];
1284
+ if (maxTier.memoryMb <= currentTier.memoryMb) {
1285
+ return {
1286
+ scaled: false,
1287
+ reason: `Already at max tier (${currentTier.name}) for ${plan} plan`,
1288
+ currentTier: currentTier.name,
1289
+ };
1290
+ }
1291
+ // Scale to max allowed tier instead
1292
+ console.log(`[provisioner] Auto-scaling workspace ${workspaceId.substring(0, 8)} from ${currentTier.name} to ${maxTierName} (max for ${plan} plan)`);
1293
+ await this.resize(workspaceId, maxTier);
1294
+ return {
1295
+ scaled: true,
1296
+ currentTier: currentTier.name,
1297
+ targetTier: maxTierName,
1298
+ reason: `Scaled to max tier for ${plan} plan`,
1299
+ };
1300
+ }
1301
+ console.log(`[provisioner] Auto-scaling workspace ${workspaceId.substring(0, 8)} from ${currentTier.name} to ${recommendedTier.name} (${currentAgentCount} agents)`);
1302
+ await this.resize(workspaceId, recommendedTier);
1303
+ return {
1304
+ scaled: true,
1305
+ currentTier: currentTier.name,
1306
+ targetTier: recommendedTier.name,
1307
+ };
1308
+ }
667
1309
  }
668
1310
  // Singleton instance
669
1311
  let _provisioner = null;