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.
- package/.gitattributes +3 -0
- package/.nvmrc +1 -0
- package/.trajectories/agent-relay-322-324.md +17 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
- package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.json +65 -0
- package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.md +37 -0
- package/.trajectories/completed/2026-01/traj_1k5if5snst2e.json +65 -0
- package/.trajectories/completed/2026-01/traj_1k5if5snst2e.md +37 -0
- package/.trajectories/completed/2026-01/traj_1rp3rges5811.json +49 -0
- package/.trajectories/completed/2026-01/traj_1rp3rges5811.md +31 -0
- package/.trajectories/completed/2026-01/traj_22bhyulruouw.json +113 -0
- package/.trajectories/completed/2026-01/traj_22bhyulruouw.md +57 -0
- package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.json +53 -0
- package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.md +32 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
- package/.trajectories/completed/2026-01/traj_3t0440mjeunc.json +26 -0
- package/.trajectories/completed/2026-01/traj_3t0440mjeunc.md +6 -0
- package/.trajectories/completed/2026-01/traj_45x9494d9xnr.json +47 -0
- package/.trajectories/completed/2026-01/traj_45x9494d9xnr.md +32 -0
- package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.json +53 -0
- package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.md +32 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
- package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.json +59 -0
- package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.md +33 -0
- package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.json +53 -0
- package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.md +32 -0
- package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.json +48 -0
- package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.md +24 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
- package/.trajectories/completed/2026-01/traj_7ludwvz45veh.json +209 -0
- package/.trajectories/completed/2026-01/traj_7ludwvz45veh.md +97 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
- package/.trajectories/completed/2026-01/traj_9921cuhel0pj.json +48 -0
- package/.trajectories/completed/2026-01/traj_9921cuhel0pj.md +24 -0
- package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.json +49 -0
- package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.md +23 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
- package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.json +53 -0
- package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.md +32 -0
- package/.trajectories/completed/2026-01/traj_cxofprm2m2en.json +49 -0
- package/.trajectories/completed/2026-01/traj_cxofprm2m2en.md +31 -0
- package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.json +26 -0
- package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.md +6 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
- package/.trajectories/completed/2026-01/traj_dfuvww9pege5.json +59 -0
- package/.trajectories/completed/2026-01/traj_dfuvww9pege5.md +37 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
- package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.json +77 -0
- package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.md +42 -0
- package/.trajectories/completed/2026-01/traj_gjdre5voouod.json +53 -0
- package/.trajectories/completed/2026-01/traj_gjdre5voouod.md +32 -0
- package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.json +25 -0
- package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.md +15 -0
- package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.json +101 -0
- package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.md +44 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
- package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json +22 -0
- package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md +5 -0
- package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.json +53 -0
- package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.md +32 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
- package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.json +25 -0
- package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.md +15 -0
- package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
- package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.json +53 -0
- package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.md +32 -0
- package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.json +53 -0
- package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.md +32 -0
- package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.json +48 -0
- package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.md +24 -0
- package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.json +53 -0
- package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.md +32 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
- package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.json +77 -0
- package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.md +42 -0
- package/.trajectories/completed/2026-01/traj_qft54mi7nfor.json +53 -0
- package/.trajectories/completed/2026-01/traj_qft54mi7nfor.md +32 -0
- package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.json +83 -0
- package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.md +47 -0
- package/.trajectories/completed/2026-01/traj_rd9toccj18a0.json +59 -0
- package/.trajectories/completed/2026-01/traj_rd9toccj18a0.md +37 -0
- package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.json +48 -0
- package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.md +16 -0
- package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.json +59 -0
- package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.md +37 -0
- package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.json +53 -0
- package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.md +32 -0
- package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.json +84 -0
- package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.md +109 -0
- package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.json +53 -0
- package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.md +32 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
- package/.trajectories/completed/2026-01/traj_v87hypnongqx.json +71 -0
- package/.trajectories/completed/2026-01/traj_v87hypnongqx.md +42 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
- package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.json +53 -0
- package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.md +32 -0
- package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.json +20 -0
- package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.md +6 -0
- package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.json +175 -0
- package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.md +82 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
- package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.json +47 -0
- package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.md +32 -0
- package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.json +59 -0
- package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.md +37 -0
- package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.json +53 -0
- package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.md +32 -0
- package/.trajectories/consolidate-settings-panel.md +24 -0
- package/.trajectories/gh-cli-user-token.md +26 -0
- package/.trajectories/index.json +468 -0
- package/ARCHITECTURE.md +1245 -0
- package/TESTING.md +278 -0
- package/deploy/init-db.sql +5 -0
- package/deploy/scripts/setup-fly-workspaces.sh +69 -0
- package/deploy/scripts/setup-railway.sh +75 -0
- package/deploy/workspace/codex.config.toml +15 -0
- package/deploy/workspace/entrypoint-browser.sh +118 -0
- package/deploy/workspace/entrypoint.sh +508 -0
- package/deploy/workspace/git-credential-relay +126 -0
- package/dist/bridge/spawner.d.ts +7 -0
- package/dist/bridge/spawner.js +40 -9
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.js +260 -1
- package/dist/cloud/api/admin.d.ts +8 -0
- package/dist/cloud/api/admin.js +212 -0
- package/dist/cloud/api/auth.js +8 -0
- package/dist/cloud/api/billing.d.ts +0 -10
- package/dist/cloud/api/billing.js +278 -67
- package/dist/cloud/api/codex-auth-helper.d.ts +21 -0
- package/dist/cloud/api/codex-auth-helper.js +307 -0
- package/dist/cloud/api/coordinators.js +402 -0
- package/dist/cloud/api/daemons.js +15 -11
- package/dist/cloud/api/git.js +127 -19
- package/dist/cloud/api/github-app.js +42 -8
- package/dist/cloud/api/nango-auth.js +297 -16
- package/dist/cloud/api/onboarding.js +112 -35
- package/dist/cloud/api/providers.js +12 -16
- package/dist/cloud/api/repos.d.ts +1 -0
- package/dist/cloud/api/repos.js +311 -49
- package/dist/cloud/api/test-helpers.js +40 -0
- package/dist/cloud/api/usage.js +13 -0
- package/dist/cloud/api/webhooks.d.ts +1 -0
- package/dist/cloud/api/webhooks.js +149 -0
- package/dist/cloud/api/workspaces.d.ts +18 -0
- package/dist/cloud/api/workspaces.js +1042 -21
- package/dist/cloud/billing/plans.js +19 -19
- package/dist/cloud/config.d.ts +8 -0
- package/dist/cloud/config.js +15 -0
- package/dist/cloud/db/drizzle.d.ts +5 -2
- package/dist/cloud/db/drizzle.js +27 -20
- package/dist/cloud/db/schema.d.ts +19 -51
- package/dist/cloud/db/schema.js +5 -4
- package/dist/cloud/index.d.ts +0 -1
- package/dist/cloud/index.js +0 -1
- package/dist/cloud/provisioner/index.d.ts +125 -1
- package/dist/cloud/provisioner/index.js +939 -53
- package/dist/cloud/server.js +161 -16
- package/dist/cloud/services/compute-enforcement.d.ts +57 -0
- package/dist/cloud/services/compute-enforcement.js +175 -0
- package/dist/cloud/services/index.d.ts +2 -0
- package/dist/cloud/services/index.js +4 -0
- package/dist/cloud/services/intro-expiration.d.ts +55 -0
- package/dist/cloud/services/intro-expiration.js +211 -0
- package/dist/cloud/services/nango.d.ts +74 -0
- package/dist/cloud/services/nango.js +218 -5
- package/dist/cloud/services/planLimits.d.ts +22 -0
- package/dist/cloud/services/planLimits.js +58 -5
- package/dist/cloud/services/ssh-security.d.ts +31 -0
- package/dist/cloud/services/ssh-security.js +63 -0
- package/dist/continuity/manager.d.ts +5 -0
- package/dist/continuity/manager.js +56 -2
- package/dist/daemon/api.d.ts +2 -0
- package/dist/daemon/api.js +214 -5
- package/dist/daemon/cli-auth.d.ts +13 -1
- package/dist/daemon/cli-auth.js +166 -47
- package/dist/daemon/connection.d.ts +7 -1
- package/dist/daemon/connection.js +15 -0
- package/dist/daemon/orchestrator.d.ts +2 -0
- package/dist/daemon/orchestrator.js +26 -0
- package/dist/daemon/repo-manager.d.ts +116 -0
- package/dist/daemon/repo-manager.js +384 -0
- package/dist/daemon/router.d.ts +60 -1
- package/dist/daemon/router.js +281 -20
- package/dist/daemon/user-directory.d.ts +111 -0
- package/dist/daemon/user-directory.js +233 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-daf87e86f783f980.js → page-4a5938c18a11a654.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/{main-97850e03d723ea8c.js → main-2ee6beb2ae96d210.js} +1 -1
- package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +1 -0
- package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -0
- package/dist/dashboard/out/app/onboarding.txt +7 -0
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +3 -3
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +3 -3
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +3 -3
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +3 -3
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +3 -3
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +3 -3
- package/dist/dashboard/out/pricing.html +3 -3
- package/dist/dashboard/out/pricing.txt +3 -3
- package/dist/dashboard/out/providers/setup/claude.html +1 -0
- package/dist/dashboard/out/providers/setup/claude.txt +8 -0
- package/dist/dashboard/out/providers/setup/codex.html +1 -0
- package/dist/dashboard/out/providers/setup/codex.txt +8 -0
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +3 -3
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +3 -3
- package/dist/dashboard-server/server.js +316 -12
- package/dist/dashboard-server/user-bridge.d.ts +103 -0
- package/dist/dashboard-server/user-bridge.js +189 -0
- package/dist/protocol/channels.d.ts +205 -0
- package/dist/protocol/channels.js +154 -0
- package/dist/protocol/types.d.ts +13 -1
- package/dist/resiliency/provider-context.js +2 -0
- package/dist/shared/cli-auth-config.d.ts +19 -0
- package/dist/shared/cli-auth-config.js +58 -2
- package/dist/utils/agent-config.js +1 -1
- package/dist/wrapper/auth-detection.d.ts +49 -0
- package/dist/wrapper/auth-detection.js +192 -0
- package/dist/wrapper/base-wrapper.d.ts +153 -0
- package/dist/wrapper/base-wrapper.js +393 -0
- package/dist/wrapper/client.d.ts +7 -1
- package/dist/wrapper/client.js +3 -0
- package/dist/wrapper/index.d.ts +1 -0
- package/dist/wrapper/index.js +4 -3
- package/dist/wrapper/pty-wrapper.d.ts +62 -84
- package/dist/wrapper/pty-wrapper.js +154 -180
- package/dist/wrapper/tmux-wrapper.d.ts +41 -66
- package/dist/wrapper/tmux-wrapper.js +90 -134
- package/package.json +5 -12
- package/scripts/postinstall.js +11 -155
- package/scripts/test-interactive-terminal.sh +248 -0
- package/test-push.txt +1 -0
- package/bin/tmux +0 -0
- package/dist/bridge/config.d.ts.map +0 -1
- package/dist/bridge/config.js.map +0 -1
- package/dist/bridge/index.d.ts.map +0 -1
- package/dist/bridge/index.js.map +0 -1
- package/dist/bridge/multi-project-client.d.ts.map +0 -1
- package/dist/bridge/multi-project-client.js.map +0 -1
- package/dist/bridge/shadow-cli.d.ts.map +0 -1
- package/dist/bridge/shadow-cli.js.map +0 -1
- package/dist/bridge/shadow-config.d.ts.map +0 -1
- package/dist/bridge/shadow-config.js.map +0 -1
- package/dist/bridge/spawner.d.ts.map +0 -1
- package/dist/bridge/spawner.js.map +0 -1
- package/dist/bridge/teams-config.d.ts.map +0 -1
- package/dist/bridge/teams-config.js.map +0 -1
- package/dist/bridge/types.d.ts.map +0 -1
- package/dist/bridge/types.js.map +0 -1
- package/dist/bridge/utils.d.ts.map +0 -1
- package/dist/bridge/utils.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cloud/api/auth.d.ts.map +0 -1
- package/dist/cloud/api/auth.js.map +0 -1
- package/dist/cloud/api/billing.d.ts.map +0 -1
- package/dist/cloud/api/billing.js.map +0 -1
- package/dist/cloud/api/cli-pty-runner.d.ts.map +0 -1
- package/dist/cloud/api/cli-pty-runner.js.map +0 -1
- package/dist/cloud/api/coordinators.d.ts.map +0 -1
- package/dist/cloud/api/coordinators.js.map +0 -1
- package/dist/cloud/api/daemons.d.ts.map +0 -1
- package/dist/cloud/api/daemons.js.map +0 -1
- package/dist/cloud/api/generic-webhooks.d.ts.map +0 -1
- package/dist/cloud/api/generic-webhooks.js.map +0 -1
- package/dist/cloud/api/git.d.ts.map +0 -1
- package/dist/cloud/api/git.js.map +0 -1
- package/dist/cloud/api/github-app.d.ts.map +0 -1
- package/dist/cloud/api/github-app.js.map +0 -1
- package/dist/cloud/api/middleware/planLimits.d.ts.map +0 -1
- package/dist/cloud/api/middleware/planLimits.js.map +0 -1
- package/dist/cloud/api/monitoring.d.ts.map +0 -1
- package/dist/cloud/api/monitoring.js.map +0 -1
- package/dist/cloud/api/nango-auth.d.ts.map +0 -1
- package/dist/cloud/api/nango-auth.js.map +0 -1
- package/dist/cloud/api/onboarding.d.ts.map +0 -1
- package/dist/cloud/api/onboarding.js.map +0 -1
- package/dist/cloud/api/policy.d.ts.map +0 -1
- package/dist/cloud/api/policy.js.map +0 -1
- package/dist/cloud/api/providers.d.ts.map +0 -1
- package/dist/cloud/api/providers.js.map +0 -1
- package/dist/cloud/api/repos.d.ts.map +0 -1
- package/dist/cloud/api/repos.js.map +0 -1
- package/dist/cloud/api/teams.d.ts.map +0 -1
- package/dist/cloud/api/teams.js.map +0 -1
- package/dist/cloud/api/test-helpers.d.ts.map +0 -1
- package/dist/cloud/api/test-helpers.js.map +0 -1
- package/dist/cloud/api/usage.d.ts.map +0 -1
- package/dist/cloud/api/usage.js.map +0 -1
- package/dist/cloud/api/webhooks.d.ts.map +0 -1
- package/dist/cloud/api/webhooks.js.map +0 -1
- package/dist/cloud/api/workspaces.d.ts.map +0 -1
- package/dist/cloud/api/workspaces.js.map +0 -1
- package/dist/cloud/billing/index.d.ts.map +0 -1
- package/dist/cloud/billing/index.js.map +0 -1
- package/dist/cloud/billing/plans.d.ts.map +0 -1
- package/dist/cloud/billing/plans.js.map +0 -1
- package/dist/cloud/billing/service.d.ts.map +0 -1
- package/dist/cloud/billing/service.js.map +0 -1
- package/dist/cloud/billing/types.d.ts.map +0 -1
- package/dist/cloud/billing/types.js.map +0 -1
- package/dist/cloud/config.d.ts.map +0 -1
- package/dist/cloud/config.js.map +0 -1
- package/dist/cloud/db/drizzle.d.ts.map +0 -1
- package/dist/cloud/db/drizzle.js.map +0 -1
- package/dist/cloud/db/index.d.ts.map +0 -1
- package/dist/cloud/db/index.js.map +0 -1
- package/dist/cloud/db/schema.d.ts.map +0 -1
- package/dist/cloud/db/schema.js.map +0 -1
- package/dist/cloud/index.d.ts.map +0 -1
- package/dist/cloud/index.js.map +0 -1
- package/dist/cloud/provisioner/index.d.ts.map +0 -1
- package/dist/cloud/provisioner/index.js.map +0 -1
- package/dist/cloud/server.d.ts.map +0 -1
- package/dist/cloud/server.js.map +0 -1
- package/dist/cloud/services/auto-scaler.d.ts.map +0 -1
- package/dist/cloud/services/auto-scaler.js.map +0 -1
- package/dist/cloud/services/capacity-manager.d.ts.map +0 -1
- package/dist/cloud/services/capacity-manager.js.map +0 -1
- package/dist/cloud/services/ci-agent-spawner.d.ts.map +0 -1
- package/dist/cloud/services/ci-agent-spawner.js.map +0 -1
- package/dist/cloud/services/coordinator.d.ts.map +0 -1
- package/dist/cloud/services/coordinator.js.map +0 -1
- package/dist/cloud/services/index.d.ts.map +0 -1
- package/dist/cloud/services/index.js.map +0 -1
- package/dist/cloud/services/mention-handler.d.ts.map +0 -1
- package/dist/cloud/services/mention-handler.js.map +0 -1
- package/dist/cloud/services/nango.d.ts.map +0 -1
- package/dist/cloud/services/nango.js.map +0 -1
- package/dist/cloud/services/persistence.d.ts.map +0 -1
- package/dist/cloud/services/persistence.js.map +0 -1
- package/dist/cloud/services/planLimits.d.ts.map +0 -1
- package/dist/cloud/services/planLimits.js.map +0 -1
- package/dist/cloud/services/scaling-orchestrator.d.ts.map +0 -1
- package/dist/cloud/services/scaling-orchestrator.js.map +0 -1
- package/dist/cloud/services/scaling-policy.d.ts.map +0 -1
- package/dist/cloud/services/scaling-policy.js.map +0 -1
- package/dist/cloud/vault/index.d.ts +0 -76
- package/dist/cloud/vault/index.d.ts.map +0 -1
- package/dist/cloud/vault/index.js +0 -219
- package/dist/cloud/vault/index.js.map +0 -1
- package/dist/cloud/webhooks/index.d.ts.map +0 -1
- package/dist/cloud/webhooks/index.js.map +0 -1
- package/dist/cloud/webhooks/parsers/github.d.ts.map +0 -1
- package/dist/cloud/webhooks/parsers/github.js.map +0 -1
- package/dist/cloud/webhooks/parsers/index.d.ts.map +0 -1
- package/dist/cloud/webhooks/parsers/index.js.map +0 -1
- package/dist/cloud/webhooks/parsers/linear.d.ts.map +0 -1
- package/dist/cloud/webhooks/parsers/linear.js.map +0 -1
- package/dist/cloud/webhooks/parsers/slack.d.ts.map +0 -1
- package/dist/cloud/webhooks/parsers/slack.js.map +0 -1
- package/dist/cloud/webhooks/responders/github.d.ts.map +0 -1
- package/dist/cloud/webhooks/responders/github.js.map +0 -1
- package/dist/cloud/webhooks/responders/index.d.ts.map +0 -1
- package/dist/cloud/webhooks/responders/index.js.map +0 -1
- package/dist/cloud/webhooks/responders/linear.d.ts.map +0 -1
- package/dist/cloud/webhooks/responders/linear.js.map +0 -1
- package/dist/cloud/webhooks/responders/slack.d.ts.map +0 -1
- package/dist/cloud/webhooks/responders/slack.js.map +0 -1
- package/dist/cloud/webhooks/router.d.ts.map +0 -1
- package/dist/cloud/webhooks/router.js.map +0 -1
- package/dist/cloud/webhooks/rules-engine.d.ts.map +0 -1
- package/dist/cloud/webhooks/rules-engine.js.map +0 -1
- package/dist/cloud/webhooks/types.d.ts.map +0 -1
- package/dist/cloud/webhooks/types.js.map +0 -1
- package/dist/continuity/formatter.d.ts.map +0 -1
- package/dist/continuity/formatter.js.map +0 -1
- package/dist/continuity/handoff-store.d.ts.map +0 -1
- package/dist/continuity/handoff-store.js.map +0 -1
- package/dist/continuity/index.d.ts.map +0 -1
- package/dist/continuity/index.js.map +0 -1
- package/dist/continuity/ledger-store.d.ts.map +0 -1
- package/dist/continuity/ledger-store.js.map +0 -1
- package/dist/continuity/manager.d.ts.map +0 -1
- package/dist/continuity/manager.js.map +0 -1
- package/dist/continuity/parser.d.ts.map +0 -1
- package/dist/continuity/parser.js.map +0 -1
- package/dist/continuity/types.d.ts.map +0 -1
- package/dist/continuity/types.js.map +0 -1
- package/dist/daemon/agent-manager.d.ts.map +0 -1
- package/dist/daemon/agent-manager.js.map +0 -1
- package/dist/daemon/agent-registry.d.ts.map +0 -1
- package/dist/daemon/agent-registry.js.map +0 -1
- package/dist/daemon/api.d.ts.map +0 -1
- package/dist/daemon/api.js.map +0 -1
- package/dist/daemon/auth.d.ts.map +0 -1
- package/dist/daemon/auth.js.map +0 -1
- package/dist/daemon/cli-auth.d.ts.map +0 -1
- package/dist/daemon/cli-auth.js.map +0 -1
- package/dist/daemon/cloud-sync.d.ts.map +0 -1
- package/dist/daemon/cloud-sync.js.map +0 -1
- package/dist/daemon/connection.d.ts.map +0 -1
- package/dist/daemon/connection.js.map +0 -1
- package/dist/daemon/index.d.ts.map +0 -1
- package/dist/daemon/index.js.map +0 -1
- package/dist/daemon/orchestrator.d.ts.map +0 -1
- package/dist/daemon/orchestrator.js.map +0 -1
- package/dist/daemon/registry.d.ts.map +0 -1
- package/dist/daemon/registry.js.map +0 -1
- package/dist/daemon/router.d.ts.map +0 -1
- package/dist/daemon/router.js.map +0 -1
- package/dist/daemon/server.d.ts.map +0 -1
- package/dist/daemon/server.js.map +0 -1
- package/dist/daemon/services/browser-testing.d.ts.map +0 -1
- package/dist/daemon/services/browser-testing.js.map +0 -1
- package/dist/daemon/services/container-spawner.d.ts.map +0 -1
- package/dist/daemon/services/container-spawner.js.map +0 -1
- package/dist/daemon/types.d.ts.map +0 -1
- package/dist/daemon/types.js.map +0 -1
- package/dist/daemon/workspace-manager.d.ts.map +0 -1
- package/dist/daemon/workspace-manager.js.map +0 -1
- package/dist/dashboard/out/_next/static/H5aWG0udPB4iOUIl_gytz/_ssgManifest.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/480-2d4111711d4e473c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/724-73c1ee5f60abe860.js +0 -9
- package/dist/dashboard/out/_next/static/chunks/766-c3a14283c88d815b.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-7120be68bea622f3.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1081dd190a331a91.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-b68a681526eb145e.js +0 -1
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
- package/dist/dashboard/out/_next/static/css/411ce23ffeae9f76.css +0 -1
- package/dist/dashboard-server/metrics.d.ts.map +0 -1
- package/dist/dashboard-server/metrics.js.map +0 -1
- package/dist/dashboard-server/needs-attention.d.ts.map +0 -1
- package/dist/dashboard-server/needs-attention.js.map +0 -1
- package/dist/dashboard-server/server.d.ts.map +0 -1
- package/dist/dashboard-server/server.js.map +0 -1
- package/dist/dashboard-server/start.d.ts.map +0 -1
- package/dist/dashboard-server/start.js.map +0 -1
- package/dist/hooks/emitter.d.ts.map +0 -1
- package/dist/hooks/emitter.js.map +0 -1
- package/dist/hooks/inbox-check/hook.d.ts.map +0 -1
- package/dist/hooks/inbox-check/hook.js.map +0 -1
- package/dist/hooks/inbox-check/index.d.ts.map +0 -1
- package/dist/hooks/inbox-check/index.js.map +0 -1
- package/dist/hooks/inbox-check/types.d.ts.map +0 -1
- package/dist/hooks/inbox-check/types.js.map +0 -1
- package/dist/hooks/inbox-check/utils.d.ts.map +0 -1
- package/dist/hooks/inbox-check/utils.js.map +0 -1
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/registry.d.ts.map +0 -1
- package/dist/hooks/registry.js.map +0 -1
- package/dist/hooks/trajectory-hooks.d.ts.map +0 -1
- package/dist/hooks/trajectory-hooks.js.map +0 -1
- package/dist/hooks/types.d.ts.map +0 -1
- package/dist/hooks/types.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/memory/adapters/index.d.ts.map +0 -1
- package/dist/memory/adapters/index.js.map +0 -1
- package/dist/memory/adapters/inmemory.d.ts.map +0 -1
- package/dist/memory/adapters/inmemory.js.map +0 -1
- package/dist/memory/adapters/supermemory.d.ts.map +0 -1
- package/dist/memory/adapters/supermemory.js.map +0 -1
- package/dist/memory/factory.d.ts.map +0 -1
- package/dist/memory/factory.js.map +0 -1
- package/dist/memory/index.d.ts.map +0 -1
- package/dist/memory/index.js.map +0 -1
- package/dist/memory/memory-hooks.d.ts.map +0 -1
- package/dist/memory/memory-hooks.js.map +0 -1
- package/dist/memory/service.d.ts.map +0 -1
- package/dist/memory/service.js.map +0 -1
- package/dist/memory/types.d.ts.map +0 -1
- package/dist/memory/types.js.map +0 -1
- package/dist/policy/agent-policy.d.ts.map +0 -1
- package/dist/policy/agent-policy.js.map +0 -1
- package/dist/policy/cloud-policy-fetcher.d.ts.map +0 -1
- package/dist/policy/cloud-policy-fetcher.js.map +0 -1
- package/dist/protocol/framing.d.ts.map +0 -1
- package/dist/protocol/framing.js.map +0 -1
- package/dist/protocol/index.d.ts.map +0 -1
- package/dist/protocol/index.js.map +0 -1
- package/dist/protocol/types.d.ts.map +0 -1
- package/dist/protocol/types.js.map +0 -1
- package/dist/resiliency/context-persistence.d.ts.map +0 -1
- package/dist/resiliency/context-persistence.js.map +0 -1
- package/dist/resiliency/crash-insights.d.ts.map +0 -1
- package/dist/resiliency/crash-insights.js.map +0 -1
- package/dist/resiliency/gossip-health.d.ts.map +0 -1
- package/dist/resiliency/gossip-health.js.map +0 -1
- package/dist/resiliency/health-monitor.d.ts.map +0 -1
- package/dist/resiliency/health-monitor.js.map +0 -1
- package/dist/resiliency/index.d.ts.map +0 -1
- package/dist/resiliency/index.js.map +0 -1
- package/dist/resiliency/leader-watchdog.d.ts.map +0 -1
- package/dist/resiliency/leader-watchdog.js.map +0 -1
- package/dist/resiliency/logger.d.ts.map +0 -1
- package/dist/resiliency/logger.js.map +0 -1
- package/dist/resiliency/memory-monitor.d.ts.map +0 -1
- package/dist/resiliency/memory-monitor.js.map +0 -1
- package/dist/resiliency/metrics.d.ts.map +0 -1
- package/dist/resiliency/metrics.js.map +0 -1
- package/dist/resiliency/provider-context.d.ts.map +0 -1
- package/dist/resiliency/provider-context.js.map +0 -1
- package/dist/resiliency/stateless-lead.d.ts.map +0 -1
- package/dist/resiliency/stateless-lead.js.map +0 -1
- package/dist/resiliency/supervisor.d.ts.map +0 -1
- package/dist/resiliency/supervisor.js.map +0 -1
- package/dist/shared/cli-auth-config.d.ts.map +0 -1
- package/dist/shared/cli-auth-config.js.map +0 -1
- package/dist/state/agent-state.d.ts.map +0 -1
- package/dist/state/agent-state.js.map +0 -1
- package/dist/storage/adapter.d.ts.map +0 -1
- package/dist/storage/adapter.js.map +0 -1
- package/dist/storage/sqlite-adapter.d.ts.map +0 -1
- package/dist/storage/sqlite-adapter.js.map +0 -1
- package/dist/trajectory/config.d.ts.map +0 -1
- package/dist/trajectory/config.js.map +0 -1
- package/dist/trajectory/index.d.ts.map +0 -1
- package/dist/trajectory/index.js.map +0 -1
- package/dist/trajectory/integration.d.ts.map +0 -1
- package/dist/trajectory/integration.js.map +0 -1
- package/dist/utils/agent-config.d.ts.map +0 -1
- package/dist/utils/agent-config.js.map +0 -1
- package/dist/utils/command-resolver.d.ts.map +0 -1
- package/dist/utils/command-resolver.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/name-generator.d.ts.map +0 -1
- package/dist/utils/name-generator.js.map +0 -1
- package/dist/utils/project-namespace.d.ts.map +0 -1
- package/dist/utils/project-namespace.js.map +0 -1
- package/dist/utils/tmux-resolver.d.ts.map +0 -1
- package/dist/utils/tmux-resolver.js.map +0 -1
- package/dist/utils/update-checker.d.ts.map +0 -1
- package/dist/utils/update-checker.js.map +0 -1
- package/dist/wrapper/client.d.ts.map +0 -1
- package/dist/wrapper/client.js.map +0 -1
- package/dist/wrapper/inbox.d.ts.map +0 -1
- package/dist/wrapper/inbox.js.map +0 -1
- package/dist/wrapper/index.d.ts.map +0 -1
- package/dist/wrapper/index.js.map +0 -1
- package/dist/wrapper/parser.d.ts.map +0 -1
- package/dist/wrapper/parser.js.map +0 -1
- package/dist/wrapper/pty-wrapper.d.ts.map +0 -1
- package/dist/wrapper/pty-wrapper.js.map +0 -1
- package/dist/wrapper/shared.d.ts.map +0 -1
- package/dist/wrapper/shared.js.map +0 -1
- package/dist/wrapper/tmux-wrapper.d.ts.map +0 -1
- package/dist/wrapper/tmux-wrapper.js.map +0 -1
- package/docs/AGENTS.md +0 -513
- package/docs/ARCHITECTURE_DECISIONS.md +0 -175
- package/docs/CLOUD-ARCHITECTURE.md +0 -804
- package/docs/CLOUD-ONBOARDING-DESIGN.md +0 -1983
- package/docs/CONTRIBUTING.md +0 -151
- package/docs/HOOKS_API.md +0 -394
- package/docs/INTEGRATION-GUIDE.md +0 -926
- package/docs/PROTOCOL.md +0 -325
- package/docs/WRAPPER_EVENTS.md +0 -358
- package/docs/agent-policy-snippet.md +0 -40
- package/docs/agent-relay-protocol.md +0 -238
- package/docs/agent-relay-snippet.md +0 -174
- package/docs/archive/CHANGELOG.md +0 -11
- package/docs/archive/CLI-SIMPLIFICATION-COMPLETE.md +0 -48
- package/docs/archive/DESIGN_BRIDGE_STAFFING.md +0 -878
- package/docs/archive/DESIGN_V2.md +0 -1079
- package/docs/archive/EXECUTIVE_SUMMARY.md +0 -358
- package/docs/archive/MONETIZATION.md +0 -1679
- package/docs/archive/PROPOSAL-trajectories.md +0 -1582
- package/docs/archive/ROADMAP.md +0 -329
- package/docs/archive/SCALING_ANALYSIS.md +0 -280
- package/docs/archive/TESTING_PRESENCE_FEATURES.md +0 -327
- package/docs/archive/TMUX_IMPLEMENTATION_NOTES.md +0 -364
- package/docs/archive/TMUX_IMPROVEMENTS.md +0 -968
- package/docs/archive/dashboard-v2-plan.md +0 -179
- package/docs/archive/removable-code-analysis.md +0 -24
- package/docs/competitive/GASTOWN.md +0 -451
- package/docs/competitive/MCP_AGENT_MAIL.md +0 -389
- package/docs/competitive/OVERVIEW.md +0 -898
- package/docs/competitive/README.md +0 -34
- package/docs/competitive/TMUX_ORCHESTRATOR.md +0 -605
- package/docs/dashboard.png +0 -0
- package/docs/design/ci-failure-webhooks.md +0 -812
- package/docs/design/comprehensive-integrations.md +0 -238
- package/docs/design/e2b-sandbox-integration.md +0 -504
- package/docs/design/github-app-permissions.md +0 -264
- package/docs/guides/CLOUD.md +0 -236
- package/docs/guides/LOCAL.md +0 -535
- package/docs/guides/SELF-HOSTED.md +0 -494
- package/docs/local-testing.md +0 -428
- package/docs/proposals/continuous-claude-integration.md +0 -622
- package/docs/proposals/custom-commands.md +0 -368
- package/docs/proposals/shadow-as-subagent.md +0 -765
- package/docs/proposals/slack-bot-integration.md +0 -1457
- package/docs/tasks/global-skills-system.tasks.md +0 -230
- package/docs/tasks/webhook-integrations.tasks.md +0 -184
- package/docs/tasks/workspace-capabilities.tasks.md +0 -121
- package/docs/testing/RESILIENCY-TEST-PLAN-2026-01-01.md +0 -366
- package/scripts/cloud-setup.sh +0 -96
- package/scripts/dev/PUBLIC_RELEASE_PLAN.md +0 -88
- package/scripts/dev/dev-team-setup.sh +0 -431
- package/scripts/e2e-test.sh +0 -119
- package/scripts/games/game-protocol.md +0 -79
- package/scripts/games/hearts-setup.sh +0 -264
- package/scripts/manual-qa.sh +0 -293
- package/scripts/run-cloud-qa.sh +0 -220
- package/scripts/test-cli-auth/Dockerfile +0 -44
- package/scripts/test-cli-auth/Dockerfile.real +0 -79
- package/scripts/test-cli-auth/README.md +0 -286
- package/scripts/test-cli-auth/ci-test-real-clis.ts +0 -251
- package/scripts/test-cli-auth/ci-test-runner.ts +0 -263
- package/scripts/test-cli-auth/mock-cli.sh +0 -147
- package/scripts/test-cli-auth/package.json +0 -14
- package/scripts/test-cli-auth/test-oauth-flow.ts +0 -220
- package/scripts/test-pty-input-auto.js +0 -222
- package/scripts/test-pty-input.js +0 -150
- package/scripts/tictactoe-setup.sh +0 -181
- /package/dist/dashboard/out/_next/static/{H5aWG0udPB4iOUIl_gytz → T1tgCqVWHFIkV7ClEtzD7}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{117-b100311aff8d5c61.js → 117-f7b8ab0809342e77.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{648-a13d3c2b1be45466.js → 648-5cc6e1921389a58a.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-a4973f3e3c82fb67.js → page-53b8a69f76db17d0.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/connect-repos/{page-dc2e3a1a22478efc.js → page-f45ecbc3e06134fc.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/history/{page-56a8b4616a90dc43.js → page-8c8bed33beb2bf1c.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/login/{page-3eac37ea6f5dd153.js → page-16f3b49e55b1e0ed.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/pricing/{page-4d72d5a5d8a9b618.js → page-982a7000fee44014.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/signup/{page-fee4ed1709070bcd.js → page-547dd0ca55ecd0ba.js} +0 -0
- /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:
|
|
93
|
-
medium: { name: 'medium', cpuCores: 2, memoryMb:
|
|
94
|
-
large: { name: 'large', cpuCores: 4, memoryMb:
|
|
95
|
-
xlarge: { name: 'xlarge', cpuCores: 8, memoryMb:
|
|
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
|
-
{
|
|
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
|
|
207
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
818
|
-
|
|
819
|
-
for (const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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;
|