agent-relay 1.2.0 → 1.3.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 (668) hide show
  1. package/.gitattributes +3 -0
  2. package/.nvmrc +1 -0
  3. package/.trajectories/agent-relay-322-324.md +17 -0
  4. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
  5. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
  6. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
  7. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
  8. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.json +65 -0
  9. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.md +37 -0
  10. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.json +65 -0
  11. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.md +37 -0
  12. package/.trajectories/completed/2026-01/traj_1rp3rges5811.json +49 -0
  13. package/.trajectories/completed/2026-01/traj_1rp3rges5811.md +31 -0
  14. package/.trajectories/completed/2026-01/traj_22bhyulruouw.json +113 -0
  15. package/.trajectories/completed/2026-01/traj_22bhyulruouw.md +57 -0
  16. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.json +53 -0
  17. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.md +32 -0
  18. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
  19. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
  20. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.json +26 -0
  21. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.md +6 -0
  22. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.json +47 -0
  23. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.md +32 -0
  24. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.json +53 -0
  25. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.md +32 -0
  26. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
  27. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
  28. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.json +59 -0
  29. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.md +33 -0
  30. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.json +53 -0
  31. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.md +32 -0
  32. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.json +48 -0
  33. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.md +24 -0
  34. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
  35. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
  36. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
  37. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
  38. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.json +209 -0
  39. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.md +97 -0
  40. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
  41. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
  42. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.json +48 -0
  43. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.md +24 -0
  44. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.json +49 -0
  45. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.md +23 -0
  46. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
  47. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
  48. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.json +53 -0
  49. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.md +32 -0
  50. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.json +49 -0
  51. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.md +31 -0
  52. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.json +26 -0
  53. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.md +6 -0
  54. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
  55. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
  56. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.json +59 -0
  57. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.md +37 -0
  58. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
  59. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
  60. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
  61. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
  62. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.json +77 -0
  63. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.md +42 -0
  64. package/.trajectories/completed/2026-01/traj_gjdre5voouod.json +53 -0
  65. package/.trajectories/completed/2026-01/traj_gjdre5voouod.md +32 -0
  66. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.json +25 -0
  67. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.md +15 -0
  68. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.json +101 -0
  69. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.md +44 -0
  70. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
  71. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
  72. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
  73. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
  74. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json +22 -0
  75. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md +5 -0
  76. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.json +53 -0
  77. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.md +32 -0
  78. package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
  79. package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
  80. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.json +25 -0
  81. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.md +15 -0
  82. package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
  83. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.json +53 -0
  84. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.md +32 -0
  85. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.json +53 -0
  86. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.md +32 -0
  87. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.json +48 -0
  88. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.md +24 -0
  89. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.json +53 -0
  90. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.md +32 -0
  91. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
  92. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
  93. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.json +77 -0
  94. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.md +42 -0
  95. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.json +53 -0
  96. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.md +32 -0
  97. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.json +83 -0
  98. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.md +47 -0
  99. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.json +59 -0
  100. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.md +37 -0
  101. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.json +48 -0
  102. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.md +16 -0
  103. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.json +59 -0
  104. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.md +37 -0
  105. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.json +53 -0
  106. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.md +32 -0
  107. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.json +84 -0
  108. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.md +109 -0
  109. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.json +53 -0
  110. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.md +32 -0
  111. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
  112. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
  113. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
  114. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
  115. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
  116. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
  117. package/.trajectories/completed/2026-01/traj_v87hypnongqx.json +71 -0
  118. package/.trajectories/completed/2026-01/traj_v87hypnongqx.md +42 -0
  119. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
  120. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
  121. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.json +53 -0
  122. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.md +32 -0
  123. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.json +20 -0
  124. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.md +6 -0
  125. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.json +175 -0
  126. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.md +82 -0
  127. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
  128. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
  129. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
  130. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
  131. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.json +47 -0
  132. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.md +32 -0
  133. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.json +59 -0
  134. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.md +37 -0
  135. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.json +53 -0
  136. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.md +32 -0
  137. package/.trajectories/consolidate-settings-panel.md +24 -0
  138. package/.trajectories/gh-cli-user-token.md +26 -0
  139. package/.trajectories/index.json +468 -0
  140. package/ARCHITECTURE.md +1245 -0
  141. package/TESTING.md +278 -0
  142. package/deploy/init-db.sql +5 -0
  143. package/deploy/scripts/setup-fly-workspaces.sh +69 -0
  144. package/deploy/scripts/setup-railway.sh +75 -0
  145. package/deploy/workspace/codex.config.toml +15 -0
  146. package/deploy/workspace/entrypoint-browser.sh +118 -0
  147. package/deploy/workspace/entrypoint.sh +508 -0
  148. package/deploy/workspace/git-credential-relay +126 -0
  149. package/dist/bridge/spawner.d.ts +7 -0
  150. package/dist/bridge/spawner.js +40 -9
  151. package/dist/bridge/types.d.ts +2 -0
  152. package/dist/cli/index.js +260 -1
  153. package/dist/cloud/api/admin.d.ts +8 -0
  154. package/dist/cloud/api/admin.js +212 -0
  155. package/dist/cloud/api/auth.js +8 -0
  156. package/dist/cloud/api/billing.d.ts +0 -10
  157. package/dist/cloud/api/billing.js +278 -67
  158. package/dist/cloud/api/codex-auth-helper.d.ts +21 -0
  159. package/dist/cloud/api/codex-auth-helper.js +307 -0
  160. package/dist/cloud/api/coordinators.js +402 -0
  161. package/dist/cloud/api/daemons.js +15 -11
  162. package/dist/cloud/api/git.js +127 -19
  163. package/dist/cloud/api/github-app.js +42 -8
  164. package/dist/cloud/api/nango-auth.js +297 -16
  165. package/dist/cloud/api/onboarding.js +112 -35
  166. package/dist/cloud/api/providers.js +12 -16
  167. package/dist/cloud/api/repos.d.ts +1 -0
  168. package/dist/cloud/api/repos.js +311 -49
  169. package/dist/cloud/api/test-helpers.js +40 -0
  170. package/dist/cloud/api/usage.js +13 -0
  171. package/dist/cloud/api/webhooks.d.ts +1 -0
  172. package/dist/cloud/api/webhooks.js +149 -0
  173. package/dist/cloud/api/workspaces.d.ts +18 -0
  174. package/dist/cloud/api/workspaces.js +1042 -21
  175. package/dist/cloud/billing/plans.js +19 -19
  176. package/dist/cloud/config.d.ts +8 -0
  177. package/dist/cloud/config.js +15 -0
  178. package/dist/cloud/db/drizzle.d.ts +5 -2
  179. package/dist/cloud/db/drizzle.js +27 -20
  180. package/dist/cloud/db/schema.d.ts +19 -51
  181. package/dist/cloud/db/schema.js +5 -4
  182. package/dist/cloud/index.d.ts +0 -1
  183. package/dist/cloud/index.js +0 -1
  184. package/dist/cloud/provisioner/index.d.ts +125 -1
  185. package/dist/cloud/provisioner/index.js +939 -53
  186. package/dist/cloud/server.js +161 -16
  187. package/dist/cloud/services/compute-enforcement.d.ts +57 -0
  188. package/dist/cloud/services/compute-enforcement.js +175 -0
  189. package/dist/cloud/services/index.d.ts +2 -0
  190. package/dist/cloud/services/index.js +4 -0
  191. package/dist/cloud/services/intro-expiration.d.ts +55 -0
  192. package/dist/cloud/services/intro-expiration.js +211 -0
  193. package/dist/cloud/services/nango.d.ts +74 -0
  194. package/dist/cloud/services/nango.js +218 -5
  195. package/dist/cloud/services/planLimits.d.ts +22 -0
  196. package/dist/cloud/services/planLimits.js +58 -5
  197. package/dist/cloud/services/ssh-security.d.ts +31 -0
  198. package/dist/cloud/services/ssh-security.js +63 -0
  199. package/dist/continuity/manager.d.ts +5 -0
  200. package/dist/continuity/manager.js +56 -2
  201. package/dist/daemon/api.d.ts +2 -0
  202. package/dist/daemon/api.js +214 -5
  203. package/dist/daemon/cli-auth.d.ts +13 -1
  204. package/dist/daemon/cli-auth.js +166 -47
  205. package/dist/daemon/connection.d.ts +7 -1
  206. package/dist/daemon/connection.js +15 -0
  207. package/dist/daemon/orchestrator.d.ts +2 -0
  208. package/dist/daemon/orchestrator.js +26 -0
  209. package/dist/daemon/repo-manager.d.ts +116 -0
  210. package/dist/daemon/repo-manager.js +384 -0
  211. package/dist/daemon/router.d.ts +60 -1
  212. package/dist/daemon/router.js +281 -20
  213. package/dist/daemon/user-directory.d.ts +111 -0
  214. package/dist/daemon/user-directory.js +233 -0
  215. package/dist/dashboard/out/404.html +1 -1
  216. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +1 -0
  217. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  218. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
  219. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
  220. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
  221. package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +1 -0
  222. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +1 -0
  223. package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +1 -0
  224. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +1 -0
  225. package/dist/dashboard/out/_next/static/chunks/app/{page-daf87e86f783f980.js → page-4a5938c18a11a654.js} +1 -1
  226. package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +1 -0
  227. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +1 -0
  228. package/dist/dashboard/out/_next/static/chunks/{main-97850e03d723ea8c.js → main-2ee6beb2ae96d210.js} +1 -1
  229. package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +1 -0
  230. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
  231. package/dist/dashboard/out/app/onboarding.html +1 -0
  232. package/dist/dashboard/out/app/onboarding.txt +7 -0
  233. package/dist/dashboard/out/app.html +1 -1
  234. package/dist/dashboard/out/app.txt +3 -3
  235. package/dist/dashboard/out/apple-icon.png +0 -0
  236. package/dist/dashboard/out/connect-repos.html +1 -1
  237. package/dist/dashboard/out/connect-repos.txt +3 -3
  238. package/dist/dashboard/out/history.html +1 -1
  239. package/dist/dashboard/out/history.txt +3 -3
  240. package/dist/dashboard/out/index.html +1 -1
  241. package/dist/dashboard/out/index.txt +3 -3
  242. package/dist/dashboard/out/login.html +2 -2
  243. package/dist/dashboard/out/login.txt +3 -3
  244. package/dist/dashboard/out/metrics.html +1 -1
  245. package/dist/dashboard/out/metrics.txt +3 -3
  246. package/dist/dashboard/out/pricing.html +3 -3
  247. package/dist/dashboard/out/pricing.txt +3 -3
  248. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  249. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  250. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  251. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  252. package/dist/dashboard/out/providers.html +1 -1
  253. package/dist/dashboard/out/providers.txt +3 -3
  254. package/dist/dashboard/out/signup.html +2 -2
  255. package/dist/dashboard/out/signup.txt +3 -3
  256. package/dist/dashboard-server/server.js +316 -12
  257. package/dist/dashboard-server/user-bridge.d.ts +103 -0
  258. package/dist/dashboard-server/user-bridge.js +189 -0
  259. package/dist/protocol/channels.d.ts +205 -0
  260. package/dist/protocol/channels.js +154 -0
  261. package/dist/protocol/types.d.ts +13 -1
  262. package/dist/resiliency/provider-context.js +2 -0
  263. package/dist/shared/cli-auth-config.d.ts +19 -0
  264. package/dist/shared/cli-auth-config.js +58 -2
  265. package/dist/utils/agent-config.js +1 -1
  266. package/dist/wrapper/auth-detection.d.ts +49 -0
  267. package/dist/wrapper/auth-detection.js +192 -0
  268. package/dist/wrapper/base-wrapper.d.ts +153 -0
  269. package/dist/wrapper/base-wrapper.js +393 -0
  270. package/dist/wrapper/client.d.ts +7 -1
  271. package/dist/wrapper/client.js +3 -0
  272. package/dist/wrapper/index.d.ts +1 -0
  273. package/dist/wrapper/index.js +4 -3
  274. package/dist/wrapper/pty-wrapper.d.ts +62 -84
  275. package/dist/wrapper/pty-wrapper.js +154 -180
  276. package/dist/wrapper/tmux-wrapper.d.ts +41 -66
  277. package/dist/wrapper/tmux-wrapper.js +90 -134
  278. package/package.json +5 -12
  279. package/scripts/postinstall.js +11 -155
  280. package/scripts/test-interactive-terminal.sh +248 -0
  281. package/test-push.txt +1 -0
  282. package/bin/tmux +0 -0
  283. package/dist/bridge/config.d.ts.map +0 -1
  284. package/dist/bridge/config.js.map +0 -1
  285. package/dist/bridge/index.d.ts.map +0 -1
  286. package/dist/bridge/index.js.map +0 -1
  287. package/dist/bridge/multi-project-client.d.ts.map +0 -1
  288. package/dist/bridge/multi-project-client.js.map +0 -1
  289. package/dist/bridge/shadow-cli.d.ts.map +0 -1
  290. package/dist/bridge/shadow-cli.js.map +0 -1
  291. package/dist/bridge/shadow-config.d.ts.map +0 -1
  292. package/dist/bridge/shadow-config.js.map +0 -1
  293. package/dist/bridge/spawner.d.ts.map +0 -1
  294. package/dist/bridge/spawner.js.map +0 -1
  295. package/dist/bridge/teams-config.d.ts.map +0 -1
  296. package/dist/bridge/teams-config.js.map +0 -1
  297. package/dist/bridge/types.d.ts.map +0 -1
  298. package/dist/bridge/types.js.map +0 -1
  299. package/dist/bridge/utils.d.ts.map +0 -1
  300. package/dist/bridge/utils.js.map +0 -1
  301. package/dist/cli/index.d.ts.map +0 -1
  302. package/dist/cli/index.js.map +0 -1
  303. package/dist/cloud/api/auth.d.ts.map +0 -1
  304. package/dist/cloud/api/auth.js.map +0 -1
  305. package/dist/cloud/api/billing.d.ts.map +0 -1
  306. package/dist/cloud/api/billing.js.map +0 -1
  307. package/dist/cloud/api/cli-pty-runner.d.ts.map +0 -1
  308. package/dist/cloud/api/cli-pty-runner.js.map +0 -1
  309. package/dist/cloud/api/coordinators.d.ts.map +0 -1
  310. package/dist/cloud/api/coordinators.js.map +0 -1
  311. package/dist/cloud/api/daemons.d.ts.map +0 -1
  312. package/dist/cloud/api/daemons.js.map +0 -1
  313. package/dist/cloud/api/generic-webhooks.d.ts.map +0 -1
  314. package/dist/cloud/api/generic-webhooks.js.map +0 -1
  315. package/dist/cloud/api/git.d.ts.map +0 -1
  316. package/dist/cloud/api/git.js.map +0 -1
  317. package/dist/cloud/api/github-app.d.ts.map +0 -1
  318. package/dist/cloud/api/github-app.js.map +0 -1
  319. package/dist/cloud/api/middleware/planLimits.d.ts.map +0 -1
  320. package/dist/cloud/api/middleware/planLimits.js.map +0 -1
  321. package/dist/cloud/api/monitoring.d.ts.map +0 -1
  322. package/dist/cloud/api/monitoring.js.map +0 -1
  323. package/dist/cloud/api/nango-auth.d.ts.map +0 -1
  324. package/dist/cloud/api/nango-auth.js.map +0 -1
  325. package/dist/cloud/api/onboarding.d.ts.map +0 -1
  326. package/dist/cloud/api/onboarding.js.map +0 -1
  327. package/dist/cloud/api/policy.d.ts.map +0 -1
  328. package/dist/cloud/api/policy.js.map +0 -1
  329. package/dist/cloud/api/providers.d.ts.map +0 -1
  330. package/dist/cloud/api/providers.js.map +0 -1
  331. package/dist/cloud/api/repos.d.ts.map +0 -1
  332. package/dist/cloud/api/repos.js.map +0 -1
  333. package/dist/cloud/api/teams.d.ts.map +0 -1
  334. package/dist/cloud/api/teams.js.map +0 -1
  335. package/dist/cloud/api/test-helpers.d.ts.map +0 -1
  336. package/dist/cloud/api/test-helpers.js.map +0 -1
  337. package/dist/cloud/api/usage.d.ts.map +0 -1
  338. package/dist/cloud/api/usage.js.map +0 -1
  339. package/dist/cloud/api/webhooks.d.ts.map +0 -1
  340. package/dist/cloud/api/webhooks.js.map +0 -1
  341. package/dist/cloud/api/workspaces.d.ts.map +0 -1
  342. package/dist/cloud/api/workspaces.js.map +0 -1
  343. package/dist/cloud/billing/index.d.ts.map +0 -1
  344. package/dist/cloud/billing/index.js.map +0 -1
  345. package/dist/cloud/billing/plans.d.ts.map +0 -1
  346. package/dist/cloud/billing/plans.js.map +0 -1
  347. package/dist/cloud/billing/service.d.ts.map +0 -1
  348. package/dist/cloud/billing/service.js.map +0 -1
  349. package/dist/cloud/billing/types.d.ts.map +0 -1
  350. package/dist/cloud/billing/types.js.map +0 -1
  351. package/dist/cloud/config.d.ts.map +0 -1
  352. package/dist/cloud/config.js.map +0 -1
  353. package/dist/cloud/db/drizzle.d.ts.map +0 -1
  354. package/dist/cloud/db/drizzle.js.map +0 -1
  355. package/dist/cloud/db/index.d.ts.map +0 -1
  356. package/dist/cloud/db/index.js.map +0 -1
  357. package/dist/cloud/db/schema.d.ts.map +0 -1
  358. package/dist/cloud/db/schema.js.map +0 -1
  359. package/dist/cloud/index.d.ts.map +0 -1
  360. package/dist/cloud/index.js.map +0 -1
  361. package/dist/cloud/provisioner/index.d.ts.map +0 -1
  362. package/dist/cloud/provisioner/index.js.map +0 -1
  363. package/dist/cloud/server.d.ts.map +0 -1
  364. package/dist/cloud/server.js.map +0 -1
  365. package/dist/cloud/services/auto-scaler.d.ts.map +0 -1
  366. package/dist/cloud/services/auto-scaler.js.map +0 -1
  367. package/dist/cloud/services/capacity-manager.d.ts.map +0 -1
  368. package/dist/cloud/services/capacity-manager.js.map +0 -1
  369. package/dist/cloud/services/ci-agent-spawner.d.ts.map +0 -1
  370. package/dist/cloud/services/ci-agent-spawner.js.map +0 -1
  371. package/dist/cloud/services/coordinator.d.ts.map +0 -1
  372. package/dist/cloud/services/coordinator.js.map +0 -1
  373. package/dist/cloud/services/index.d.ts.map +0 -1
  374. package/dist/cloud/services/index.js.map +0 -1
  375. package/dist/cloud/services/mention-handler.d.ts.map +0 -1
  376. package/dist/cloud/services/mention-handler.js.map +0 -1
  377. package/dist/cloud/services/nango.d.ts.map +0 -1
  378. package/dist/cloud/services/nango.js.map +0 -1
  379. package/dist/cloud/services/persistence.d.ts.map +0 -1
  380. package/dist/cloud/services/persistence.js.map +0 -1
  381. package/dist/cloud/services/planLimits.d.ts.map +0 -1
  382. package/dist/cloud/services/planLimits.js.map +0 -1
  383. package/dist/cloud/services/scaling-orchestrator.d.ts.map +0 -1
  384. package/dist/cloud/services/scaling-orchestrator.js.map +0 -1
  385. package/dist/cloud/services/scaling-policy.d.ts.map +0 -1
  386. package/dist/cloud/services/scaling-policy.js.map +0 -1
  387. package/dist/cloud/vault/index.d.ts +0 -76
  388. package/dist/cloud/vault/index.d.ts.map +0 -1
  389. package/dist/cloud/vault/index.js +0 -219
  390. package/dist/cloud/vault/index.js.map +0 -1
  391. package/dist/cloud/webhooks/index.d.ts.map +0 -1
  392. package/dist/cloud/webhooks/index.js.map +0 -1
  393. package/dist/cloud/webhooks/parsers/github.d.ts.map +0 -1
  394. package/dist/cloud/webhooks/parsers/github.js.map +0 -1
  395. package/dist/cloud/webhooks/parsers/index.d.ts.map +0 -1
  396. package/dist/cloud/webhooks/parsers/index.js.map +0 -1
  397. package/dist/cloud/webhooks/parsers/linear.d.ts.map +0 -1
  398. package/dist/cloud/webhooks/parsers/linear.js.map +0 -1
  399. package/dist/cloud/webhooks/parsers/slack.d.ts.map +0 -1
  400. package/dist/cloud/webhooks/parsers/slack.js.map +0 -1
  401. package/dist/cloud/webhooks/responders/github.d.ts.map +0 -1
  402. package/dist/cloud/webhooks/responders/github.js.map +0 -1
  403. package/dist/cloud/webhooks/responders/index.d.ts.map +0 -1
  404. package/dist/cloud/webhooks/responders/index.js.map +0 -1
  405. package/dist/cloud/webhooks/responders/linear.d.ts.map +0 -1
  406. package/dist/cloud/webhooks/responders/linear.js.map +0 -1
  407. package/dist/cloud/webhooks/responders/slack.d.ts.map +0 -1
  408. package/dist/cloud/webhooks/responders/slack.js.map +0 -1
  409. package/dist/cloud/webhooks/router.d.ts.map +0 -1
  410. package/dist/cloud/webhooks/router.js.map +0 -1
  411. package/dist/cloud/webhooks/rules-engine.d.ts.map +0 -1
  412. package/dist/cloud/webhooks/rules-engine.js.map +0 -1
  413. package/dist/cloud/webhooks/types.d.ts.map +0 -1
  414. package/dist/cloud/webhooks/types.js.map +0 -1
  415. package/dist/continuity/formatter.d.ts.map +0 -1
  416. package/dist/continuity/formatter.js.map +0 -1
  417. package/dist/continuity/handoff-store.d.ts.map +0 -1
  418. package/dist/continuity/handoff-store.js.map +0 -1
  419. package/dist/continuity/index.d.ts.map +0 -1
  420. package/dist/continuity/index.js.map +0 -1
  421. package/dist/continuity/ledger-store.d.ts.map +0 -1
  422. package/dist/continuity/ledger-store.js.map +0 -1
  423. package/dist/continuity/manager.d.ts.map +0 -1
  424. package/dist/continuity/manager.js.map +0 -1
  425. package/dist/continuity/parser.d.ts.map +0 -1
  426. package/dist/continuity/parser.js.map +0 -1
  427. package/dist/continuity/types.d.ts.map +0 -1
  428. package/dist/continuity/types.js.map +0 -1
  429. package/dist/daemon/agent-manager.d.ts.map +0 -1
  430. package/dist/daemon/agent-manager.js.map +0 -1
  431. package/dist/daemon/agent-registry.d.ts.map +0 -1
  432. package/dist/daemon/agent-registry.js.map +0 -1
  433. package/dist/daemon/api.d.ts.map +0 -1
  434. package/dist/daemon/api.js.map +0 -1
  435. package/dist/daemon/auth.d.ts.map +0 -1
  436. package/dist/daemon/auth.js.map +0 -1
  437. package/dist/daemon/cli-auth.d.ts.map +0 -1
  438. package/dist/daemon/cli-auth.js.map +0 -1
  439. package/dist/daemon/cloud-sync.d.ts.map +0 -1
  440. package/dist/daemon/cloud-sync.js.map +0 -1
  441. package/dist/daemon/connection.d.ts.map +0 -1
  442. package/dist/daemon/connection.js.map +0 -1
  443. package/dist/daemon/index.d.ts.map +0 -1
  444. package/dist/daemon/index.js.map +0 -1
  445. package/dist/daemon/orchestrator.d.ts.map +0 -1
  446. package/dist/daemon/orchestrator.js.map +0 -1
  447. package/dist/daemon/registry.d.ts.map +0 -1
  448. package/dist/daemon/registry.js.map +0 -1
  449. package/dist/daemon/router.d.ts.map +0 -1
  450. package/dist/daemon/router.js.map +0 -1
  451. package/dist/daemon/server.d.ts.map +0 -1
  452. package/dist/daemon/server.js.map +0 -1
  453. package/dist/daemon/services/browser-testing.d.ts.map +0 -1
  454. package/dist/daemon/services/browser-testing.js.map +0 -1
  455. package/dist/daemon/services/container-spawner.d.ts.map +0 -1
  456. package/dist/daemon/services/container-spawner.js.map +0 -1
  457. package/dist/daemon/types.d.ts.map +0 -1
  458. package/dist/daemon/types.js.map +0 -1
  459. package/dist/daemon/workspace-manager.d.ts.map +0 -1
  460. package/dist/daemon/workspace-manager.js.map +0 -1
  461. package/dist/dashboard/out/_next/static/H5aWG0udPB4iOUIl_gytz/_ssgManifest.js +0 -1
  462. package/dist/dashboard/out/_next/static/chunks/480-2d4111711d4e473c.js +0 -1
  463. package/dist/dashboard/out/_next/static/chunks/724-73c1ee5f60abe860.js +0 -9
  464. package/dist/dashboard/out/_next/static/chunks/766-c3a14283c88d815b.js +0 -1
  465. package/dist/dashboard/out/_next/static/chunks/app/app/page-7120be68bea622f3.js +0 -1
  466. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1081dd190a331a91.js +0 -1
  467. package/dist/dashboard/out/_next/static/chunks/app/providers/page-b68a681526eb145e.js +0 -1
  468. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
  469. package/dist/dashboard/out/_next/static/css/411ce23ffeae9f76.css +0 -1
  470. package/dist/dashboard-server/metrics.d.ts.map +0 -1
  471. package/dist/dashboard-server/metrics.js.map +0 -1
  472. package/dist/dashboard-server/needs-attention.d.ts.map +0 -1
  473. package/dist/dashboard-server/needs-attention.js.map +0 -1
  474. package/dist/dashboard-server/server.d.ts.map +0 -1
  475. package/dist/dashboard-server/server.js.map +0 -1
  476. package/dist/dashboard-server/start.d.ts.map +0 -1
  477. package/dist/dashboard-server/start.js.map +0 -1
  478. package/dist/hooks/emitter.d.ts.map +0 -1
  479. package/dist/hooks/emitter.js.map +0 -1
  480. package/dist/hooks/inbox-check/hook.d.ts.map +0 -1
  481. package/dist/hooks/inbox-check/hook.js.map +0 -1
  482. package/dist/hooks/inbox-check/index.d.ts.map +0 -1
  483. package/dist/hooks/inbox-check/index.js.map +0 -1
  484. package/dist/hooks/inbox-check/types.d.ts.map +0 -1
  485. package/dist/hooks/inbox-check/types.js.map +0 -1
  486. package/dist/hooks/inbox-check/utils.d.ts.map +0 -1
  487. package/dist/hooks/inbox-check/utils.js.map +0 -1
  488. package/dist/hooks/index.d.ts.map +0 -1
  489. package/dist/hooks/index.js.map +0 -1
  490. package/dist/hooks/registry.d.ts.map +0 -1
  491. package/dist/hooks/registry.js.map +0 -1
  492. package/dist/hooks/trajectory-hooks.d.ts.map +0 -1
  493. package/dist/hooks/trajectory-hooks.js.map +0 -1
  494. package/dist/hooks/types.d.ts.map +0 -1
  495. package/dist/hooks/types.js.map +0 -1
  496. package/dist/index.d.ts.map +0 -1
  497. package/dist/index.js.map +0 -1
  498. package/dist/memory/adapters/index.d.ts.map +0 -1
  499. package/dist/memory/adapters/index.js.map +0 -1
  500. package/dist/memory/adapters/inmemory.d.ts.map +0 -1
  501. package/dist/memory/adapters/inmemory.js.map +0 -1
  502. package/dist/memory/adapters/supermemory.d.ts.map +0 -1
  503. package/dist/memory/adapters/supermemory.js.map +0 -1
  504. package/dist/memory/factory.d.ts.map +0 -1
  505. package/dist/memory/factory.js.map +0 -1
  506. package/dist/memory/index.d.ts.map +0 -1
  507. package/dist/memory/index.js.map +0 -1
  508. package/dist/memory/memory-hooks.d.ts.map +0 -1
  509. package/dist/memory/memory-hooks.js.map +0 -1
  510. package/dist/memory/service.d.ts.map +0 -1
  511. package/dist/memory/service.js.map +0 -1
  512. package/dist/memory/types.d.ts.map +0 -1
  513. package/dist/memory/types.js.map +0 -1
  514. package/dist/policy/agent-policy.d.ts.map +0 -1
  515. package/dist/policy/agent-policy.js.map +0 -1
  516. package/dist/policy/cloud-policy-fetcher.d.ts.map +0 -1
  517. package/dist/policy/cloud-policy-fetcher.js.map +0 -1
  518. package/dist/protocol/framing.d.ts.map +0 -1
  519. package/dist/protocol/framing.js.map +0 -1
  520. package/dist/protocol/index.d.ts.map +0 -1
  521. package/dist/protocol/index.js.map +0 -1
  522. package/dist/protocol/types.d.ts.map +0 -1
  523. package/dist/protocol/types.js.map +0 -1
  524. package/dist/resiliency/context-persistence.d.ts.map +0 -1
  525. package/dist/resiliency/context-persistence.js.map +0 -1
  526. package/dist/resiliency/crash-insights.d.ts.map +0 -1
  527. package/dist/resiliency/crash-insights.js.map +0 -1
  528. package/dist/resiliency/gossip-health.d.ts.map +0 -1
  529. package/dist/resiliency/gossip-health.js.map +0 -1
  530. package/dist/resiliency/health-monitor.d.ts.map +0 -1
  531. package/dist/resiliency/health-monitor.js.map +0 -1
  532. package/dist/resiliency/index.d.ts.map +0 -1
  533. package/dist/resiliency/index.js.map +0 -1
  534. package/dist/resiliency/leader-watchdog.d.ts.map +0 -1
  535. package/dist/resiliency/leader-watchdog.js.map +0 -1
  536. package/dist/resiliency/logger.d.ts.map +0 -1
  537. package/dist/resiliency/logger.js.map +0 -1
  538. package/dist/resiliency/memory-monitor.d.ts.map +0 -1
  539. package/dist/resiliency/memory-monitor.js.map +0 -1
  540. package/dist/resiliency/metrics.d.ts.map +0 -1
  541. package/dist/resiliency/metrics.js.map +0 -1
  542. package/dist/resiliency/provider-context.d.ts.map +0 -1
  543. package/dist/resiliency/provider-context.js.map +0 -1
  544. package/dist/resiliency/stateless-lead.d.ts.map +0 -1
  545. package/dist/resiliency/stateless-lead.js.map +0 -1
  546. package/dist/resiliency/supervisor.d.ts.map +0 -1
  547. package/dist/resiliency/supervisor.js.map +0 -1
  548. package/dist/shared/cli-auth-config.d.ts.map +0 -1
  549. package/dist/shared/cli-auth-config.js.map +0 -1
  550. package/dist/state/agent-state.d.ts.map +0 -1
  551. package/dist/state/agent-state.js.map +0 -1
  552. package/dist/storage/adapter.d.ts.map +0 -1
  553. package/dist/storage/adapter.js.map +0 -1
  554. package/dist/storage/sqlite-adapter.d.ts.map +0 -1
  555. package/dist/storage/sqlite-adapter.js.map +0 -1
  556. package/dist/trajectory/config.d.ts.map +0 -1
  557. package/dist/trajectory/config.js.map +0 -1
  558. package/dist/trajectory/index.d.ts.map +0 -1
  559. package/dist/trajectory/index.js.map +0 -1
  560. package/dist/trajectory/integration.d.ts.map +0 -1
  561. package/dist/trajectory/integration.js.map +0 -1
  562. package/dist/utils/agent-config.d.ts.map +0 -1
  563. package/dist/utils/agent-config.js.map +0 -1
  564. package/dist/utils/command-resolver.d.ts.map +0 -1
  565. package/dist/utils/command-resolver.js.map +0 -1
  566. package/dist/utils/index.d.ts.map +0 -1
  567. package/dist/utils/index.js.map +0 -1
  568. package/dist/utils/logger.d.ts.map +0 -1
  569. package/dist/utils/logger.js.map +0 -1
  570. package/dist/utils/name-generator.d.ts.map +0 -1
  571. package/dist/utils/name-generator.js.map +0 -1
  572. package/dist/utils/project-namespace.d.ts.map +0 -1
  573. package/dist/utils/project-namespace.js.map +0 -1
  574. package/dist/utils/tmux-resolver.d.ts.map +0 -1
  575. package/dist/utils/tmux-resolver.js.map +0 -1
  576. package/dist/utils/update-checker.d.ts.map +0 -1
  577. package/dist/utils/update-checker.js.map +0 -1
  578. package/dist/wrapper/client.d.ts.map +0 -1
  579. package/dist/wrapper/client.js.map +0 -1
  580. package/dist/wrapper/inbox.d.ts.map +0 -1
  581. package/dist/wrapper/inbox.js.map +0 -1
  582. package/dist/wrapper/index.d.ts.map +0 -1
  583. package/dist/wrapper/index.js.map +0 -1
  584. package/dist/wrapper/parser.d.ts.map +0 -1
  585. package/dist/wrapper/parser.js.map +0 -1
  586. package/dist/wrapper/pty-wrapper.d.ts.map +0 -1
  587. package/dist/wrapper/pty-wrapper.js.map +0 -1
  588. package/dist/wrapper/shared.d.ts.map +0 -1
  589. package/dist/wrapper/shared.js.map +0 -1
  590. package/dist/wrapper/tmux-wrapper.d.ts.map +0 -1
  591. package/dist/wrapper/tmux-wrapper.js.map +0 -1
  592. package/docs/AGENTS.md +0 -513
  593. package/docs/ARCHITECTURE_DECISIONS.md +0 -175
  594. package/docs/CLOUD-ARCHITECTURE.md +0 -804
  595. package/docs/CLOUD-ONBOARDING-DESIGN.md +0 -1983
  596. package/docs/CONTRIBUTING.md +0 -151
  597. package/docs/HOOKS_API.md +0 -394
  598. package/docs/INTEGRATION-GUIDE.md +0 -926
  599. package/docs/PROTOCOL.md +0 -325
  600. package/docs/WRAPPER_EVENTS.md +0 -358
  601. package/docs/agent-policy-snippet.md +0 -40
  602. package/docs/agent-relay-protocol.md +0 -238
  603. package/docs/agent-relay-snippet.md +0 -174
  604. package/docs/archive/CHANGELOG.md +0 -11
  605. package/docs/archive/CLI-SIMPLIFICATION-COMPLETE.md +0 -48
  606. package/docs/archive/DESIGN_BRIDGE_STAFFING.md +0 -878
  607. package/docs/archive/DESIGN_V2.md +0 -1079
  608. package/docs/archive/EXECUTIVE_SUMMARY.md +0 -358
  609. package/docs/archive/MONETIZATION.md +0 -1679
  610. package/docs/archive/PROPOSAL-trajectories.md +0 -1582
  611. package/docs/archive/ROADMAP.md +0 -329
  612. package/docs/archive/SCALING_ANALYSIS.md +0 -280
  613. package/docs/archive/TESTING_PRESENCE_FEATURES.md +0 -327
  614. package/docs/archive/TMUX_IMPLEMENTATION_NOTES.md +0 -364
  615. package/docs/archive/TMUX_IMPROVEMENTS.md +0 -968
  616. package/docs/archive/dashboard-v2-plan.md +0 -179
  617. package/docs/archive/removable-code-analysis.md +0 -24
  618. package/docs/competitive/GASTOWN.md +0 -451
  619. package/docs/competitive/MCP_AGENT_MAIL.md +0 -389
  620. package/docs/competitive/OVERVIEW.md +0 -898
  621. package/docs/competitive/README.md +0 -34
  622. package/docs/competitive/TMUX_ORCHESTRATOR.md +0 -605
  623. package/docs/dashboard.png +0 -0
  624. package/docs/design/ci-failure-webhooks.md +0 -812
  625. package/docs/design/comprehensive-integrations.md +0 -238
  626. package/docs/design/e2b-sandbox-integration.md +0 -504
  627. package/docs/design/github-app-permissions.md +0 -264
  628. package/docs/guides/CLOUD.md +0 -236
  629. package/docs/guides/LOCAL.md +0 -535
  630. package/docs/guides/SELF-HOSTED.md +0 -494
  631. package/docs/local-testing.md +0 -428
  632. package/docs/proposals/continuous-claude-integration.md +0 -622
  633. package/docs/proposals/custom-commands.md +0 -368
  634. package/docs/proposals/shadow-as-subagent.md +0 -765
  635. package/docs/proposals/slack-bot-integration.md +0 -1457
  636. package/docs/tasks/global-skills-system.tasks.md +0 -230
  637. package/docs/tasks/webhook-integrations.tasks.md +0 -184
  638. package/docs/tasks/workspace-capabilities.tasks.md +0 -121
  639. package/docs/testing/RESILIENCY-TEST-PLAN-2026-01-01.md +0 -366
  640. package/scripts/cloud-setup.sh +0 -96
  641. package/scripts/dev/PUBLIC_RELEASE_PLAN.md +0 -88
  642. package/scripts/dev/dev-team-setup.sh +0 -431
  643. package/scripts/e2e-test.sh +0 -119
  644. package/scripts/games/game-protocol.md +0 -79
  645. package/scripts/games/hearts-setup.sh +0 -264
  646. package/scripts/manual-qa.sh +0 -293
  647. package/scripts/run-cloud-qa.sh +0 -220
  648. package/scripts/test-cli-auth/Dockerfile +0 -44
  649. package/scripts/test-cli-auth/Dockerfile.real +0 -79
  650. package/scripts/test-cli-auth/README.md +0 -286
  651. package/scripts/test-cli-auth/ci-test-real-clis.ts +0 -251
  652. package/scripts/test-cli-auth/ci-test-runner.ts +0 -263
  653. package/scripts/test-cli-auth/mock-cli.sh +0 -147
  654. package/scripts/test-cli-auth/package.json +0 -14
  655. package/scripts/test-cli-auth/test-oauth-flow.ts +0 -220
  656. package/scripts/test-pty-input-auto.js +0 -222
  657. package/scripts/test-pty-input.js +0 -150
  658. package/scripts/tictactoe-setup.sh +0 -181
  659. /package/dist/dashboard/out/_next/static/{H5aWG0udPB4iOUIl_gytz → T1tgCqVWHFIkV7ClEtzD7}/_buildManifest.js +0 -0
  660. /package/dist/dashboard/out/_next/static/chunks/{117-b100311aff8d5c61.js → 117-f7b8ab0809342e77.js} +0 -0
  661. /package/dist/dashboard/out/_next/static/chunks/{648-a13d3c2b1be45466.js → 648-5cc6e1921389a58a.js} +0 -0
  662. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-a4973f3e3c82fb67.js → page-53b8a69f76db17d0.js} +0 -0
  663. /package/dist/dashboard/out/_next/static/chunks/app/connect-repos/{page-dc2e3a1a22478efc.js → page-f45ecbc3e06134fc.js} +0 -0
  664. /package/dist/dashboard/out/_next/static/chunks/app/history/{page-56a8b4616a90dc43.js → page-8c8bed33beb2bf1c.js} +0 -0
  665. /package/dist/dashboard/out/_next/static/chunks/app/login/{page-3eac37ea6f5dd153.js → page-16f3b49e55b1e0ed.js} +0 -0
  666. /package/dist/dashboard/out/_next/static/chunks/app/pricing/{page-4d72d5a5d8a9b618.js → page-982a7000fee44014.js} +0 -0
  667. /package/dist/dashboard/out/_next/static/chunks/app/signup/{page-fee4ed1709070bcd.js → page-547dd0ca55ecd0ba.js} +0 -0
  668. /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-bf46c09eb57e019c.js → fd9d1056-609918ca7b6280bb.js} +0 -0
