@agenticmail/enterprise 0.2.1

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 (69) hide show
  1. package/ARCHITECTURE.md +183 -0
  2. package/agenticmail-enterprise.db +0 -0
  3. package/dashboards/README.md +120 -0
  4. package/dashboards/dotnet/Program.cs +261 -0
  5. package/dashboards/express/app.js +146 -0
  6. package/dashboards/go/main.go +513 -0
  7. package/dashboards/html/index.html +535 -0
  8. package/dashboards/java/AgenticMailDashboard.java +376 -0
  9. package/dashboards/php/index.php +414 -0
  10. package/dashboards/python/app.py +273 -0
  11. package/dashboards/ruby/app.rb +195 -0
  12. package/dist/chunk-77IDQJL3.js +7 -0
  13. package/dist/chunk-7RGCCHIT.js +115 -0
  14. package/dist/chunk-DXNKR3TG.js +1355 -0
  15. package/dist/chunk-IQWA44WT.js +970 -0
  16. package/dist/chunk-LCUZGIDH.js +965 -0
  17. package/dist/chunk-N2JVTNNJ.js +2553 -0
  18. package/dist/chunk-O462UJBH.js +363 -0
  19. package/dist/chunk-PNKVD2UK.js +26 -0
  20. package/dist/cli.js +218 -0
  21. package/dist/dashboard/index.html +558 -0
  22. package/dist/db-adapter-DEWEFNIV.js +7 -0
  23. package/dist/dynamodb-CCGL2E77.js +426 -0
  24. package/dist/engine/index.js +1261 -0
  25. package/dist/index.js +522 -0
  26. package/dist/mongodb-ODTXIVPV.js +319 -0
  27. package/dist/mysql-RM3S2FV5.js +521 -0
  28. package/dist/postgres-LN7A6MGQ.js +518 -0
  29. package/dist/routes-2JEPIIKC.js +441 -0
  30. package/dist/routes-74ZLKJKP.js +399 -0
  31. package/dist/server.js +7 -0
  32. package/dist/sqlite-3K5YOZ4K.js +439 -0
  33. package/dist/turso-LDWODSDI.js +442 -0
  34. package/package.json +49 -0
  35. package/src/admin/routes.ts +331 -0
  36. package/src/auth/routes.ts +130 -0
  37. package/src/cli.ts +260 -0
  38. package/src/dashboard/index.html +558 -0
  39. package/src/db/adapter.ts +230 -0
  40. package/src/db/dynamodb.ts +456 -0
  41. package/src/db/factory.ts +51 -0
  42. package/src/db/mongodb.ts +360 -0
  43. package/src/db/mysql.ts +472 -0
  44. package/src/db/postgres.ts +479 -0
  45. package/src/db/sql-schema.ts +123 -0
  46. package/src/db/sqlite.ts +391 -0
  47. package/src/db/turso.ts +411 -0
  48. package/src/deploy/fly.ts +368 -0
  49. package/src/deploy/managed.ts +213 -0
  50. package/src/engine/activity.ts +474 -0
  51. package/src/engine/agent-config.ts +429 -0
  52. package/src/engine/agenticmail-bridge.ts +296 -0
  53. package/src/engine/approvals.ts +278 -0
  54. package/src/engine/db-adapter.ts +682 -0
  55. package/src/engine/db-schema.ts +335 -0
  56. package/src/engine/deployer.ts +595 -0
  57. package/src/engine/index.ts +134 -0
  58. package/src/engine/knowledge.ts +486 -0
  59. package/src/engine/lifecycle.ts +635 -0
  60. package/src/engine/openclaw-hook.ts +371 -0
  61. package/src/engine/routes.ts +528 -0
  62. package/src/engine/skills.ts +473 -0
  63. package/src/engine/tenant.ts +345 -0
  64. package/src/engine/tool-catalog.ts +189 -0
  65. package/src/index.ts +64 -0
  66. package/src/lib/resilience.ts +326 -0
  67. package/src/middleware/index.ts +286 -0
  68. package/src/server.ts +310 -0
  69. package/tsconfig.json +14 -0
