agent-relay 1.0.21 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/dist/bridge/shadow-cli.d.ts +17 -0
  2. package/dist/bridge/shadow-cli.d.ts.map +1 -0
  3. package/dist/bridge/shadow-cli.js +75 -0
  4. package/dist/bridge/shadow-cli.js.map +1 -0
  5. package/dist/bridge/shadow-config.d.ts +87 -0
  6. package/dist/bridge/shadow-config.d.ts.map +1 -0
  7. package/dist/bridge/shadow-config.js +134 -0
  8. package/dist/bridge/shadow-config.js.map +1 -0
  9. package/dist/bridge/spawner.d.ts +15 -1
  10. package/dist/bridge/spawner.d.ts.map +1 -1
  11. package/dist/bridge/spawner.js +164 -4
  12. package/dist/bridge/spawner.js.map +1 -1
  13. package/dist/bridge/types.d.ts +55 -0
  14. package/dist/bridge/types.d.ts.map +1 -1
  15. package/dist/cli/index.js +796 -11
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cloud/api/auth.d.ts +19 -0
  18. package/dist/cloud/api/auth.d.ts.map +1 -0
  19. package/dist/cloud/api/auth.js +216 -0
  20. package/dist/cloud/api/auth.js.map +1 -0
  21. package/dist/cloud/api/billing.d.ts +17 -0
  22. package/dist/cloud/api/billing.d.ts.map +1 -0
  23. package/dist/cloud/api/billing.js +353 -0
  24. package/dist/cloud/api/billing.js.map +1 -0
  25. package/dist/cloud/api/coordinators.d.ts +8 -0
  26. package/dist/cloud/api/coordinators.d.ts.map +1 -0
  27. package/dist/cloud/api/coordinators.js +347 -0
  28. package/dist/cloud/api/coordinators.js.map +1 -0
  29. package/dist/cloud/api/daemons.d.ts +12 -0
  30. package/dist/cloud/api/daemons.d.ts.map +1 -0
  31. package/dist/cloud/api/daemons.js +320 -0
  32. package/dist/cloud/api/daemons.js.map +1 -0
  33. package/dist/cloud/api/middleware/planLimits.d.ts +36 -0
  34. package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
  35. package/dist/cloud/api/middleware/planLimits.js +164 -0
  36. package/dist/cloud/api/middleware/planLimits.js.map +1 -0
  37. package/dist/cloud/api/onboarding.d.ts +8 -0
  38. package/dist/cloud/api/onboarding.d.ts.map +1 -0
  39. package/dist/cloud/api/onboarding.js +407 -0
  40. package/dist/cloud/api/onboarding.js.map +1 -0
  41. package/dist/cloud/api/providers.d.ts +7 -0
  42. package/dist/cloud/api/providers.d.ts.map +1 -0
  43. package/dist/cloud/api/providers.js +435 -0
  44. package/dist/cloud/api/providers.js.map +1 -0
  45. package/dist/cloud/api/repos.d.ts +7 -0
  46. package/dist/cloud/api/repos.d.ts.map +1 -0
  47. package/dist/cloud/api/repos.js +314 -0
  48. package/dist/cloud/api/repos.js.map +1 -0
  49. package/dist/cloud/api/teams.d.ts +7 -0
  50. package/dist/cloud/api/teams.d.ts.map +1 -0
  51. package/dist/cloud/api/teams.js +279 -0
  52. package/dist/cloud/api/teams.js.map +1 -0
  53. package/dist/cloud/api/usage.d.ts +7 -0
  54. package/dist/cloud/api/usage.d.ts.map +1 -0
  55. package/dist/cloud/api/usage.js +98 -0
  56. package/dist/cloud/api/usage.js.map +1 -0
  57. package/dist/cloud/api/workspaces.d.ts +7 -0
  58. package/dist/cloud/api/workspaces.d.ts.map +1 -0
  59. package/dist/cloud/api/workspaces.js +510 -0
  60. package/dist/cloud/api/workspaces.js.map +1 -0
  61. package/dist/cloud/billing/index.d.ts +9 -0
  62. package/dist/cloud/billing/index.d.ts.map +1 -0
  63. package/dist/cloud/billing/index.js +9 -0
  64. package/dist/cloud/billing/index.js.map +1 -0
  65. package/dist/cloud/billing/plans.d.ts +39 -0
  66. package/dist/cloud/billing/plans.d.ts.map +1 -0
  67. package/dist/cloud/billing/plans.js +232 -0
  68. package/dist/cloud/billing/plans.js.map +1 -0
  69. package/dist/cloud/billing/service.d.ts +80 -0
  70. package/dist/cloud/billing/service.d.ts.map +1 -0
  71. package/dist/cloud/billing/service.js +388 -0
  72. package/dist/cloud/billing/service.js.map +1 -0
  73. package/dist/cloud/billing/types.d.ts +135 -0
  74. package/dist/cloud/billing/types.d.ts.map +1 -0
  75. package/dist/cloud/billing/types.js +7 -0
  76. package/dist/cloud/billing/types.js.map +1 -0
  77. package/dist/cloud/config.d.ts +59 -0
  78. package/dist/cloud/config.d.ts.map +1 -0
  79. package/dist/cloud/config.js +83 -0
  80. package/dist/cloud/config.js.map +1 -0
  81. package/dist/cloud/db/drizzle.d.ts +132 -0
  82. package/dist/cloud/db/drizzle.d.ts.map +1 -0
  83. package/dist/cloud/db/drizzle.js +613 -0
  84. package/dist/cloud/db/drizzle.js.map +1 -0
  85. package/dist/cloud/db/index.d.ts +30 -0
  86. package/dist/cloud/db/index.d.ts.map +1 -0
  87. package/dist/cloud/db/index.js +44 -0
  88. package/dist/cloud/db/index.js.map +1 -0
  89. package/dist/cloud/db/schema.d.ts +1792 -0
  90. package/dist/cloud/db/schema.d.ts.map +1 -0
  91. package/dist/cloud/db/schema.js +234 -0
  92. package/dist/cloud/db/schema.js.map +1 -0
  93. package/dist/cloud/index.d.ts +11 -0
  94. package/dist/cloud/index.d.ts.map +1 -0
  95. package/dist/cloud/index.js +37 -0
  96. package/dist/cloud/index.js.map +1 -0
  97. package/dist/cloud/provisioner/index.d.ts +51 -0
  98. package/dist/cloud/provisioner/index.d.ts.map +1 -0
  99. package/dist/cloud/provisioner/index.js +676 -0
  100. package/dist/cloud/provisioner/index.js.map +1 -0
  101. package/dist/cloud/server.d.ts +16 -0
  102. package/dist/cloud/server.d.ts.map +1 -0
  103. package/dist/cloud/server.js +190 -0
  104. package/dist/cloud/server.js.map +1 -0
  105. package/dist/cloud/services/coordinator.d.ts +62 -0
  106. package/dist/cloud/services/coordinator.d.ts.map +1 -0
  107. package/dist/cloud/services/coordinator.js +389 -0
  108. package/dist/cloud/services/coordinator.js.map +1 -0
  109. package/dist/cloud/services/planLimits.d.ts +110 -0
  110. package/dist/cloud/services/planLimits.d.ts.map +1 -0
  111. package/dist/cloud/services/planLimits.js +254 -0
  112. package/dist/cloud/services/planLimits.js.map +1 -0
  113. package/dist/cloud/vault/index.d.ts +76 -0
  114. package/dist/cloud/vault/index.d.ts.map +1 -0
  115. package/dist/cloud/vault/index.js +219 -0
  116. package/dist/cloud/vault/index.js.map +1 -0
  117. package/dist/daemon/agent-manager.d.ts +87 -0
  118. package/dist/daemon/agent-manager.d.ts.map +1 -0
  119. package/dist/daemon/agent-manager.js +412 -0
  120. package/dist/daemon/agent-manager.js.map +1 -0
  121. package/dist/daemon/agent-registry.d.ts +2 -0
  122. package/dist/daemon/agent-registry.d.ts.map +1 -1
  123. package/dist/daemon/agent-registry.js +3 -0
  124. package/dist/daemon/agent-registry.js.map +1 -1
  125. package/dist/daemon/api.d.ts +69 -0
  126. package/dist/daemon/api.d.ts.map +1 -0
  127. package/dist/daemon/api.js +425 -0
  128. package/dist/daemon/api.js.map +1 -0
  129. package/dist/daemon/cloud-sync.d.ts +101 -0
  130. package/dist/daemon/cloud-sync.d.ts.map +1 -0
  131. package/dist/daemon/cloud-sync.js +261 -0
  132. package/dist/daemon/cloud-sync.js.map +1 -0
  133. package/dist/daemon/index.d.ts +4 -0
  134. package/dist/daemon/index.d.ts.map +1 -1
  135. package/dist/daemon/index.js +6 -0
  136. package/dist/daemon/index.js.map +1 -1
  137. package/dist/daemon/orchestrator.d.ts +155 -0
  138. package/dist/daemon/orchestrator.d.ts.map +1 -0
  139. package/dist/daemon/orchestrator.js +736 -0
  140. package/dist/daemon/orchestrator.js.map +1 -0
  141. package/dist/daemon/router.d.ts +24 -0
  142. package/dist/daemon/router.d.ts.map +1 -1
  143. package/dist/daemon/router.js +71 -1
  144. package/dist/daemon/router.js.map +1 -1
  145. package/dist/daemon/server.d.ts +37 -0
  146. package/dist/daemon/server.d.ts.map +1 -1
  147. package/dist/daemon/server.js +191 -16
  148. package/dist/daemon/server.js.map +1 -1
  149. package/dist/daemon/types.d.ts +127 -0
  150. package/dist/daemon/types.d.ts.map +1 -0
  151. package/dist/daemon/types.js +6 -0
  152. package/dist/daemon/types.js.map +1 -0
  153. package/dist/daemon/workspace-manager.d.ts +75 -0
  154. package/dist/daemon/workspace-manager.d.ts.map +1 -0
  155. package/dist/daemon/workspace-manager.js +289 -0
  156. package/dist/daemon/workspace-manager.js.map +1 -0
  157. package/dist/dashboard/out/404.html +1 -1
  158. package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +1 -0
  159. package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +1 -0
  160. package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +1 -0
  161. package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +1 -0
  162. package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +1 -0
  163. package/dist/dashboard/out/_next/static/chunks/app/history/page-b6edd4dde8d08194.js +1 -0
  164. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
  165. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +1 -0
  166. package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +1 -0
  167. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-d80e03a5297f95b6.js +1 -0
  168. package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
  169. package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-c2f423b9c9f4591b.js} +1 -1
  170. package/dist/dashboard/out/_next/static/chunks/{webpack-c81f7fd28659d64f.js → webpack-a5acc2831d094776.js} +1 -1
  171. package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +1 -0
  172. package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +1 -0
  173. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  174. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  175. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  176. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  177. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  178. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
  179. package/dist/dashboard/out/alt-logos/logo.svg +38 -0
  180. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  181. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  182. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  183. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  184. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  185. package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
  186. package/dist/dashboard/out/app.html +14 -0
  187. package/dist/dashboard/out/app.txt +7 -0
  188. package/dist/dashboard/out/history.html +1 -0
  189. package/dist/dashboard/out/history.txt +7 -0
  190. package/dist/dashboard/out/index.html +1 -1
  191. package/dist/dashboard/out/index.txt +2 -2
  192. package/dist/dashboard/out/metrics.html +1 -515
  193. package/dist/dashboard/out/metrics.txt +2 -2
  194. package/dist/dashboard/out/pricing.html +13 -0
  195. package/dist/dashboard/out/pricing.txt +7 -0
  196. package/dist/dashboard-server/metrics.d.ts.map +1 -1
  197. package/dist/dashboard-server/metrics.js +3 -2
  198. package/dist/dashboard-server/metrics.js.map +1 -1
  199. package/dist/dashboard-server/server.d.ts.map +1 -1
  200. package/dist/dashboard-server/server.js +1279 -56
  201. package/dist/dashboard-server/server.js.map +1 -1
  202. package/dist/protocol/types.d.ts +10 -1
  203. package/dist/protocol/types.d.ts.map +1 -1
  204. package/dist/resiliency/context-persistence.d.ts +140 -0
  205. package/dist/resiliency/context-persistence.d.ts.map +1 -0
  206. package/dist/resiliency/context-persistence.js +397 -0
  207. package/dist/resiliency/context-persistence.js.map +1 -0
  208. package/dist/resiliency/health-monitor.d.ts +97 -0
  209. package/dist/resiliency/health-monitor.d.ts.map +1 -0
  210. package/dist/resiliency/health-monitor.js +291 -0
  211. package/dist/resiliency/health-monitor.js.map +1 -0
  212. package/dist/resiliency/index.d.ts +63 -0
  213. package/dist/resiliency/index.d.ts.map +1 -0
  214. package/dist/resiliency/index.js +63 -0
  215. package/dist/resiliency/index.js.map +1 -0
  216. package/dist/resiliency/logger.d.ts +114 -0
  217. package/dist/resiliency/logger.d.ts.map +1 -0
  218. package/dist/resiliency/logger.js +250 -0
  219. package/dist/resiliency/logger.js.map +1 -0
  220. package/dist/resiliency/metrics.d.ts +115 -0
  221. package/dist/resiliency/metrics.d.ts.map +1 -0
  222. package/dist/resiliency/metrics.js +239 -0
  223. package/dist/resiliency/metrics.js.map +1 -0
  224. package/dist/resiliency/provider-context.d.ts +100 -0
  225. package/dist/resiliency/provider-context.d.ts.map +1 -0
  226. package/dist/resiliency/provider-context.js +360 -0
  227. package/dist/resiliency/provider-context.js.map +1 -0
  228. package/dist/resiliency/supervisor.d.ts +109 -0
  229. package/dist/resiliency/supervisor.d.ts.map +1 -0
  230. package/dist/resiliency/supervisor.js +337 -0
  231. package/dist/resiliency/supervisor.js.map +1 -0
  232. package/dist/storage/adapter.d.ts +2 -0
  233. package/dist/storage/adapter.d.ts.map +1 -1
  234. package/dist/storage/adapter.js +12 -2
  235. package/dist/storage/adapter.js.map +1 -1
  236. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  237. package/dist/storage/sqlite-adapter.js +18 -14
  238. package/dist/storage/sqlite-adapter.js.map +1 -1
  239. package/dist/utils/index.d.ts +1 -0
  240. package/dist/utils/index.d.ts.map +1 -1
  241. package/dist/utils/index.js +1 -0
  242. package/dist/utils/index.js.map +1 -1
  243. package/dist/utils/logger.d.ts +40 -0
  244. package/dist/utils/logger.d.ts.map +1 -0
  245. package/dist/utils/logger.js +84 -0
  246. package/dist/utils/logger.js.map +1 -0
  247. package/dist/wrapper/client.d.ts +16 -1
  248. package/dist/wrapper/client.d.ts.map +1 -1
  249. package/dist/wrapper/client.js +32 -1
  250. package/dist/wrapper/client.js.map +1 -1
  251. package/dist/wrapper/parser.d.ts +3 -0
  252. package/dist/wrapper/parser.d.ts.map +1 -1
  253. package/dist/wrapper/parser.js +121 -18
  254. package/dist/wrapper/parser.js.map +1 -1
  255. package/dist/wrapper/pty-wrapper.d.ts +28 -1
  256. package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
  257. package/dist/wrapper/pty-wrapper.js +166 -30
  258. package/dist/wrapper/pty-wrapper.js.map +1 -1
  259. package/dist/wrapper/tmux-wrapper.d.ts +5 -0
  260. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  261. package/dist/wrapper/tmux-wrapper.js +58 -18
  262. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  263. package/docs/CLOUD-ARCHITECTURE.md +652 -0
  264. package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
  265. package/docs/TESTING_PRESENCE_FEATURES.md +327 -0
  266. package/docs/agent-relay-snippet.md +107 -4
  267. package/docs/guides/CLOUD.md +236 -0
  268. package/docs/guides/LOCAL.md +535 -0
  269. package/docs/guides/SELF-HOSTED.md +494 -0
  270. package/docs/proposals/shadow-as-subagent.md +765 -0
  271. package/docs/proposals/slack-bot-integration.md +1457 -0
  272. package/package.json +33 -4
  273. package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
  274. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
  275. package/dist/dashboard/out/_next/static/chunks/app/page-49055e5d2b5e34ec.js +0 -1
  276. package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
  277. package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
  278. /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_buildManifest.js +0 -0
  279. /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_ssgManifest.js +0 -0
  280. /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b2cd8d6485aacf2b.js} +0 -0
  281. /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-8f3f26864ce515e5.js} +0 -0
  282. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-0b990dbb71d72a98.js} +0 -0
  283. /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
