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