@@ -0,0 +1,595 @@
1
+ /**
2
+ * Deployment Engine
3
+ *
4
+ * Handles provisioning and deploying agents to any target:
5
+ * Docker containers, VPS via SSH, Fly.io, Railway, etc.
6
+ *
7
+ * The admin clicks "Deploy" in the dashboard → this engine does the rest.
8
+ */
9
+
10
+ import type { AgentConfig, DeploymentTarget, DeploymentStatus } from './agent-config.js';
11
+ import { AgentConfigGenerator } from './agent-config.js';
12
+
13
+ // ─── Types ──────────────────────────────────────────────
14
+
15
+ export interface DeploymentEvent {
16
+ timestamp: string;
17
+ phase: DeploymentPhase;
18
+ status: 'started' | 'completed' | 'failed';
19
+ message: string;
20
+ details?: any;
21
+ }
22
+
23
+ export type DeploymentPhase =
24
+ | 'validate'
25
+ | 'provision'
26
+ | 'configure'
27
+ | 'upload'
28
+ | 'install'
29
+ | 'start'
30
+ | 'healthcheck'
31
+ | 'complete';
32
+
33
+ export interface DeploymentResult {
34
+ success: boolean;
35
+ url?: string; // Agent's accessible URL
36
+ sshCommand?: string; // For VPS: how to SSH in
37
+ containerId?: string; // For Docker
38
+ appId?: string; // For cloud platforms
39
+ events: DeploymentEvent[];
40
+ error?: string;
41
+ }
42
+
43
+ export interface LiveAgentStatus {
44
+ agentId: string;
45
+ name: string;
46
+ status: DeploymentStatus;
47
+ uptime?: number; // Seconds
48
+ lastHealthCheck?: string;
49
+ healthStatus?: 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
50
+ metrics?: {
51
+ cpuPercent: number;
52
+ memoryMb: number;
53
+ toolCallsToday: number;
54
+ activeSessionCount: number;
55
+ errorRate: number; // Last hour
56
+ };
57
+ endpoint?: string;
58
+ version?: string;
59
+ }
60
+
61
+ // ─── Deployment Engine ──────────────────────────────────
62
+
63
+ export class DeploymentEngine {
64
+ private configGen = new AgentConfigGenerator();
65
+ private deployments = new Map<string, DeploymentResult>();
66
+ private liveStatus = new Map<string, LiveAgentStatus>();
67
+
68
+ /**
69
+ * Deploy an agent to its configured target
70
+ */
71
+ async deploy(config: AgentConfig, onEvent?: (event: DeploymentEvent) => void): Promise<DeploymentResult> {
72
+ const events: DeploymentEvent[] = [];
73
+ const emit = (phase: DeploymentPhase, status: DeploymentEvent['status'], message: string, details?: any) => {
74
+ const event: DeploymentEvent = { timestamp: new Date().toISOString(), phase, status, message, details };
75
+ events.push(event);
76
+ onEvent?.(event);
77
+ };
78
+
79
+ try {
80
+ // 1. Validate
81
+ emit('validate', 'started', 'Validating agent configuration...');
82
+ this.validateConfig(config);
83
+ emit('validate', 'completed', 'Configuration valid');
84
+
85
+ // 2. Route to target-specific deployer
86
+ let result: DeploymentResult;
87
+ switch (config.deployment.target) {
88
+ case 'docker':
89
+ result = await this.deployDocker(config, emit);
90
+ break;
91
+ case 'vps':
92
+ result = await this.deployVPS(config, emit);
93
+ break;
94
+ case 'fly':
95
+ result = await this.deployFly(config, emit);
96
+ break;
97
+ case 'railway':
98
+ result = await this.deployRailway(config, emit);
99
+ break;
100
+ default:
101
+ throw new Error(`Unsupported deployment target: ${config.deployment.target}`);
102
+ }
103
+
104
+ result.events = events;
105
+ this.deployments.set(config.id, result);
106
+ return result;
107
+
108
+ } catch (error: any) {
109
+ emit('complete', 'failed', `Deployment failed: ${error.message}`);
110
+ const result: DeploymentResult = { success: false, events, error: error.message };
111
+ this.deployments.set(config.id, result);
112
+ return result;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Stop a running agent
118
+ */
119
+ async stop(config: AgentConfig): Promise<{ success: boolean; message: string }> {
120
+ switch (config.deployment.target) {
121
+ case 'docker':
122
+ return this.execCommand(`docker stop agenticmail-${config.name} && docker rm agenticmail-${config.name}`);
123
+ case 'vps':
124
+ return this.execSSH(config, `sudo systemctl stop agenticmail-${config.name}`);
125
+ case 'fly':
126
+ return this.execCommand(`fly apps destroy agenticmail-${config.name} --yes`);
127
+ default:
128
+ return { success: false, message: `Cannot stop: unsupported target ${config.deployment.target}` };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Restart a running agent
134
+ */
135
+ async restart(config: AgentConfig): Promise<{ success: boolean; message: string }> {
136
+ switch (config.deployment.target) {
137
+ case 'docker':
138
+ return this.execCommand(`docker restart agenticmail-${config.name}`);
139
+ case 'vps':
140
+ return this.execSSH(config, `sudo systemctl restart agenticmail-${config.name}`);
141
+ case 'fly':
142
+ return this.execCommand(`fly apps restart agenticmail-${config.name}`);
143
+ default:
144
+ return { success: false, message: `Cannot restart: unsupported target ${config.deployment.target}` };
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Get live status of a deployed agent
150
+ */
151
+ async getStatus(config: AgentConfig): Promise<LiveAgentStatus> {
152
+ const base: LiveAgentStatus = {
153
+ agentId: config.id,
154
+ name: config.displayName,
155
+ status: 'not-deployed',
156
+ healthStatus: 'unknown',
157
+ };
158
+
159
+ try {
160
+ switch (config.deployment.target) {
161
+ case 'docker':
162
+ return await this.getDockerStatus(config, base);
163
+ case 'vps':
164
+ return await this.getVPSStatus(config, base);
165
+ case 'fly':
166
+ return await this.getCloudStatus(config, base);
167
+ default:
168
+ return base;
169
+ }
170
+ } catch {
171
+ return { ...base, status: 'error', healthStatus: 'unhealthy' };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Stream logs from a deployed agent
177
+ */
178
+ async getLogs(config: AgentConfig, lines: number = 100): Promise<string> {
179
+ switch (config.deployment.target) {
180
+ case 'docker':
181
+ return (await this.execCommand(`docker logs --tail ${lines} agenticmail-${config.name}`)).message;
182
+ case 'vps':
183
+ return (await this.execSSH(config, `journalctl -u agenticmail-${config.name} --no-pager -n ${lines}`)).message;
184
+ case 'fly':
185
+ return (await this.execCommand(`fly logs -a agenticmail-${config.name} -n ${lines}`)).message;
186
+ default:
187
+ return 'Log streaming not supported for this target';
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Update a deployed agent's configuration without full redeployment
193
+ */
194
+ async updateConfig(config: AgentConfig): Promise<{ success: boolean; message: string }> {
195
+ const workspace = this.configGen.generateWorkspace(config);
196
+ const gatewayConfig = this.configGen.generateGatewayConfig(config);
197
+
198
+ switch (config.deployment.target) {
199
+ case 'docker': {
200
+ // Write config files into the container
201
+ for (const [file, content] of Object.entries(workspace)) {
202
+ const escaped = content.replace(/'/g, "'\\''");
203
+ await this.execCommand(`docker exec agenticmail-${config.name} sh -c 'echo "${Buffer.from(content).toString('base64')}" | base64 -d > /workspace/${file}'`);
204
+ }
205
+ // Restart gateway inside container
206
+ await this.execCommand(`docker exec agenticmail-${config.name} openclaw gateway restart`);
207
+ return { success: true, message: 'Configuration updated and gateway restarted' };
208
+ }
209
+ case 'vps': {
210
+ const vps = config.deployment.config.vps!;
211
+ for (const [file, content] of Object.entries(workspace)) {
212
+ await this.execSSH(config, `cat > ${vps.installPath}/workspace/${file} << 'EOF'\n${content}\nEOF`);
213
+ }
214
+ await this.execSSH(config, `sudo systemctl restart agenticmail-${config.name}`);
215
+ return { success: true, message: 'Configuration updated and service restarted' };
216
+ }
217
+ default:
218
+ return { success: false, message: 'Hot config update not supported for this target' };
219
+ }
220
+ }
221
+
222
+ // ─── Docker Deployment ────────────────────────────────
223
+
224
+ private async deployDocker(config: AgentConfig, emit: Function): Promise<DeploymentResult> {
225
+ const dc = config.deployment.config.docker;
226
+ if (!dc) throw new Error('Docker config missing');
227
+
228
+ // Generate docker-compose
229
+ emit('provision', 'started', 'Generating Docker configuration...');
230
+ const compose = this.configGen.generateDockerCompose(config);
231
+ emit('provision', 'completed', 'Docker Compose generated');
232
+
233
+ // Generate workspace files
234
+ emit('configure', 'started', 'Generating agent workspace...');
235
+ const workspace = this.configGen.generateWorkspace(config);
236
+ emit('configure', 'completed', `Generated ${Object.keys(workspace).length} workspace files`);
237
+
238
+ // Pull image
239
+ emit('install', 'started', `Pulling image ${dc.image}:${dc.tag}...`);
240
+ await this.execCommand(`docker pull ${dc.image}:${dc.tag}`);
241
+ emit('install', 'completed', 'Image pulled');
242
+
243
+ // Start container
244
+ emit('start', 'started', 'Starting container...');
245
+
246
+ // Build env args
247
+ const envArgs = Object.entries(dc.env).map(([k, v]) => `-e ${k}="${v}"`).join(' ');
248
+ const volumeArgs = dc.volumes.map(v => `-v ${v}`).join(' ');
249
+ const portArgs = dc.ports.map(p => `-p ${p}:${p}`).join(' ');
250
+
251
+ const runCmd = `docker run -d --name agenticmail-${config.name} --restart ${dc.restart} ${portArgs} ${volumeArgs} ${envArgs} ${dc.resources ? `--cpus="${dc.resources.cpuLimit}" --memory="${dc.resources.memoryLimit}"` : ''} ${dc.image}:${dc.tag}`;
252
+ const runResult = await this.execCommand(runCmd);
253
+
254
+ if (!runResult.success) {
255
+ throw new Error(`Container failed to start: ${runResult.message}`);
256
+ }
257
+
258
+ const containerId = runResult.message.trim().substring(0, 12);
259
+ emit('start', 'completed', `Container ${containerId} running`);
260
+
261
+ // Write workspace files into container
262
+ emit('upload', 'started', 'Writing workspace files...');
263
+ for (const [file, content] of Object.entries(workspace)) {
264
+ await this.execCommand(`docker exec agenticmail-${config.name} sh -c 'echo "${Buffer.from(content).toString('base64')}" | base64 -d > /workspace/${file}'`);
265
+ }
266
+ emit('upload', 'completed', 'Workspace configured');
267
+
268
+ // Health check
269
+ emit('healthcheck', 'started', 'Checking agent health...');
270
+ let healthy = false;
271
+ for (let i = 0; i < 10; i++) {
272
+ await new Promise(r => setTimeout(r, 3000));
273
+ const check = await this.execCommand(`docker exec agenticmail-${config.name} openclaw status 2>/dev/null || echo "not ready"`);
274
+ if (check.success && !check.message.includes('not ready')) {
275
+ healthy = true;
276
+ break;
277
+ }
278
+ }
279
+
280
+ if (healthy) {
281
+ emit('healthcheck', 'completed', 'Agent is healthy');
282
+ emit('complete', 'completed', `Agent "${config.displayName}" deployed successfully`);
283
+ } else {
284
+ emit('healthcheck', 'failed', 'Agent did not become healthy within 30s');
285
+ }
286
+
287
+ return {
288
+ success: healthy,
289
+ containerId,
290
+ url: `http://localhost:${dc.ports[0]}`,
291
+ events: [],
292
+ };
293
+ }
294
+
295
+ // ─── VPS Deployment ───────────────────────────────────
296
+
297
+ private async deployVPS(config: AgentConfig, emit: Function): Promise<DeploymentResult> {
298
+ const vps = config.deployment.config.vps;
299
+ if (!vps) throw new Error('VPS config missing');
300
+
301
+ // Generate deploy script
302
+ emit('provision', 'started', `Connecting to ${vps.host}...`);
303
+ const script = this.configGen.generateVPSDeployScript(config);
304
+ emit('provision', 'completed', 'Deploy script generated');
305
+
306
+ // Test SSH connection
307
+ emit('configure', 'started', 'Testing SSH connection...');
308
+ const sshTest = await this.execSSH(config, 'echo "ok"');
309
+ if (!sshTest.success) {
310
+ throw new Error(`SSH connection failed: ${sshTest.message}`);
311
+ }
312
+ emit('configure', 'completed', 'SSH connection verified');
313
+
314
+ // Upload and run deploy script
315
+ emit('upload', 'started', 'Uploading deployment script...');
316
+ const scriptB64 = Buffer.from(script).toString('base64');
317
+ await this.execSSH(config, `echo "${scriptB64}" | base64 -d > /tmp/deploy-agenticmail.sh && chmod +x /tmp/deploy-agenticmail.sh`);
318
+ emit('upload', 'completed', 'Script uploaded');
319
+
320
+ emit('install', 'started', 'Running deployment (this may take a few minutes)...');
321
+ const deployResult = await this.execSSH(config, 'bash /tmp/deploy-agenticmail.sh');
322
+ if (!deployResult.success) {
323
+ throw new Error(`Deployment script failed: ${deployResult.message}`);
324
+ }
325
+ emit('install', 'completed', 'Installation complete');
326
+
327
+ // Verify service is running
328
+ emit('healthcheck', 'started', 'Verifying service status...');
329
+ await new Promise(r => setTimeout(r, 5000));
330
+ const statusCheck = await this.execSSH(config, `systemctl is-active agenticmail-${config.name}`);
331
+ const isActive = statusCheck.success && statusCheck.message.trim() === 'active';
332
+
333
+ if (isActive) {
334
+ emit('healthcheck', 'completed', 'Service is active');
335
+ emit('complete', 'completed', `Agent deployed to ${vps.host}`);
336
+ } else {
337
+ emit('healthcheck', 'failed', 'Service not active');
338
+ }
339
+
340
+ return {
341
+ success: isActive,
342
+ sshCommand: `ssh ${vps.user}@${vps.host}${vps.port !== 22 ? ` -p ${vps.port}` : ''}`,
343
+ events: [],
344
+ };
345
+ }
346
+
347
+ // ─── Fly.io Deployment ────────────────────────────────
348
+
349
+ private async deployFly(config: AgentConfig, emit: Function): Promise<DeploymentResult> {
350
+ const cloud = config.deployment.config.cloud;
351
+ if (!cloud || cloud.provider !== 'fly') throw new Error('Fly.io config missing');
352
+
353
+ const appName = cloud.appName || `agenticmail-${config.name}`;
354
+
355
+ emit('provision', 'started', `Creating Fly.io app ${appName}...`);
356
+ await this.execCommand(`fly apps create ${appName} --org personal`, { FLY_API_TOKEN: cloud.apiToken });
357
+ emit('provision', 'completed', `App ${appName} created`);
358
+
359
+ // Generate Dockerfile
360
+ emit('configure', 'started', 'Generating Dockerfile...');
361
+ const dockerfile = this.generateDockerfile(config);
362
+ const workspace = this.configGen.generateWorkspace(config);
363
+
364
+ // Write temp build context
365
+ const buildDir = `/tmp/agenticmail-build-${config.name}`;
366
+ await this.execCommand(`mkdir -p ${buildDir}/workspace`);
367
+ await this.writeFile(`${buildDir}/Dockerfile`, dockerfile);
368
+ for (const [file, content] of Object.entries(workspace)) {
369
+ await this.writeFile(`${buildDir}/workspace/${file}`, content);
370
+ }
371
+
372
+ // Write fly.toml
373
+ const flyToml = `
374
+ app = "${appName}"
375
+ primary_region = "${cloud.region || 'iad'}"
376
+
377
+ [build]
378
+ dockerfile = "Dockerfile"
379
+
380
+ [http_service]
381
+ internal_port = 3000
382
+ force_https = true
383
+ auto_stop_machines = true
384
+ auto_start_machines = true
385
+ min_machines_running = 1
386
+
387
+ [[vm]]
388
+ size = "${cloud.size || 'shared-cpu-1x'}"
389
+ memory = "512mb"
390
+ `;
391
+ await this.writeFile(`${buildDir}/fly.toml`, flyToml);
392
+ emit('configure', 'completed', 'Build context ready');
393
+
394
+ // Deploy
395
+ emit('install', 'started', 'Deploying to Fly.io (building + pushing)...');
396
+ const deployResult = await this.execCommand(`cd ${buildDir} && fly deploy --now`, { FLY_API_TOKEN: cloud.apiToken });
397
+ emit('install', deployResult.success ? 'completed' : 'failed', deployResult.message);
398
+
399
+ // Cleanup
400
+ await this.execCommand(`rm -rf ${buildDir}`);
401
+
402
+ const url = cloud.customDomain || `https://${appName}.fly.dev`;
403
+
404
+ if (deployResult.success) {
405
+ emit('complete', 'completed', `Agent live at ${url}`);
406
+ }
407
+
408
+ return {
409
+ success: deployResult.success,
410
+ url,
411
+ appId: appName,
412
+ events: [],
413
+ };
414
+ }
415
+
416
+ // ─── Railway Deployment ───────────────────────────────
417
+
418
+ private async deployRailway(config: AgentConfig, emit: Function): Promise<DeploymentResult> {
419
+ const cloud = config.deployment.config.cloud;
420
+ if (!cloud || cloud.provider !== 'railway') throw new Error('Railway config missing');
421
+
422
+ emit('provision', 'started', 'Creating Railway project...');
423
+ // Railway CLI deployment
424
+ const appName = cloud.appName || `agenticmail-${config.name}`;
425
+ const result = await this.execCommand(`railway init --name ${appName}`, { RAILWAY_TOKEN: cloud.apiToken });
426
+ emit('provision', result.success ? 'completed' : 'failed', result.message);
427
+
428
+ return {
429
+ success: result.success,
430
+ url: `https://${appName}.up.railway.app`,
431
+ appId: appName,
432
+ events: [],
433
+ };
434
+ }
435
+
436
+ // ─── Status Checkers ──────────────────────────────────
437
+
438
+ private async getDockerStatus(config: AgentConfig, base: LiveAgentStatus): Promise<LiveAgentStatus> {
439
+ const inspect = await this.execCommand(`docker inspect agenticmail-${config.name} --format '{{.State.Status}} {{.State.StartedAt}}'`);
440
+ if (!inspect.success) return { ...base, status: 'not-deployed' };
441
+
442
+ const [status, startedAt] = inspect.message.trim().split(' ');
443
+ const running = status === 'running';
444
+ const uptime = running ? Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000) : 0;
445
+
446
+ // Get resource usage
447
+ let metrics: LiveAgentStatus['metrics'] = undefined;
448
+ if (running) {
449
+ const stats = await this.execCommand(`docker stats agenticmail-${config.name} --no-stream --format '{{.CPUPerc}} {{.MemUsage}}'`);
450
+ if (stats.success) {
451
+ const parts = stats.message.trim().split(' ');
452
+ metrics = {
453
+ cpuPercent: parseFloat(parts[0]) || 0,
454
+ memoryMb: parseFloat(parts[1]) || 0,
455
+ toolCallsToday: 0,
456
+ activeSessionCount: 0,
457
+ errorRate: 0,
458
+ };
459
+ }
460
+ }
461
+
462
+ return {
463
+ ...base,
464
+ status: running ? 'running' : 'stopped',
465
+ uptime,
466
+ healthStatus: running ? 'healthy' : 'unhealthy',
467
+ lastHealthCheck: new Date().toISOString(),
468
+ metrics,
469
+ };
470
+ }
471
+
472
+ private async getVPSStatus(config: AgentConfig, base: LiveAgentStatus): Promise<LiveAgentStatus> {
473
+ const result = await this.execSSH(config, `systemctl is-active agenticmail-${config.name}`);
474
+ const active = result.success && result.message.trim() === 'active';
475
+
476
+ let uptime = 0;
477
+ if (active) {
478
+ const uptimeResult = await this.execSSH(config, `systemctl show agenticmail-${config.name} --property=ActiveEnterTimestamp --value`);
479
+ if (uptimeResult.success) {
480
+ uptime = Math.floor((Date.now() - new Date(uptimeResult.message.trim()).getTime()) / 1000);
481
+ }
482
+ }
483
+
484
+ return {
485
+ ...base,
486
+ status: active ? 'running' : 'stopped',
487
+ uptime,
488
+ healthStatus: active ? 'healthy' : 'unhealthy',
489
+ lastHealthCheck: new Date().toISOString(),
490
+ };
491
+ }
492
+
493
+ private async getCloudStatus(config: AgentConfig, base: LiveAgentStatus): Promise<LiveAgentStatus> {
494
+ const cloud = config.deployment.config.cloud;
495
+ if (!cloud) return base;
496
+
497
+ const appName = cloud.appName || `agenticmail-${config.name}`;
498
+ const result = await this.execCommand(`fly status -a ${appName} --json`, { FLY_API_TOKEN: cloud.apiToken });
499
+
500
+ if (!result.success) return { ...base, status: 'error' };
501
+
502
+ try {
503
+ const status = JSON.parse(result.message);
504
+ return {
505
+ ...base,
506
+ status: status.Deployed ? 'running' : 'stopped',
507
+ healthStatus: status.Deployed ? 'healthy' : 'unhealthy',
508
+ endpoint: `https://${appName}.fly.dev`,
509
+ version: status.Version?.toString(),
510
+ };
511
+ } catch {
512
+ return { ...base, status: 'error' };
513
+ }
514
+ }
515
+
516
+ // ─── Helpers ──────────────────────────────────────────
517
+
518
+ private validateConfig(config: AgentConfig) {
519
+ if (!config.name) throw new Error('Agent name is required');
520
+ if (!config.identity.role) throw new Error('Agent role is required');
521
+ if (!config.model.modelId) throw new Error('Model ID is required');
522
+ if (!config.deployment.target) throw new Error('Deployment target is required');
523
+
524
+ switch (config.deployment.target) {
525
+ case 'docker':
526
+ if (!config.deployment.config.docker) throw new Error('Docker configuration missing');
527
+ break;
528
+ case 'vps':
529
+ if (!config.deployment.config.vps?.host) throw new Error('VPS host is required');
530
+ break;
531
+ case 'fly':
532
+ case 'railway':
533
+ if (!config.deployment.config.cloud?.apiToken) throw new Error('Cloud API token is required');
534
+ break;
535
+ }
536
+ }
537
+
538
+ private generateDockerfile(config: AgentConfig): string {
539
+ return `FROM node:22-slim
540
+
541
+ WORKDIR /app
542
+
543
+ RUN npm install -g openclaw agenticmail @agenticmail/core @agenticmail/openclaw
544
+
545
+ COPY workspace/ /workspace/
546
+
547
+ ENV NODE_ENV=production
548
+ ENV OPENCLAW_MODEL=${config.model.provider}/${config.model.modelId}
549
+ ENV OPENCLAW_THINKING=${config.model.thinkingLevel}
550
+
551
+ EXPOSE 3000
552
+
553
+ CMD ["openclaw", "gateway", "start"]
554
+ `;
555
+ }
556
+
557
+ private async execCommand(cmd: string, env?: Record<string, string>): Promise<{ success: boolean; message: string }> {
558
+ const { exec } = await import('child_process');
559
+ const { promisify } = await import('util');
560
+ const execAsync = promisify(exec);
561
+
562
+ try {
563
+ const { stdout, stderr } = await execAsync(cmd, {
564
+ timeout: 300_000, // 5 min max
565
+ env: { ...process.env, ...env },
566
+ });
567
+ return { success: true, message: stdout || stderr };
568
+ } catch (error: any) {
569
+ return { success: false, message: error.stderr || error.message };
570
+ }
571
+ }
572
+
573
+ private async execSSH(config: AgentConfig, command: string): Promise<{ success: boolean; message: string }> {
574
+ const vps = config.deployment.config.vps;
575
+ if (!vps) return { success: false, message: 'No VPS config' };
576
+
577
+ const sshArgs = [
578
+ '-o StrictHostKeyChecking=no',
579
+ `-p ${vps.port || 22}`,
580
+ vps.sshKeyPath ? `-i ${vps.sshKeyPath}` : '',
581
+ `${vps.user}@${vps.host}`,
582
+ `"${command.replace(/"/g, '\\"')}"`,
583
+ ].filter(Boolean).join(' ');
584
+
585
+ return this.execCommand(`ssh ${sshArgs}`);
586
+ }
587
+
588
+ private async writeFile(path: string, content: string): Promise<void> {
589
+ const { writeFile } = await import('fs/promises');
590
+ const { dirname } = await import('path');
591
+ const { mkdir } = await import('fs/promises');
592
+ await mkdir(dirname(path), { recursive: true });
593
+ await writeFile(path, content, 'utf-8');
594
+ }
595
+ }