@@ -6,11 +6,49 @@
6
6
  import * as crypto from 'crypto';
7
7
  import { getConfig } from '../config.js';
8
8
  import { db } from '../db/index.js';
9
- import { vault } from '../vault/index.js';
10
9
  import { nangoService } from '../services/nango.js';
10
+ import { canAutoScale, canScaleToTier, getResourceTierForPlan, } from '../services/planLimits.js';
11
+ import { deriveSshPassword } from '../services/ssh-security.js';
11
12
  const WORKSPACE_PORT = 3888;
13
+ const CODEX_OAUTH_PORT = 1455; // Codex CLI OAuth callback port - must be mapped for local dev
12
14
  const FETCH_TIMEOUT_MS = 10_000;
13
15
  const WORKSPACE_IMAGE = process.env.WORKSPACE_IMAGE || 'ghcr.io/agentworkforce/relay-workspace:latest';
16
+ // In-memory tracker for provisioning progress (workspace ID -> progress)
17
+ const provisioningProgress = new Map();
18
+ /**
19
+ * Update the provisioning stage for a workspace
20
+ */
21
+ function updateProvisioningStage(workspaceId, stage) {
22
+ const existing = provisioningProgress.get(workspaceId);
23
+ provisioningProgress.set(workspaceId, {
24
+ stage,
25
+ startedAt: existing?.startedAt ?? Date.now(),
26
+ updatedAt: Date.now(),
27
+ });
28
+ console.log(`[provisioner] Workspace ${workspaceId.substring(0, 8)} stage: ${stage}`);
29
+ }
30
+ /**
31
+ * Get the current provisioning stage for a workspace
32
+ */
33
+ export function getProvisioningStage(workspaceId) {
34
+ return provisioningProgress.get(workspaceId) ?? null;
35
+ }
36
+ /**
37
+ * Clear provisioning progress (call when complete or failed)
38
+ */
39
+ function clearProvisioningProgress(workspaceId) {
40
+ provisioningProgress.delete(workspaceId);
41
+ }
42
+ /**
43
+ * Schedule cleanup of provisioning progress after a delay
44
+ * This gives the frontend time to poll and see the 'complete' stage
45
+ */
46
+ function scheduleProgressCleanup(workspaceId, delayMs = 30_000) {
47
+ setTimeout(() => {
48
+ clearProvisioningProgress(workspaceId);
49
+ console.log(`[provisioner] Cleaned up provisioning progress for ${workspaceId.substring(0, 8)}`);
50
+ }, delayMs);
51
+ }
14
52
  /**
15
53
  * Get a fresh GitHub App installation token from Nango.
16
54
  * Looks up the user's connected repositories to find a valid Nango connection.
@@ -33,20 +71,6 @@ async function getGithubAppTokenForUser(userId) {
33
71
  return null;
34
72
  }
35
73
  }
36
- async function loadCredentialToken(userId, provider) {
37
- try {
38
- const cred = await vault.getCredential(userId, provider);
39
- if (cred?.accessToken) {
40
- return cred.accessToken;
41
- }
42
- }
43
- catch (error) {
44
- console.warn(`Failed to decrypt ${provider} credential from vault; trying raw storage fallback`, error);
45
- const raw = await db.credentials.findByUserAndProvider(userId, provider);
46
- return raw?.accessToken ?? null;
47
- }
48
- return null;
49
- }
50
74
  async function wait(ms) {
51
75
  return new Promise((resolve) => setTimeout(resolve, ms));
52
76
  }
@@ -88,11 +112,108 @@ async function softHealthCheck(url) {
88
112
  console.warn(`[health] Failed to reach ${url}/health`, error);
89
113
  }
90
114
  }
115
+ /**
116
+ * Wait for machine to be in "started" state using Fly.io's /wait endpoint
117
+ * This is more efficient than polling - the API blocks until the state is reached
118
+ * @see https://fly.io/docs/machines/api/machines-resource/#wait-for-a-machine-to-reach-a-specific-state
119
+ */
120
+ async function waitForMachineStarted(apiToken, appName, machineId, timeoutSeconds = 120) {
121
+ console.log(`[provisioner] Waiting for machine ${machineId} to start (timeout: ${timeoutSeconds}s)...`);
122
+ // Fly.io /wait endpoint has max timeout of 60s, so we need to loop for longer waits
123
+ const maxSingleWait = 60;
124
+ const startTime = Date.now();
125
+ const deadline = startTime + timeoutSeconds * 1000;
126
+ while (Date.now() < deadline) {
127
+ const remainingMs = deadline - Date.now();
128
+ const waitSeconds = Math.min(maxSingleWait, Math.ceil(remainingMs / 1000));
129
+ if (waitSeconds <= 0)
130
+ break;
131
+ try {
132
+ // Use Fly.io's /wait endpoint - blocks until machine reaches target state
133
+ // timeout is an integer in seconds (max 60)
134
+ const res = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines/${machineId}/wait?state=started&timeout=${waitSeconds}`, {
135
+ headers: { Authorization: `Bearer ${apiToken}` },
136
+ });
137
+ if (res.ok) {
138
+ console.log(`[provisioner] Machine ${machineId} is now started`);
139
+ return;
140
+ }
141
+ // 408 = timeout, machine didn't reach state in time - try again if we have time
142
+ if (res.status === 408) {
143
+ console.log(`[provisioner] Machine ${machineId} not ready yet, continuing to wait...`);
144
+ continue;
145
+ }
146
+ // Other error
147
+ const errorText = await res.text();
148
+ throw new Error(`Wait for machine failed: ${res.status} ${errorText}`);
149
+ }
150
+ catch (error) {
151
+ if (error instanceof Error && error.message.includes('Wait for machine failed')) {
152
+ throw error;
153
+ }
154
+ console.warn(`[provisioner] Error waiting for machine:`, error);
155
+ throw new Error(`Failed to wait for machine ${machineId}: ${error.message}`);
156
+ }
157
+ }
158
+ // Timeout reached - get current state for error message
159
+ const stateRes = await fetch(`https://api.machines.dev/v1/apps/${appName}/machines/${machineId}`, { headers: { Authorization: `Bearer ${apiToken}` } });
160
+ const machine = stateRes.ok ? (await stateRes.json()) : { state: 'unknown' };
161
+ throw new Error(`Machine ${machineId} did not start within ${timeoutSeconds}s (last state: ${machine.state})`);
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
91
212
  export const RESOURCE_TIERS = {
92
- small: { name: 'small', cpuCores: 1, memoryMb: 512, maxAgents: 5 },
93
- medium: { name: 'medium', cpuCores: 2, memoryMb: 1024, maxAgents: 10 },
94
- large: { name: 'large', cpuCores: 4, memoryMb: 2048, maxAgents: 20 },
95
- xlarge: { name: 'xlarge', cpuCores: 8, memoryMb: 4096, maxAgents: 50 },
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' },
96
217
  };
97
218
  /**
98
219
  * Fly.io provisioner
@@ -105,6 +226,8 @@ class FlyProvisioner {
105
226
  cloudApiUrl;
106
227
  sessionSecret;
107
228
  registryAuth;
229
+ snapshotRetentionDays;
230
+ volumeSizeGb;
108
231
  constructor() {
109
232
  const config = getConfig();
110
233
  if (!config.compute.fly) {
@@ -117,6 +240,9 @@ class FlyProvisioner {
117
240
  this.registryAuth = config.compute.fly.registryAuth;
118
241
  this.cloudApiUrl = config.publicUrl;
119
242
  this.sessionSecret = config.sessionSecret;
243
+ // Snapshot settings: default 14 days retention, 10GB volume
244
+ this.snapshotRetentionDays = Math.min(60, Math.max(1, config.compute.fly.snapshotRetentionDays ?? 14));
245
+ this.volumeSizeGb = config.compute.fly.volumeSizeGb ?? 10;
120
246
  }
121
247
  /**
122
248
  * Generate a workspace token for API authentication
@@ -128,8 +254,91 @@ class FlyProvisioner {
128
254
  .update(`workspace:${workspaceId}`)
129
255
  .digest('hex');
130
256
  }
257
+ /**
258
+ * Create a volume with automatic snapshot settings
259
+ * Fly.io takes daily snapshots automatically; we configure retention
260
+ */
261
+ async createVolume(appName) {
262
+ const volumeName = 'workspace_data';
263
+ console.log(`[fly] Creating volume ${volumeName} with ${this.snapshotRetentionDays}-day snapshot retention...`);
264
+ const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/volumes`, {
265
+ method: 'POST',
266
+ headers: {
267
+ Authorization: `Bearer ${this.apiToken}`,
268
+ 'Content-Type': 'application/json',
269
+ },
270
+ body: JSON.stringify({
271
+ name: volumeName,
272
+ region: this.region,
273
+ size_gb: this.volumeSizeGb,
274
+ // Enable automatic daily snapshots (default is true, but be explicit)
275
+ auto_backup_enabled: true,
276
+ // Retain snapshots for configured days (default 5, we use 14)
277
+ snapshot_retention: this.snapshotRetentionDays,
278
+ }),
279
+ });
280
+ if (!response.ok) {
281
+ const error = await response.text();
282
+ throw new Error(`Failed to create volume: ${error}`);
283
+ }
284
+ const volume = await response.json();
285
+ console.log(`[fly] Volume ${volume.id} created with auto-snapshots (${this.snapshotRetentionDays} days retention)`);
286
+ return volume;
287
+ }
288
+ /**
289
+ * Create an on-demand snapshot of a workspace volume
290
+ * Use before risky operations or as manual backup
291
+ */
292
+ async createSnapshot(appName, volumeId) {
293
+ console.log(`[fly] Creating on-demand snapshot for volume ${volumeId}...`);
294
+ const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/volumes/${volumeId}/snapshots`, {
295
+ method: 'POST',
296
+ headers: {
297
+ Authorization: `Bearer ${this.apiToken}`,
298
+ 'Content-Type': 'application/json',
299
+ },
300
+ });
301
+ if (!response.ok) {
302
+ const error = await response.text();
303
+ throw new Error(`Failed to create snapshot: ${error}`);
304
+ }
305
+ const snapshot = await response.json();
306
+ console.log(`[fly] Snapshot ${snapshot.id} created`);
307
+ return snapshot;
308
+ }
309
+ /**
310
+ * List snapshots for a workspace volume
311
+ */
312
+ async listSnapshots(appName, volumeId) {
313
+ const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/volumes/${volumeId}/snapshots`, {
314
+ headers: {
315
+ Authorization: `Bearer ${this.apiToken}`,
316
+ },
317
+ });
318
+ if (!response.ok) {
319
+ return [];
320
+ }
321
+ return await response.json();
322
+ }
323
+ /**
324
+ * Get volume info for a workspace
325
+ */
326
+ async getVolume(appName) {
327
+ const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/volumes`, {
328
+ headers: {
329
+ Authorization: `Bearer ${this.apiToken}`,
330
+ },
331
+ });
332
+ if (!response.ok) {
333
+ return null;
334
+ }
335
+ const volumes = await response.json();
336
+ return volumes.find(v => v.name === 'workspace_data') || null;
337
+ }
131
338
  async provision(workspace, credentials) {
132
339
  const appName = `ar-${workspace.id.substring(0, 8)}`;
340
+ // Stage: Creating workspace
341
+ updateProvisioningStage(workspace.id, 'creating');
133
342
  // Create Fly app
134
343
  await fetchWithRetry('https://api.machines.dev/v1/apps', {
135
344
  method: 'POST',
@@ -142,10 +351,82 @@ class FlyProvisioner {
142
351
  org_slug: this.org,
143
352
  }),
144
353
  });
354
+ // Stage: Networking
355
+ updateProvisioningStage(workspace.id, 'networking');
356
+ // Allocate IPs for the app (required for public DNS)
357
+ // Must use GraphQL API - Machines REST API doesn't support IP allocation
358
+ // Shared IPv4 is free, IPv6 is free
359
+ console.log(`[fly] Allocating IPs for ${appName}...`);
360
+ const allocateIP = async (type) => {
361
+ try {
362
+ // Map our type to Fly GraphQL enum
363
+ const graphqlType = type === 'shared_v4' ? 'shared_v4' : 'v6';
364
+ const res = await fetchWithRetry('https://api.fly.io/graphql', {
365
+ method: 'POST',
366
+ headers: {
367
+ Authorization: `Bearer ${this.apiToken}`,
368
+ 'Content-Type': 'application/json',
369
+ },
370
+ body: JSON.stringify({
371
+ query: `
372
+ mutation AllocateIPAddress($input: AllocateIPAddressInput!) {
373
+ allocateIpAddress(input: $input) {
374
+ ipAddress {
375
+ id
376
+ address
377
+ type
378
+ }
379
+ }
380
+ }
381
+ `,
382
+ variables: {
383
+ input: {
384
+ appId: appName,
385
+ type: graphqlType,
386
+ },
387
+ },
388
+ }),
389
+ });
390
+ if (!res.ok) {
391
+ const errorText = await res.text();
392
+ console.warn(`[fly] Failed to allocate ${type}: ${res.status} ${errorText}`);
393
+ return false;
394
+ }
395
+ const data = await res.json();
396
+ if (data.errors?.length) {
397
+ // Ignore "already allocated" errors
398
+ const alreadyAllocated = data.errors.some(e => e.message.includes('already') || e.message.includes('exists'));
399
+ if (!alreadyAllocated) {
400
+ console.warn(`[fly] GraphQL error allocating ${type}: ${data.errors[0].message}`);
401
+ return false;
402
+ }
403
+ console.log(`[fly] IP ${type} already allocated`);
404
+ return true;
405
+ }
406
+ const address = data.data?.allocateIpAddress?.ipAddress?.address;
407
+ console.log(`[fly] Allocated ${type}: ${address}`);
408
+ return true;
409
+ }
410
+ catch (err) {
411
+ console.warn(`[fly] Failed to allocate ${type}: ${err.message}`);
412
+ return false;
413
+ }
414
+ };
415
+ const [sharedV4Result, v6Result] = await Promise.all([
416
+ allocateIP('shared_v4'),
417
+ allocateIP('v6'),
418
+ ]);
419
+ console.log(`[fly] IP allocation results: shared_v4=${sharedV4Result}, v6=${v6Result}`);
420
+ // Stage: Secrets
421
+ updateProvisioningStage(workspace.id, 'secrets');
145
422
  // Set secrets (provider credentials)
146
423
  const secrets = {};
147
424
  for (const [provider, token] of credentials) {
148
425
  secrets[`${provider.toUpperCase()}_TOKEN`] = token;
426
+ // Also set GH_TOKEN for gh CLI compatibility
427
+ if (provider === 'github') {
428
+ secrets['GH_TOKEN'] = token;
429
+ }
149
430
  }
150
431
  if (Object.keys(secrets).length > 0) {
151
432
  await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/secrets`, {
@@ -164,6 +445,33 @@ class FlyProvisioner {
164
445
  if (customHostname) {
165
446
  await this.allocateCertificate(appName, customHostname);
166
447
  }
448
+ // Stage: Machine (includes volume creation)
449
+ updateProvisioningStage(workspace.id, 'machine');
450
+ // Create volume with automatic daily snapshots before machine
451
+ // Fly.io takes daily snapshots automatically; we configure retention
452
+ const volume = await this.createVolume(appName);
453
+ // Determine instance size based on user's plan
454
+ // Free tier: 1 CPU, 2GB (~$10/mo) - Claude needs 2GB minimum
455
+ // Paid tiers: 2 CPU, 2GB (~$15/mo)
456
+ // Introductory bonus: Free users get Pro-level resources for first 14 days
457
+ const user = await db.users.findById(workspace.userId);
458
+ const userPlan = user?.plan || 'free';
459
+ const isFreeTier = userPlan === 'free';
460
+ // Check if user is in introductory period (first 14 days)
461
+ const INTRO_PERIOD_DAYS = 14;
462
+ const userCreatedAt = user?.createdAt ? new Date(user.createdAt) : new Date();
463
+ const daysSinceSignup = (Date.now() - userCreatedAt.getTime()) / (1000 * 60 * 60 * 24);
464
+ const isIntroPeriod = isFreeTier && daysSinceSignup < INTRO_PERIOD_DAYS;
465
+ const guestConfig = {
466
+ cpu_kind: 'shared',
467
+ cpus: isIntroPeriod ? 2 : (isFreeTier ? 1 : 2), // Intro gets 2 CPUs like Pro
468
+ memory_mb: isIntroPeriod ? 4096 : 2048, // Intro gets 4GB like Pro
469
+ };
470
+ if (isIntroPeriod) {
471
+ const daysRemaining = Math.ceil(INTRO_PERIOD_DAYS - daysSinceSignup);
472
+ console.log(`[fly] Introductory bonus active (${daysRemaining} days remaining) - 2 CPU / 4GB`);
473
+ }
474
+ console.log(`[fly] Using ${guestConfig.cpus} CPU / ${guestConfig.memory_mb}MB for ${userPlan} plan`);
167
475
  // Create machine with auto-stop/start for cost optimization
168
476
  const machineResponse = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines`, {
169
477
  method: 'POST',
@@ -185,6 +493,7 @@ class FlyProvisioner {
185
493
  }),
186
494
  env: {
187
495
  WORKSPACE_ID: workspace.id,
496
+ WORKSPACE_OWNER_USER_ID: workspace.userId,
188
497
  SUPERVISOR_ENABLED: String(workspace.config.supervisorEnabled ?? false),
189
498
  MAX_AGENTS: String(workspace.config.maxAgents ?? 10),
190
499
  REPOSITORIES: (workspace.config.repositories ?? []).join(','),
@@ -194,26 +503,76 @@ class FlyProvisioner {
194
503
  // Git gateway configuration
195
504
  CLOUD_API_URL: this.cloudApiUrl,
196
505
  WORKSPACE_TOKEN: this.generateWorkspaceToken(workspace.id),
506
+ // SSH for CLI tunneling (Codex OAuth callback forwarding)
507
+ // Each workspace gets a unique password derived from its ID + secret salt
508
+ ENABLE_SSH: 'true',
509
+ SSH_PASSWORD: deriveSshPassword(workspace.id),
197
510
  },
198
511
  services: [
199
512
  {
200
513
  ports: [
201
- { port: 443, handlers: ['tls', 'http'] },
514
+ {
515
+ port: 443,
516
+ handlers: ['tls', 'http'],
517
+ // Force HTTP/1.1 to backend for WebSocket upgrade compatibility
518
+ // HTTP/2 doesn't support traditional WebSocket upgrade mechanism
519
+ http_options: {
520
+ h2_backend: false,
521
+ },
522
+ },
202
523
  { port: 80, handlers: ['http'] },
203
524
  ],
204
525
  protocol: 'tcp',
205
526
  internal_port: WORKSPACE_PORT,
206
- // Auto-stop after 5 minutes of inactivity
207
- auto_stop_machines: true,
527
+ // Auto-stop after inactivity to reduce costs
528
+ // Fly Proxy automatically wakes machines on incoming requests
529
+ auto_stop_machines: 'stop', // stop (not suspend) for faster wake
530
+ auto_start_machines: true,
531
+ min_machines_running: 0,
532
+ // Idle timeout before auto-stop (in seconds)
533
+ // Longer timeout = better UX, shorter = lower costs
534
+ concurrency: {
535
+ type: 'requests',
536
+ soft_limit: 25,
537
+ hard_limit: 50,
538
+ },
539
+ },
540
+ // SSH service for CLI tunneling (Codex OAuth callback forwarding)
541
+ // Exposes port 2222 publicly for SSH connections from user's machine
542
+ {
543
+ ports: [
544
+ {
545
+ port: 2222,
546
+ handlers: [], // Empty handlers = raw TCP passthrough
547
+ },
548
+ ],
549
+ protocol: 'tcp',
550
+ internal_port: 2222,
551
+ // SSH connections should also wake the machine
552
+ auto_stop_machines: 'stop',
208
553
  auto_start_machines: true,
209
554
  min_machines_running: 0,
210
555
  },
211
556
  ],
212
- guest: {
213
- cpu_kind: 'shared',
214
- cpus: 1,
215
- memory_mb: 512,
557
+ checks: {
558
+ health: {
559
+ type: 'http',
560
+ port: WORKSPACE_PORT,
561
+ path: '/health',
562
+ interval: '30s',
563
+ timeout: '5s',
564
+ grace_period: '10s',
565
+ },
216
566
  },
567
+ // Instance size based on plan - free tier gets smaller instance
568
+ guest: guestConfig,
569
+ // Mount the volume we created with snapshot settings
570
+ mounts: [
571
+ {
572
+ volume: volume.id,
573
+ path: '/data',
574
+ },
575
+ ],
217
576
  },
218
577
  }),
219
578
  });