@@ -0,0 +1,676 @@
1
+ /**
2
+ * Agent Relay Cloud - Workspace Provisioner
3
+ *
4
+ * One-click provisioning for compute resources (Fly.io, Railway, Docker).
5
+ */
6
+ import { getConfig } from '../config.js';
7
+ import { db } from '../db/index.js';
8
+ import { vault } from '../vault/index.js';
9
+ const WORKSPACE_PORT = 3888;
10
+ const FETCH_TIMEOUT_MS = 10_000;
11
+ async function loadCredentialToken(userId, provider) {
12
+ try {
13
+ const cred = await vault.getCredential(userId, provider);
14
+ if (cred?.accessToken) {
15
+ return cred.accessToken;
16
+ }
17
+ }
18
+ catch (error) {
19
+ console.warn(`Failed to decrypt ${provider} credential from vault; trying raw storage fallback`, error);
20
+ const raw = await db.credentials.findByUserAndProvider(userId, provider);
21
+ return raw?.accessToken ?? null;
22
+ }
23
+ return null;
24
+ }
25
+ async function wait(ms) {
26
+ return new Promise((resolve) => setTimeout(resolve, ms));
27
+ }
28
+ async function fetchWithRetry(url, options = {}) {
29
+ const retries = options.retries ?? 2;
30
+ let attempt = 0;
31
+ while (attempt <= retries) {
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
34
+ try {
35
+ const response = await fetch(url, { ...options, signal: controller.signal });
36
+ clearTimeout(timer);
37
+ if (!response.ok && response.status >= 500 && attempt < retries) {
38
+ attempt += 1;
39
+ await wait(500 * attempt);
40
+ continue;
41
+ }
42
+ return response;
43
+ }
44
+ catch (error) {
45
+ clearTimeout(timer);
46
+ if (attempt >= retries) {
47
+ throw error;
48
+ }
49
+ attempt += 1;
50
+ await wait(500 * attempt);
51
+ }
52
+ }
53
+ throw new Error('fetchWithRetry exhausted retries');
54
+ }
55
+ async function softHealthCheck(url) {
56
+ try {
57
+ const res = await fetchWithRetry(`${url.replace(/\/$/, '')}/health`, { method: 'GET', retries: 1 });
58
+ if (!res.ok) {
59
+ console.warn(`[health] Non-200 from ${url}/health: ${res.status}`);
60
+ }
61
+ }
62
+ catch (error) {
63
+ console.warn(`[health] Failed to reach ${url}/health`, error);
64
+ }
65
+ }
66
+ /**
67
+ * Fly.io provisioner
68
+ */
69
+ class FlyProvisioner {
70
+ apiToken;
71
+ org;
72
+ region;
73
+ workspaceDomain;
74
+ constructor() {
75
+ const config = getConfig();
76
+ if (!config.compute.fly) {
77
+ throw new Error('Fly.io configuration missing');
78
+ }
79
+ this.apiToken = config.compute.fly.apiToken;
80
+ this.org = config.compute.fly.org;
81
+ this.region = config.compute.fly.region || 'sjc';
82
+ this.workspaceDomain = config.compute.fly.workspaceDomain;
83
+ }
84
+ async provision(workspace, credentials) {
85
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
86
+ // Create Fly app
87
+ const createResponse = await fetchWithRetry('https://api.machines.dev/v1/apps', {
88
+ method: 'POST',
89
+ headers: {
90
+ Authorization: `Bearer ${this.apiToken}`,
91
+ 'Content-Type': 'application/json',
92
+ },
93
+ body: JSON.stringify({
94
+ app_name: appName,
95
+ org_slug: this.org,
96
+ }),
97
+ });
98
+ // Set secrets (credentials)
99
+ const secrets = {};
100
+ for (const [provider, token] of credentials) {
101
+ secrets[`${provider.toUpperCase()}_TOKEN`] = token;
102
+ }
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
+ // If custom workspace domain is configured, add certificate
112
+ const customHostname = this.workspaceDomain
113
+ ? `${appName}.${this.workspaceDomain}`
114
+ : null;
115
+ if (customHostname) {
116
+ await this.allocateCertificate(appName, customHostname);
117
+ }
118
+ // Create machine with auto-stop/start for cost optimization
119
+ const machineResponse = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines`, {
120
+ method: 'POST',
121
+ headers: {
122
+ Authorization: `Bearer ${this.apiToken}`,
123
+ 'Content-Type': 'application/json',
124
+ },
125
+ body: JSON.stringify({
126
+ region: this.region,
127
+ config: {
128
+ image: 'ghcr.io/khaliqgant/agent-relay-workspace:latest',
129
+ env: {
130
+ WORKSPACE_ID: workspace.id,
131
+ SUPERVISOR_ENABLED: String(workspace.config.supervisorEnabled ?? false),
132
+ MAX_AGENTS: String(workspace.config.maxAgents ?? 10),
133
+ REPOSITORIES: (workspace.config.repositories ?? []).join(','),
134
+ PROVIDERS: (workspace.config.providers ?? []).join(','),
135
+ PORT: String(WORKSPACE_PORT),
136
+ AGENT_RELAY_DASHBOARD_PORT: String(WORKSPACE_PORT),
137
+ },
138
+ services: [
139
+ {
140
+ ports: [
141
+ { port: 443, handlers: ['tls', 'http'] },
142
+ { port: 80, handlers: ['http'] },
143
+ ],
144
+ protocol: 'tcp',
145
+ internal_port: WORKSPACE_PORT,
146
+ // Auto-stop after 5 minutes of inactivity
147
+ auto_stop_machines: true,
148
+ auto_start_machines: true,
149
+ min_machines_running: 0,
150
+ },
151
+ ],
152
+ guest: {
153
+ cpu_kind: 'shared',
154
+ cpus: 1,
155
+ memory_mb: 512,
156
+ },
157
+ },
158
+ }),
159
+ });
160
+ if (!machineResponse.ok) {
161
+ const error = await machineResponse.text();
162
+ throw new Error(`Failed to create Fly machine: ${error}`);
163
+ }
164
+ const machine = (await machineResponse.json());
165
+ // Return custom domain URL if configured, otherwise default fly.dev
166
+ const publicUrl = customHostname
167
+ ? `https://${customHostname}`
168
+ : `https://${appName}.fly.dev`;
169
+ await softHealthCheck(publicUrl);
170
+ return {
171
+ computeId: machine.id,
172
+ publicUrl,
173
+ };
174
+ }
175
+ /**
176
+ * Allocate SSL certificate for custom domain
177
+ */
178
+ async allocateCertificate(appName, hostname) {
179
+ const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/certificates`, {
180
+ method: 'POST',
181
+ headers: {
182
+ Authorization: `Bearer ${this.apiToken}`,
183
+ 'Content-Type': 'application/json',
184
+ },
185
+ body: JSON.stringify({ hostname }),
186
+ });
187
+ if (!response.ok) {
188
+ const error = await response.text();
189
+ // Don't fail if cert already exists
190
+ if (!error.includes('already exists')) {
191
+ throw new Error(`Failed to allocate certificate for ${hostname}: ${error}`);
192
+ }
193
+ }
194
+ }
195
+ async deprovision(workspace) {
196
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
197
+ await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}`, {
198
+ method: 'DELETE',
199
+ headers: {
200
+ Authorization: `Bearer ${this.apiToken}`,
201
+ },
202
+ });
203
+ }
204
+ async getStatus(workspace) {
205
+ if (!workspace.computeId)
206
+ return 'error';
207
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
208
+ const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}`, {
209
+ headers: {
210
+ Authorization: `Bearer ${this.apiToken}`,
211
+ },
212
+ });
213
+ if (!response.ok)
214
+ return 'error';
215
+ const machine = await response.json();
216
+ switch (machine.state) {
217
+ case 'started':
218
+ return 'running';
219
+ case 'stopped':
220
+ return 'stopped';
221
+ case 'created':
222
+ case 'starting':
223
+ return 'provisioning';
224
+ default:
225
+ return 'error';
226
+ }
227
+ }
228
+ async restart(workspace) {
229
+ if (!workspace.computeId)
230
+ return;
231
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
232
+ await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}/restart`, {
233
+ method: 'POST',
234
+ headers: {
235
+ Authorization: `Bearer ${this.apiToken}`,
236
+ },
237
+ });
238
+ }
239
+ }
240
+ /**
241
+ * Railway provisioner
242
+ */
243
+ class RailwayProvisioner {
244
+ apiToken;
245
+ constructor() {
246
+ const config = getConfig();
247
+ if (!config.compute.railway) {
248
+ throw new Error('Railway configuration missing');
249
+ }
250
+ this.apiToken = config.compute.railway.apiToken;
251
+ }
252
+ async provision(workspace, credentials) {
253
+ // Create project
254
+ const projectResponse = await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
255
+ method: 'POST',
256
+ headers: {
257
+ Authorization: `Bearer ${this.apiToken}`,
258
+ 'Content-Type': 'application/json',
259
+ },
260
+ body: JSON.stringify({
261
+ query: `
262
+ mutation CreateProject($input: ProjectCreateInput!) {
263
+ projectCreate(input: $input) {
264
+ id
265
+ name
266
+ }
267
+ }
268
+ `,
269
+ variables: {
270
+ input: {
271
+ name: `agent-relay-${workspace.id.substring(0, 8)}`,
272
+ },
273
+ },
274
+ }),
275
+ });
276
+ const projectData = await projectResponse.json();
277
+ const projectId = projectData.data.projectCreate.id;
278
+ // Deploy service
279
+ const serviceResponse = await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
280
+ method: 'POST',
281
+ headers: {
282
+ Authorization: `Bearer ${this.apiToken}`,
283
+ 'Content-Type': 'application/json',
284
+ },
285
+ body: JSON.stringify({
286
+ query: `
287
+ mutation CreateService($input: ServiceCreateInput!) {
288
+ serviceCreate(input: $input) {
289
+ id
290
+ }
291
+ }
292
+ `,
293
+ variables: {
294
+ input: {
295
+ projectId,
296
+ name: 'workspace',
297
+ source: {
298
+ image: 'ghcr.io/khaliqgant/agent-relay-workspace:latest',
299
+ },
300
+ },
301
+ },
302
+ }),
303
+ });
304
+ const serviceData = await serviceResponse.json();
305
+ const serviceId = serviceData.data.serviceCreate.id;
306
+ // Set environment variables
307
+ const envVars = {
308
+ WORKSPACE_ID: workspace.id,
309
+ SUPERVISOR_ENABLED: String(workspace.config.supervisorEnabled ?? false),
310
+ MAX_AGENTS: String(workspace.config.maxAgents ?? 10),
311
+ REPOSITORIES: (workspace.config.repositories ?? []).join(','),
312
+ PROVIDERS: (workspace.config.providers ?? []).join(','),
313
+ PORT: String(WORKSPACE_PORT),
314
+ AGENT_RELAY_DASHBOARD_PORT: String(WORKSPACE_PORT),
315
+ };
316
+ for (const [provider, token] of credentials) {
317
+ envVars[`${provider.toUpperCase()}_TOKEN`] = token;
318
+ }
319
+ await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
320
+ method: 'POST',
321
+ headers: {
322
+ Authorization: `Bearer ${this.apiToken}`,
323
+ 'Content-Type': 'application/json',
324
+ },
325
+ body: JSON.stringify({
326
+ query: `
327
+ mutation SetVariables($input: VariableCollectionUpsertInput!) {
328
+ variableCollectionUpsert(input: $input)
329
+ }
330
+ `,
331
+ variables: {
332
+ input: {
333
+ projectId,
334
+ serviceId,
335
+ variables: envVars,
336
+ },
337
+ },
338
+ }),
339
+ });
340
+ // Generate domain
341
+ const domainResponse = await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
342
+ method: 'POST',
343
+ headers: {
344
+ Authorization: `Bearer ${this.apiToken}`,
345
+ 'Content-Type': 'application/json',
346
+ },
347
+ body: JSON.stringify({
348
+ query: `
349
+ mutation CreateDomain($input: ServiceDomainCreateInput!) {
350
+ serviceDomainCreate(input: $input) {
351
+ domain
352
+ }
353
+ }
354
+ `,
355
+ variables: {
356
+ input: {
357
+ serviceId,
358
+ },
359
+ },
360
+ }),
361
+ });
362
+ const domainData = await domainResponse.json();
363
+ const domain = domainData.data.serviceDomainCreate.domain;
364
+ await softHealthCheck(`https://${domain}`);
365
+ return {
366
+ computeId: projectId,
367
+ publicUrl: `https://${domain}`,
368
+ };
369
+ }
370
+ async deprovision(workspace) {
371
+ if (!workspace.computeId)
372
+ return;
373
+ await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
374
+ method: 'POST',
375
+ headers: {
376
+ Authorization: `Bearer ${this.apiToken}`,
377
+ 'Content-Type': 'application/json',
378
+ },
379
+ body: JSON.stringify({
380
+ query: `
381
+ mutation DeleteProject($id: String!) {
382
+ projectDelete(id: $id)
383
+ }
384
+ `,
385
+ variables: {
386
+ id: workspace.computeId,
387
+ },
388
+ }),
389
+ });
390
+ }
391
+ async getStatus(workspace) {
392
+ if (!workspace.computeId)
393
+ return 'error';
394
+ const response = await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
395
+ method: 'POST',
396
+ headers: {
397
+ Authorization: `Bearer ${this.apiToken}`,
398
+ 'Content-Type': 'application/json',
399
+ },
400
+ body: JSON.stringify({
401
+ query: `
402
+ query GetProject($id: String!) {
403
+ project(id: $id) {
404
+ deployments {
405
+ edges {
406
+ node {
407
+ status
408
+ }
409
+ }
410
+ }
411
+ }
412
+ }
413
+ `,
414
+ variables: {
415
+ id: workspace.computeId,
416
+ },
417
+ }),
418
+ });
419
+ const data = await response.json();
420
+ const deployments = data.data?.project?.deployments?.edges;
421
+ if (!deployments || deployments.length === 0)
422
+ return 'provisioning';
423
+ const latestStatus = deployments[0].node.status;
424
+ switch (latestStatus) {
425
+ case 'SUCCESS':
426
+ return 'running';
427
+ case 'BUILDING':
428
+ case 'DEPLOYING':
429
+ return 'provisioning';
430
+ case 'CRASHED':
431
+ case 'FAILED':
432
+ return 'error';
433
+ default:
434
+ return 'stopped';
435
+ }
436
+ }
437
+ async restart(workspace) {
438
+ // Railway doesn't have a direct restart - redeploy instead
439
+ if (!workspace.computeId)
440
+ return;
441
+ await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
442
+ method: 'POST',
443
+ headers: {
444
+ Authorization: `Bearer ${this.apiToken}`,
445
+ 'Content-Type': 'application/json',
446
+ },
447
+ body: JSON.stringify({
448
+ query: `
449
+ mutation RedeployService($input: DeploymentTriggerInput!) {
450
+ deploymentTrigger(input: $input)
451
+ }
452
+ `,
453
+ variables: {
454
+ input: {
455
+ projectId: workspace.computeId,
456
+ },
457
+ },
458
+ }),
459
+ });
460
+ }
461
+ }
462
+ /**
463
+ * Local Docker provisioner (for development/self-hosted)
464
+ */
465
+ class DockerProvisioner {
466
+ async provision(workspace, credentials) {
467
+ const containerName = `ar-${workspace.id.substring(0, 8)}`;
468
+ // Build environment variables
469
+ const envArgs = [
470
+ `-e WORKSPACE_ID=${workspace.id}`,
471
+ `-e SUPERVISOR_ENABLED=${workspace.config.supervisorEnabled ?? false}`,
472
+ `-e MAX_AGENTS=${workspace.config.maxAgents ?? 10}`,
473
+ `-e REPOSITORIES=${(workspace.config.repositories ?? []).join(',')}`,
474
+ `-e PROVIDERS=${(workspace.config.providers ?? []).join(',')}`,
475
+ `-e PORT=${WORKSPACE_PORT}`,
476
+ `-e AGENT_RELAY_DASHBOARD_PORT=${WORKSPACE_PORT}`,
477
+ ];
478
+ for (const [provider, token] of credentials) {
479
+ envArgs.push(`-e ${provider.toUpperCase()}_TOKEN=${token}`);
480
+ }
481
+ // Run container
482
+ const { execSync } = await import('child_process');
483
+ const hostPort = 3000 + Math.floor(Math.random() * 1000);
484
+ try {
485
+ execSync(`docker run -d --name ${containerName} -p ${hostPort}:${WORKSPACE_PORT} ${envArgs.join(' ')} ghcr.io/khaliqgant/agent-relay-workspace:latest`, { stdio: 'pipe' });
486
+ return {
487
+ computeId: containerName,
488
+ publicUrl: `http://localhost:${hostPort}`,
489
+ };
490
+ }
491
+ catch (error) {
492
+ throw new Error(`Failed to start Docker container: ${error}`);
493
+ }
494
+ }
495
+ async deprovision(workspace) {
496
+ if (!workspace.computeId)
497
+ return;
498
+ const { execSync } = await import('child_process');
499
+ try {
500
+ execSync(`docker rm -f ${workspace.computeId}`, { stdio: 'pipe' });
501
+ }
502
+ catch {
503
+ // Container may already be removed
504
+ }
505
+ }
506
+ async getStatus(workspace) {
507
+ if (!workspace.computeId)
508
+ return 'error';
509
+ const { execSync } = await import('child_process');
510
+ try {
511
+ const result = execSync(`docker inspect -f '{{.State.Status}}' ${workspace.computeId}`, { stdio: 'pipe' }).toString().trim();
512
+ switch (result) {
513
+ case 'running':
514
+ return 'running';
515
+ case 'exited':
516
+ case 'dead':
517
+ return 'stopped';
518
+ case 'created':
519
+ case 'restarting':
520
+ return 'provisioning';
521
+ default:
522
+ return 'error';
523
+ }
524
+ }
525
+ catch {
526
+ return 'error';
527
+ }
528
+ }
529
+ async restart(workspace) {
530
+ if (!workspace.computeId)
531
+ return;
532
+ const { execSync } = await import('child_process');
533
+ try {
534
+ execSync(`docker restart ${workspace.computeId}`, { stdio: 'pipe' });
535
+ }
536
+ catch (error) {
537
+ throw new Error(`Failed to restart container: ${error}`);
538
+ }
539
+ }
540
+ }
541
+ /**
542
+ * Main Workspace Provisioner
543
+ */
544
+ export class WorkspaceProvisioner {
545
+ provisioner;
546
+ constructor() {
547
+ const config = getConfig();
548
+ switch (config.compute.provider) {
549
+ case 'fly':
550
+ this.provisioner = new FlyProvisioner();
551
+ break;
552
+ case 'railway':
553
+ this.provisioner = new RailwayProvisioner();
554
+ break;
555
+ case 'docker':
556
+ default:
557
+ this.provisioner = new DockerProvisioner();
558
+ }
559
+ }
560
+ /**
561
+ * Provision a new workspace (one-click)
562
+ */
563
+ async provision(config) {
564
+ // Create workspace record
565
+ const workspace = await db.workspaces.create({
566
+ userId: config.userId,
567
+ name: config.name,
568
+ computeProvider: getConfig().compute.provider,
569
+ config: {
570
+ providers: config.providers,
571
+ repositories: config.repositories,
572
+ supervisorEnabled: config.supervisorEnabled ?? true,
573
+ maxAgents: config.maxAgents ?? 10,
574
+ },
575
+ });
576
+ // Get credentials
577
+ const credentials = new Map();
578
+ for (const provider of config.providers) {
579
+ const token = await loadCredentialToken(config.userId, provider);
580
+ if (token) {
581
+ credentials.set(provider, token);
582
+ }
583
+ }
584
+ // GitHub token is required for cloning repositories
585
+ if (config.repositories.length > 0) {
586
+ const githubToken = await loadCredentialToken(config.userId, 'github');
587
+ if (githubToken) {
588
+ credentials.set('github', githubToken);
589
+ }
590
+ else {
591
+ console.warn(`No GitHub token found for user ${config.userId}; repository cloning may fail.`);
592
+ }
593
+ }
594
+ // Provision compute
595
+ try {
596
+ const { computeId, publicUrl } = await this.provisioner.provision(workspace, credentials);
597
+ await db.workspaces.updateStatus(workspace.id, 'running', {
598
+ computeId,
599
+ publicUrl,
600
+ });
601
+ return {
602
+ workspaceId: workspace.id,
603
+ status: 'running',
604
+ publicUrl,
605
+ };
606
+ }
607
+ catch (error) {
608
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
609
+ await db.workspaces.updateStatus(workspace.id, 'error', {
610
+ errorMessage,
611
+ });
612
+ return {
613
+ workspaceId: workspace.id,
614
+ status: 'error',
615
+ error: errorMessage,
616
+ };
617
+ }
618
+ }
619
+ /**
620
+ * Deprovision a workspace
621
+ */
622
+ async deprovision(workspaceId) {
623
+ const workspace = await db.workspaces.findById(workspaceId);
624
+ if (!workspace) {
625
+ throw new Error('Workspace not found');
626
+ }
627
+ await this.provisioner.deprovision(workspace);
628
+ await db.workspaces.delete(workspaceId);
629
+ }
630
+ /**
631
+ * Get workspace status
632
+ */
633
+ async getStatus(workspaceId) {
634
+ const workspace = await db.workspaces.findById(workspaceId);
635
+ if (!workspace) {
636
+ throw new Error('Workspace not found');
637
+ }
638
+ const status = await this.provisioner.getStatus(workspace);
639
+ // Update database if status changed
640
+ if (status !== workspace.status) {
641
+ await db.workspaces.updateStatus(workspaceId, status);
642
+ }
643
+ return status;
644
+ }
645
+ /**
646
+ * Restart a workspace
647
+ */
648
+ async restart(workspaceId) {
649
+ const workspace = await db.workspaces.findById(workspaceId);
650
+ if (!workspace) {
651
+ throw new Error('Workspace not found');
652
+ }
653
+ await this.provisioner.restart(workspace);
654
+ }
655
+ /**
656
+ * Stop a workspace
657
+ */
658
+ async stop(workspaceId) {
659
+ const workspace = await db.workspaces.findById(workspaceId);
660
+ if (!workspace) {
661
+ throw new Error('Workspace not found');
662
+ }
663
+ // For now, just deprovision to stop
664
+ await this.provisioner.deprovision(workspace);
665
+ await db.workspaces.updateStatus(workspaceId, 'stopped');
666
+ }
667
+ }
668
+ // Singleton instance
669
+ let _provisioner = null;
670
+ export function getProvisioner() {
671
+ if (!_provisioner) {
672
+ _provisioner = new WorkspaceProvisioner();
673
+ }
674
+ return _provisioner;
675
+ }
676
+ //# sourceMappingURL=index.js.map