@@ -226,7 +585,19 @@ class FlyProvisioner {
226
585
  const publicUrl = customHostname
227
586
  ? `https://${customHostname}`
228
587
  : `https://${appName}.fly.dev`;
229
- await softHealthCheck(publicUrl);
588
+ // Stage: Booting
589
+ updateProvisioningStage(workspace.id, 'booting');
590
+ // Wait for machine to be in started state
591
+ await waitForMachineStarted(this.apiToken, appName, machine.id);
592
+ // Stage: Health check
593
+ updateProvisioningStage(workspace.id, 'health');
594
+ // Wait for health check to pass (includes DNS propagation time)
595
+ // Pass appName to enable internal Fly network health checks
596
+ await waitForHealthy(publicUrl, appName);
597
+ // Stage: Complete
598
+ updateProvisioningStage(workspace.id, 'complete');
599
+ // Schedule cleanup of provisioning progress after 30s (gives frontend time to see 'complete')
600
+ scheduleProgressCleanup(workspace.id);
230
601
  return {
231
602
  computeId: machine.id,
232
603
  publicUrl,
@@ -298,13 +669,24 @@ class FlyProvisioner {
298
669
  }
299
670
  /**
300
671
  * Resize workspace - vertical scaling via Fly Machines API
672
+ * @param skipRestart - If true, config is saved but machine won't restart (changes apply on next start)
301
673
  */
302
- async resize(workspace, tier) {
303
- if (!workspace.computeId)
304
- return;
305
- const appName = `ar-${workspace.id.substring(0, 8)}`;
674
+ async resize(workspaceOrId, tier, skipRestart = false) {
675
+ const workspaceId = typeof workspaceOrId === 'string' ? workspaceOrId : workspaceOrId.id;
676
+ const computeId = typeof workspaceOrId === 'string' ? undefined : workspaceOrId.computeId;
677
+ // If passed just an ID, look up the workspace
678
+ let machineId = computeId;
679
+ if (!machineId) {
680
+ const workspace = await db.workspaces.findById(workspaceId);
681
+ if (!workspace?.computeId)
682
+ return;
683
+ machineId = workspace.computeId;
684
+ }
685
+ const appName = `ar-${workspaceId.substring(0, 8)}`;
306
686
  // Update machine configuration
307
- await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}`, {
687
+ // If running: reboots with new specs (unless skip_launch: true)
688
+ // If stopped: config saved, applies on next start
689
+ await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${machineId}`, {
308
690
  method: 'POST',
309
691
  headers: {
310
692
  Authorization: `Bearer ${this.apiToken}`,
@@ -313,7 +695,8 @@ class FlyProvisioner {
313
695
  body: JSON.stringify({
314
696
  config: {
315
697
  guest: {
316
- cpu_kind: tier.cpuCores <= 2 ? 'shared' : 'performance',
698
+ // Use tier-specific CPU type (shared for cost, performance for power)
699
+ cpu_kind: tier.cpuKind,
317
700
  cpus: tier.cpuCores,
318
701
  memory_mb: tier.memoryMb,
319
702
  },
@@ -321,9 +704,11 @@ class FlyProvisioner {
321
704
  MAX_AGENTS: String(tier.maxAgents),
322
705
  },
323
706
  },
707
+ skip_launch: skipRestart, // If true, don't restart - changes apply on next start
324
708
  }),
325
709
  });
326
- console.log(`[fly] Resized workspace ${workspace.id} to ${tier.name} (${tier.cpuCores} CPU, ${tier.memoryMb}MB RAM)`);
710
+ const restartNote = skipRestart ? ' (will apply on next restart)' : ' (restarting)';
711
+ console.log(`[fly] Resized workspace ${workspaceId.substring(0, 8)} to ${tier.name} (${tier.cpuCores} CPU, ${tier.memoryMb}MB RAM)${restartNote}`);
327
712
  }
328
713
  /**
329
714
  * Update the max agent limit for a workspace
@@ -377,6 +762,118 @@ class FlyProvisioner {
377
762
  return RESOURCE_TIERS.medium;
378
763
  return RESOURCE_TIERS.small;
379
764
  }
765
+ /**
766
+ * Update machine image without restarting
767
+ * Note: The machine needs to be restarted later to use the new image
768
+ */
769
+ async updateMachineImage(workspace, newImage) {
770
+ if (!workspace.computeId)
771
+ return;
772
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
773
+ // Get current machine config first
774
+ const getResponse = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}`, {
775
+ headers: {
776
+ Authorization: `Bearer ${this.apiToken}`,
777
+ },
778
+ });
779
+ if (!getResponse.ok) {
780
+ throw new Error(`Failed to get machine config: ${await getResponse.text()}`);
781
+ }
782
+ const machine = await getResponse.json();
783
+ // Update the image in the config
784
+ const updatedConfig = {
785
+ ...machine.config,
786
+ image: newImage,
787
+ // Include registry auth if configured
788
+ ...(this.registryAuth && {
789
+ image_registry_auth: {
790
+ registry: 'ghcr.io',
791
+ username: this.registryAuth.username,
792
+ password: this.registryAuth.password,
793
+ },
794
+ }),
795
+ };
796
+ // Update machine with new image config (skip_launch keeps it in current state)
797
+ const updateResponse = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}?skip_launch=true`, {
798
+ method: 'POST',
799
+ headers: {
800
+ Authorization: `Bearer ${this.apiToken}`,
801
+ 'Content-Type': 'application/json',
802
+ },
803
+ body: JSON.stringify({ config: updatedConfig }),
804
+ });
805
+ if (!updateResponse.ok) {
806
+ throw new Error(`Failed to update machine image: ${await updateResponse.text()}`);
807
+ }
808
+ console.log(`[fly] Updated machine image for workspace ${workspace.id.substring(0, 8)} to ${newImage}`);
809
+ }
810
+ /**
811
+ * Check if workspace has active agents by querying the daemon
812
+ */
813
+ async checkActiveAgents(workspace) {
814
+ if (!workspace.publicUrl) {
815
+ return { hasActiveAgents: false, agentCount: 0, agents: [] };
816
+ }
817
+ try {
818
+ // Use internal Fly network URL if available (more reliable)
819
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
820
+ const isOnFly = !!process.env.FLY_APP_NAME;
821
+ const baseUrl = isOnFly
822
+ ? `http://${appName}.internal:3888`
823
+ : workspace.publicUrl;
824
+ const controller = new AbortController();
825
+ const timer = setTimeout(() => controller.abort(), 10_000);
826
+ const response = await fetch(`${baseUrl}/api/agents`, {
827
+ method: 'GET',
828
+ headers: {
829
+ 'Accept': 'application/json',
830
+ },
831
+ signal: controller.signal,
832
+ });
833
+ clearTimeout(timer);
834
+ if (!response.ok) {
835
+ console.warn(`[fly] Failed to check agents for ${workspace.id.substring(0, 8)}: ${response.status}`);
836
+ return { hasActiveAgents: false, agentCount: 0, agents: [] };
837
+ }
838
+ const data = await response.json();
839
+ const agents = data.agents || [];
840
+ // Consider agents with 'active' or 'idle' activity state as active
841
+ // 'disconnected' agents are not active
842
+ const activeAgents = agents.filter(a => a.status === 'running' || a.activityState === 'active' || a.activityState === 'idle');
843
+ return {
844
+ hasActiveAgents: activeAgents.length > 0,
845
+ agentCount: activeAgents.length,
846
+ agents: agents.map(a => ({ name: a.name, status: a.status || a.activityState || 'unknown' })),
847
+ };
848
+ }
849
+ catch (error) {
850
+ // Workspace might be stopped or unreachable - treat as no active agents
851
+ console.warn(`[fly] Could not reach workspace ${workspace.id.substring(0, 8)} to check agents:`, error.message);
852
+ return { hasActiveAgents: false, agentCount: 0, agents: [] };
853
+ }
854
+ }
855
+ /**
856
+ * Get the current machine state
857
+ */
858
+ async getMachineState(workspace) {
859
+ if (!workspace.computeId)
860
+ return 'unknown';
861
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
862
+ try {
863
+ const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}`, {
864
+ headers: {
865
+ Authorization: `Bearer ${this.apiToken}`,
866
+ },
867
+ });
868
+ if (!response.ok)
869
+ return 'unknown';
870
+ const machine = await response.json();
871
+ return machine.state;
872
+ }
873
+ catch {
874
+ return 'unknown';
875
+ }
876
+ }
380
877
  }
381
878
  /**
382
879
  * Railway provisioner
@@ -457,6 +954,7 @@ class RailwayProvisioner {
457
954
  // Set environment variables
458
955
  const envVars = {
459
956
  WORKSPACE_ID: workspace.id,
957
+ WORKSPACE_OWNER_USER_ID: workspace.userId,
460
958
  SUPERVISOR_ENABLED: String(workspace.config.supervisorEnabled ?? false),
461
959
  MAX_AGENTS: String(workspace.config.maxAgents ?? 10),
462
960
  REPOSITORIES: (workspace.config.repositories ?? []).join(','),
@@ -468,6 +966,10 @@ class RailwayProvisioner {
468
966
  };
469
967
  for (const [provider, token] of credentials) {
470
968
  envVars[`${provider.toUpperCase()}_TOKEN`] = token;
969
+ // Also set GH_TOKEN for gh CLI compatibility
970
+ if (provider === 'github') {
971
+ envVars['GH_TOKEN'] = token;
972
+ }
471
973
  }
472
974
  await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
473
975
  method: 'POST',
@@ -671,6 +1173,7 @@ class DockerProvisioner {
671
1173
  // Build environment variables
672
1174
  const envArgs = [
673
1175
  `-e WORKSPACE_ID=${workspace.id}`,
1176
+ `-e WORKSPACE_OWNER_USER_ID=${workspace.userId}`,
674
1177
  `-e SUPERVISOR_ENABLED=${workspace.config.supervisorEnabled ?? false}`,
675
1178
  `-e MAX_AGENTS=${workspace.config.maxAgents ?? 10}`,
676
1179
  `-e REPOSITORIES=${(workspace.config.repositories ?? []).join(',')}`,
@@ -682,10 +1185,17 @@ class DockerProvisioner {
682
1185
  ];
683
1186
  for (const [provider, token] of credentials) {
684
1187
  envArgs.push(`-e ${provider.toUpperCase()}_TOKEN=${token}`);
1188
+ // Also set GH_TOKEN for gh CLI compatibility
1189
+ if (provider === 'github') {
1190
+ envArgs.push(`-e GH_TOKEN=${token}`);
1191
+ }
685
1192
  }
686
1193
  // Run container
687
1194
  const { execSync } = await import('child_process');
688
1195
  const hostPort = 3000 + Math.floor(Math.random() * 1000);
1196
+ // SSH port for tunneling (Codex OAuth callback forwarding)
1197
+ // Derive from hostPort to avoid collisions: API port 3500 -> SSH port 22500
1198
+ const sshHostPort = 22000 + (hostPort - 3000);
689
1199
  // When running in Docker, connect to the same network for container-to-container communication
690
1200
  const runningInDocker = process.env.RUNNING_IN_DOCKER === 'true';
691
1201
  const networkArg = runningInDocker ? '--network agent-relay-dev' : '';
@@ -699,7 +1209,18 @@ class DockerProvisioner {
699
1209
  console.log('[provisioner] Dev mode: mounting local dist/ and docs/ folders into workspace container');
700
1210
  }
701
1211
  try {
702
- execSync(`docker run -d --user root --name ${containerName} ${networkArg} ${volumeArgs} -p ${hostPort}:${WORKSPACE_PORT} ${envArgs.join(' ')} ${WORKSPACE_IMAGE}`, { stdio: 'pipe' });
1212
+ // Map workspace API port and SSH port (for tunneling)
1213
+ // SSH is used by CLI to forward localhost:1455 to workspace container for Codex OAuth
1214
+ // Set CODEX_DIRECT_PORT=true to also map port 1455 directly (for debugging only)
1215
+ const directCodexPort = process.env.CODEX_DIRECT_PORT === 'true';
1216
+ const portMappings = directCodexPort
1217
+ ? `-p ${hostPort}:${WORKSPACE_PORT} -p ${sshHostPort}:2222 -p ${CODEX_OAUTH_PORT}:${CODEX_OAUTH_PORT}`
1218
+ : `-p ${hostPort}:${WORKSPACE_PORT} -p ${sshHostPort}:2222`;
1219
+ // Enable SSH in the container for tunneling
1220
+ // Each workspace gets a unique password derived from its ID + secret salt
1221
+ envArgs.push('-e ENABLE_SSH=true');
1222
+ envArgs.push(`-e SSH_PASSWORD=${deriveSshPassword(workspace.id)}`);
1223
+ execSync(`docker run -d --user root --name ${containerName} ${networkArg} ${volumeArgs} ${portMappings} ${envArgs.join(' ')} ${WORKSPACE_IMAGE}`, { stdio: 'pipe' });
703
1224
  const publicUrl = `http://localhost:${hostPort}`;
704
1225
  // Wait for container to be healthy before returning
705
1226
  // When running in Docker, use the internal container name for health check
@@ -710,6 +1231,7 @@ class DockerProvisioner {
710
1231
  return {
711
1232
  computeId: containerName,
712
1233
  publicUrl,
1234
+ sshPort: sshHostPort,
713
1235
  };
714
1236
  }
715
1237
  catch (error) {
@@ -791,6 +1313,7 @@ export class WorkspaceProvisioner {
791
1313
  }
792
1314
  /**
793
1315
  * Provision a new workspace (one-click)
1316
+ * Returns immediately with 'provisioning' status and runs actual provisioning in background
794
1317
  */
795
1318
  async provision(config) {
796
1319
  // Create workspace record
@@ -814,14 +1337,58 @@ export class WorkspaceProvisioner {
814
1337
  });
815
1338
  // Auto-accept the creator's membership
816
1339
  await db.workspaceMembers.acceptInvite(workspace.id, config.userId);
817
- // Get credentials
818
- const credentials = new Map();
819
- for (const provider of config.providers) {
820
- const token = await loadCredentialToken(config.userId, provider);
821
- if (token) {
822
- credentials.set(provider, token);
1340
+ // Link repositories to this workspace
1341
+ // This enables auto-access for users with GitHub access to these repos
1342
+ for (const repoFullName of config.repositories) {
1343
+ try {
1344
+ // Find the user's repo record (may not exist if user didn't import it first)
1345
+ const userRepos = await db.repositories.findByUserId(config.userId);
1346
+ const repoRecord = userRepos.find(r => r.githubFullName.toLowerCase() === repoFullName.toLowerCase());
1347
+ if (repoRecord) {
1348
+ await db.repositories.assignToWorkspace(repoRecord.id, workspace.id);
1349
+ console.log(`[provisioner] Linked repo ${repoFullName} to workspace ${workspace.id.substring(0, 8)}`);
1350
+ }
1351
+ else {
1352
+ // Create a placeholder repo record if it doesn't exist
1353
+ // This ensures the repo is tracked for workspace access checks
1354
+ console.log(`[provisioner] Creating repo record for ${repoFullName}`);
1355
+ const newRepo = await db.repositories.upsert({
1356
+ userId: config.userId,
1357
+ githubFullName: repoFullName,
1358
+ githubId: 0, // Will be updated when actually synced
1359
+ defaultBranch: 'main',
1360
+ isPrivate: true, // Assume private, will be updated
1361
+ workspaceId: workspace.id,
1362
+ });
1363
+ console.log(`[provisioner] Created and linked repo ${repoFullName} (id: ${newRepo.id.substring(0, 8)})`);
1364
+ }
1365
+ }
1366
+ catch (err) {
1367
+ console.warn(`[provisioner] Failed to link repo ${repoFullName}:`, err);
1368
+ // Continue with other repos
823
1369
  }
824
1370
  }
1371
+ // Initialize stage tracking immediately
1372
+ updateProvisioningStage(workspace.id, 'creating');
1373
+ // Run provisioning in the background so frontend can poll for stages
1374
+ this.runProvisioningAsync(workspace, config).catch((error) => {
1375
+ console.error(`[provisioner] Background provisioning failed for ${workspace.id}:`, error);
1376
+ });
1377
+ // Return immediately with 'provisioning' status
1378
+ return {
1379
+ workspaceId: workspace.id,
1380
+ status: 'provisioning',
1381
+ };
1382
+ }
1383
+ /**
1384
+ * Run the actual provisioning work asynchronously
1385
+ */
1386
+ async runProvisioningAsync(workspace, config) {
1387
+ // Build credentials map for workspace provisioning
1388
+ // Note: Provider tokens (Claude, Codex, etc.) are no longer stored centrally.
1389
+ // CLI tools authenticate directly on workspace instances.
1390
+ // Only GitHub App tokens are obtained from Nango for repository cloning.
1391
+ const credentials = new Map();
825
1392
  // GitHub token is required for cloning repositories
826
1393
  // Use direct token if provided (for testing), otherwise get from Nango
827
1394
  if (config.repositories.length > 0) {
@@ -848,22 +1415,21 @@ export class WorkspaceProvisioner {
848
1415
  computeId,
849
1416
  publicUrl,
850
1417
  });
851
- return {
852
- workspaceId: workspace.id,
853
- status: 'running',
854
- publicUrl,
855
- };
1418
+ // Schedule cleanup of provisioning progress after 30s (gives frontend time to see 'complete')
1419
+ setTimeout(() => {
1420
+ clearProvisioningProgress(workspace.id);
1421
+ console.log(`[provisioner] Cleaned up provisioning progress for ${workspace.id.substring(0, 8)}`);
1422
+ }, 30_000);
1423
+ console.log(`[provisioner] Workspace ${workspace.id} provisioned successfully at ${publicUrl}`);
856
1424
  }
857
1425
  catch (error) {
858
1426
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
859
1427
  await db.workspaces.updateStatus(workspace.id, 'error', {
860
1428
  errorMessage,
861
1429
  });
862
- return {
863
- workspaceId: workspace.id,
864
- status: 'error',
865
- error: errorMessage,
866
- };
1430
+ // Clear provisioning progress on error
1431
+ clearProvisioningProgress(workspace.id);
1432
+ console.error(`[provisioner] Workspace ${workspace.id} provisioning failed:`, errorMessage);
867
1433
  }
868
1434
  }
869
1435
  /**
@@ -885,6 +1451,11 @@ export class WorkspaceProvisioner {
885
1451
  if (!workspace) {
886
1452
  throw new Error('Workspace not found');
887
1453
  }
1454
+ // During early provisioning, computeId isn't set yet
1455
+ // Return the database status instead of querying the provider
1456
+ if (!workspace.computeId && workspace.status === 'provisioning') {
1457
+ return 'provisioning';
1458
+ }
888
1459
  const status = await this.provisioner.getStatus(workspace);
889
1460
  // Update database if status changed
890
1461
  if (status !== workspace.status) {
@@ -916,8 +1487,9 @@ export class WorkspaceProvisioner {
916
1487
  }
917
1488
  /**
918
1489
  * Resize a workspace (vertical scaling)
1490
+ * @param skipRestart - If true, config is saved but machine won't restart (changes apply on next start)
919
1491
  */
920
- async resize(workspaceId, tier) {
1492
+ async resize(workspaceId, tier, skipRestart = false) {
921
1493
  const workspace = await db.workspaces.findById(workspaceId);
922
1494
  if (!workspace) {
923
1495
  throw new Error('Workspace not found');
@@ -925,7 +1497,7 @@ export class WorkspaceProvisioner {
925
1497
  if (!this.provisioner.resize) {
926
1498
  throw new Error('Resize not supported by current compute provider');
927
1499
  }
928
- await this.provisioner.resize(workspace, tier);
1500
+ await this.provisioner.resize(workspace, tier, skipRestart);
929
1501
  // Update workspace config with new limits
930
1502
  await db.workspaces.updateConfig(workspaceId, {
931
1503
  ...workspace.config,
@@ -965,6 +1537,320 @@ export class WorkspaceProvisioner {
965
1537
  const tierName = workspace.config.resourceTier || 'small';
966
1538
  return RESOURCE_TIERS[tierName] || RESOURCE_TIERS.small;
967
1539
  }
1540
+ /**
1541
+ * Get recommended tier based on agent count
1542
+ * Uses 1.5-2GB per agent as baseline for Claude Code
1543
+ */
1544
+ getRecommendedTier(agentCount) {
1545
+ // Find the smallest tier that supports this agent count
1546
+ const tiers = Object.values(RESOURCE_TIERS).sort((a, b) => a.maxAgents - b.maxAgents);
1547
+ for (const tier of tiers) {
1548
+ if (tier.maxAgents >= agentCount) {
1549
+ return tier;
1550
+ }
1551
+ }
1552
+ // If agent count exceeds all tiers, return the largest
1553
+ return RESOURCE_TIERS.xlarge;
1554
+ }
1555
+ /**
1556
+ * Auto-scale workspace based on current agent count
1557
+ * Respects plan limits - free tier cannot scale, others have max tier limits
1558
+ * Returns { scaled: boolean, reason?: string }
1559
+ */
1560
+ async autoScale(workspaceId, currentAgentCount) {
1561
+ const workspace = await db.workspaces.findById(workspaceId);
1562
+ if (!workspace) {
1563
+ throw new Error('Workspace not found');
1564
+ }
1565
+ // Get user's plan
1566
+ const user = await db.users.findById(workspace.userId);
1567
+ const plan = user?.plan || 'free';
1568
+ // Check if plan allows auto-scaling
1569
+ if (!canAutoScale(plan)) {
1570
+ return {
1571
+ scaled: false,
1572
+ reason: 'Auto-scaling requires Pro plan or higher',
1573
+ };
1574
+ }
1575
+ const currentTier = await this.getCurrentTier(workspaceId);
1576
+ const recommendedTier = this.getRecommendedTier(currentAgentCount);
1577
+ // Only scale UP, never down (to avoid disruption)
1578
+ if (recommendedTier.memoryMb <= currentTier.memoryMb) {
1579
+ return {
1580
+ scaled: false,
1581
+ currentTier: currentTier.name,
1582
+ };
1583
+ }
1584
+ // Check if plan allows scaling to the recommended tier
1585
+ if (!canScaleToTier(plan, recommendedTier.name)) {
1586
+ // Find the max tier allowed for this plan
1587
+ const maxTierName = getResourceTierForPlan(plan);
1588
+ const maxTier = RESOURCE_TIERS[maxTierName];
1589
+ if (maxTier.memoryMb <= currentTier.memoryMb) {
1590
+ return {
1591
+ scaled: false,
1592
+ reason: `Already at max tier (${currentTier.name}) for ${plan} plan`,
1593
+ currentTier: currentTier.name,
1594
+ };
1595
+ }
1596
+ // Scale to max allowed tier instead
1597
+ console.log(`[provisioner] Auto-scaling workspace ${workspaceId.substring(0, 8)} from ${currentTier.name} to ${maxTierName} (max for ${plan} plan)`);
1598
+ await this.resize(workspaceId, maxTier);
1599
+ return {
1600
+ scaled: true,
1601
+ currentTier: currentTier.name,
1602
+ targetTier: maxTierName,
1603
+ reason: `Scaled to max tier for ${plan} plan`,
1604
+ };
1605
+ }
1606
+ console.log(`[provisioner] Auto-scaling workspace ${workspaceId.substring(0, 8)} from ${currentTier.name} to ${recommendedTier.name} (${currentAgentCount} agents)`);
1607
+ await this.resize(workspaceId, recommendedTier);
1608
+ return {
1609
+ scaled: true,
1610
+ currentTier: currentTier.name,
1611
+ targetTier: recommendedTier.name,
1612
+ };
1613
+ }
1614
+ // ============================================================================
1615
+ // Snapshot Management
1616
+ // ============================================================================
1617
+ /**
1618
+ * Create an on-demand snapshot of a workspace's volume
1619
+ * Use before risky operations (e.g., major refactors, untrusted code execution)
1620
+ */
1621
+ async createSnapshot(workspaceId) {
1622
+ const workspace = await db.workspaces.findById(workspaceId);
1623
+ if (!workspace) {
1624
+ throw new Error('Workspace not found');
1625
+ }
1626
+ // Only Fly.io provisioner supports snapshots
1627
+ if (!(this.provisioner instanceof FlyProvisioner)) {
1628
+ console.warn('[provisioner] Snapshots only supported on Fly.io');
1629
+ return null;
1630
+ }
1631
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
1632
+ const flyProvisioner = this.provisioner;
1633
+ // Get the volume
1634
+ const volume = await flyProvisioner.getVolume(appName);
1635
+ if (!volume) {
1636
+ throw new Error('No volume found for workspace');
1637
+ }
1638
+ // Create snapshot
1639
+ const snapshot = await flyProvisioner.createSnapshot(appName, volume.id);
1640
+ return { snapshotId: snapshot.id };
1641
+ }
1642
+ /**
1643
+ * List available snapshots for a workspace
1644
+ * Includes both automatic daily snapshots and on-demand snapshots
1645
+ */
1646
+ async listSnapshots(workspaceId) {
1647
+ const workspace = await db.workspaces.findById(workspaceId);
1648
+ if (!workspace) {
1649
+ throw new Error('Workspace not found');
1650
+ }
1651
+ // Only Fly.io provisioner supports snapshots
1652
+ if (!(this.provisioner instanceof FlyProvisioner)) {
1653
+ return [];
1654
+ }
1655
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
1656
+ const flyProvisioner = this.provisioner;
1657
+ // Get the volume
1658
+ const volume = await flyProvisioner.getVolume(appName);
1659
+ if (!volume) {
1660
+ return [];
1661
+ }
1662
+ // List snapshots
1663
+ const snapshots = await flyProvisioner.listSnapshots(appName, volume.id);
1664
+ return snapshots.map(s => ({
1665
+ id: s.id,
1666
+ createdAt: s.created_at,
1667
+ sizeBytes: s.size,
1668
+ }));
1669
+ }
1670
+ /**
1671
+ * Get the volume ID for a workspace (needed for restore operations)
1672
+ */
1673
+ async getVolumeId(workspaceId) {
1674
+ const workspace = await db.workspaces.findById(workspaceId);
1675
+ if (!workspace) {
1676
+ throw new Error('Workspace not found');
1677
+ }
1678
+ if (!(this.provisioner instanceof FlyProvisioner)) {
1679
+ return null;
1680
+ }
1681
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
1682
+ const flyProvisioner = this.provisioner;
1683
+ const volume = await flyProvisioner.getVolume(appName);
1684
+ return volume?.id || null;
1685
+ }
1686
+ // ============================================================================
1687
+ // Graceful Image Update
1688
+ // ============================================================================
1689
+ /**
1690
+ * Result of a graceful update attempt
1691
+ */
1692
+ static UpdateResult = {
1693
+ UPDATED: 'updated',
1694
+ UPDATED_PENDING_RESTART: 'updated_pending_restart',
1695
+ SKIPPED_ACTIVE_AGENTS: 'skipped_active_agents',
1696
+ SKIPPED_NOT_RUNNING: 'skipped_not_running',
1697
+ NOT_SUPPORTED: 'not_supported',
1698
+ ERROR: 'error',
1699
+ };
1700
+ /**
1701
+ * Gracefully update a single workspace's image
1702
+ *
1703
+ * Behavior:
1704
+ * - If workspace is stopped: Update config, will use new image on next wake
1705
+ * - If workspace is running with no agents: Update config and restart
1706
+ * - If workspace is running with active agents: Skip (or force if specified)
1707
+ *
1708
+ * @param workspaceId - Workspace to update
1709
+ * @param newImage - New Docker image to use
1710
+ * @param options - Update options
1711
+ * @returns Update result with details
1712
+ */
1713
+ async gracefulUpdateImage(workspaceId, newImage, options = {}) {
1714
+ const workspace = await db.workspaces.findById(workspaceId);
1715
+ if (!workspace) {
1716
+ return {
1717
+ result: WorkspaceProvisioner.UpdateResult.ERROR,
1718
+ workspaceId,
1719
+ error: 'Workspace not found',
1720
+ };
1721
+ }
1722
+ // Only Fly.io supports graceful updates
1723
+ if (!(this.provisioner instanceof FlyProvisioner)) {
1724
+ return {
1725
+ result: WorkspaceProvisioner.UpdateResult.NOT_SUPPORTED,
1726
+ workspaceId,
1727
+ error: 'Graceful updates only supported on Fly.io',
1728
+ };
1729
+ }
1730
+ const flyProvisioner = this.provisioner;
1731
+ try {
1732
+ // Check machine state
1733
+ const machineState = await flyProvisioner.getMachineState(workspace);
1734
+ if (machineState === 'stopped' || machineState === 'suspended') {
1735
+ // Machine is not running - safe to update, will apply on next wake
1736
+ await flyProvisioner.updateMachineImage(workspace, newImage);
1737
+ console.log(`[provisioner] Updated stopped workspace ${workspaceId.substring(0, 8)} to ${newImage}`);
1738
+ return {
1739
+ result: WorkspaceProvisioner.UpdateResult.UPDATED_PENDING_RESTART,
1740
+ workspaceId,
1741
+ machineState,
1742
+ };
1743
+ }
1744
+ if (machineState === 'started') {
1745
+ // Machine is running - check for active agents
1746
+ const agentCheck = await flyProvisioner.checkActiveAgents(workspace);
1747
+ if (agentCheck.hasActiveAgents && !options.force) {
1748
+ // Has active agents and not forcing - skip
1749
+ console.log(`[provisioner] Skipped workspace ${workspaceId.substring(0, 8)}: ${agentCheck.agentCount} active agents`);
1750
+ return {
1751
+ result: WorkspaceProvisioner.UpdateResult.SKIPPED_ACTIVE_AGENTS,
1752
+ workspaceId,
1753
+ machineState,
1754
+ agentCount: agentCheck.agentCount,
1755
+ agents: agentCheck.agents,
1756
+ };
1757
+ }
1758
+ // Update the image config
1759
+ await flyProvisioner.updateMachineImage(workspace, newImage);
1760
+ if (options.skipRestart) {
1761
+ // Config updated but not restarting - will apply on next restart/auto-stop-wake
1762
+ console.log(`[provisioner] Updated workspace ${workspaceId.substring(0, 8)} config (restart skipped)`);
1763
+ return {
1764
+ result: WorkspaceProvisioner.UpdateResult.UPDATED_PENDING_RESTART,
1765
+ workspaceId,
1766
+ machineState,
1767
+ agentCount: agentCheck.agentCount,
1768
+ agents: agentCheck.agents,
1769
+ };
1770
+ }
1771
+ // Restart to apply new image
1772
+ await flyProvisioner.restart(workspace);
1773
+ console.log(`[provisioner] Updated and restarted workspace ${workspaceId.substring(0, 8)}`);
1774
+ return {
1775
+ result: WorkspaceProvisioner.UpdateResult.UPDATED,
1776
+ workspaceId,
1777
+ machineState,
1778
+ agentCount: agentCheck.agentCount,
1779
+ };
1780
+ }
1781
+ // Unknown state
1782
+ return {
1783
+ result: WorkspaceProvisioner.UpdateResult.SKIPPED_NOT_RUNNING,
1784
+ workspaceId,
1785
+ machineState,
1786
+ };
1787
+ }
1788
+ catch (error) {
1789
+ console.error(`[provisioner] Error updating workspace ${workspaceId.substring(0, 8)}:`, error);
1790
+ return {
1791
+ result: WorkspaceProvisioner.UpdateResult.ERROR,
1792
+ workspaceId,
1793
+ error: error.message,
1794
+ };
1795
+ }
1796
+ }
1797
+ /**
1798
+ * Gracefully update all workspaces to a new image
1799
+ *
1800
+ * Processes workspaces in batches, respecting active agents unless forced.
1801
+ * Returns detailed results for each workspace.
1802
+ *
1803
+ * @param newImage - New Docker image to use
1804
+ * @param options - Update options
1805
+ * @returns Summary and per-workspace results
1806
+ */
1807
+ async gracefulUpdateAllImages(newImage, options = {}) {
1808
+ // Get all workspaces to update
1809
+ let workspaces;
1810
+ if (options.workspaceIds?.length) {
1811
+ // Specific workspaces
1812
+ workspaces = (await Promise.all(options.workspaceIds.map(id => db.workspaces.findById(id)))).filter((w) => w !== null);
1813
+ }
1814
+ else if (options.userIds?.length) {
1815
+ // Workspaces for specific users
1816
+ const allWorkspaces = await Promise.all(options.userIds.map(userId => db.workspaces.findByUserId(userId)));
1817
+ workspaces = allWorkspaces.flat();
1818
+ }
1819
+ else {
1820
+ // All workspaces - need to query by status to get running ones
1821
+ // For now, we'll get all workspaces from the provisioning provider
1822
+ workspaces = await db.workspaces.findAll();
1823
+ }
1824
+ // Filter to only Fly.io workspaces
1825
+ workspaces = workspaces.filter(w => w.computeProvider === 'fly' && w.computeId);
1826
+ console.log(`[provisioner] Starting graceful update of ${workspaces.length} workspaces to ${newImage}`);
1827
+ const batchSize = options.batchSize ?? 5;
1828
+ const results = [];
1829
+ // Process in batches
1830
+ for (let i = 0; i < workspaces.length; i += batchSize) {
1831
+ const batch = workspaces.slice(i, i + batchSize);
1832
+ const batchResults = await Promise.all(batch.map(workspace => this.gracefulUpdateImage(workspace.id, newImage, {
1833
+ force: options.force,
1834
+ skipRestart: options.skipRestart,
1835
+ })));
1836
+ results.push(...batchResults);
1837
+ // Small delay between batches to avoid overwhelming Fly API
1838
+ if (i + batchSize < workspaces.length) {
1839
+ await wait(1000);
1840
+ }
1841
+ }
1842
+ // Compute summary
1843
+ const summary = {
1844
+ total: results.length,
1845
+ updated: results.filter(r => r.result === WorkspaceProvisioner.UpdateResult.UPDATED).length,
1846
+ pendingRestart: results.filter(r => r.result === WorkspaceProvisioner.UpdateResult.UPDATED_PENDING_RESTART).length,
1847
+ skippedActiveAgents: results.filter(r => r.result === WorkspaceProvisioner.UpdateResult.SKIPPED_ACTIVE_AGENTS).length,
1848
+ skippedNotRunning: results.filter(r => r.result === WorkspaceProvisioner.UpdateResult.SKIPPED_NOT_RUNNING).length,
1849
+ errors: results.filter(r => r.result === WorkspaceProvisioner.UpdateResult.ERROR).length,
1850
+ };
1851
+ console.log(`[provisioner] Graceful update complete:`, summary);
1852
+ return { summary, results };
1853
+ }
968
1854
  }
969
1855
  // Singleton instance
970
1856
  let _provisioner = null;