@agenticmail/enterprise 0.5.83 → 0.5.85

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.
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Enterprise Deployment Environment Detection
3
+ *
4
+ * Detects the deployment environment (container vs VM vs local dev)
5
+ * and available system capabilities (browser, audio, video, display).
6
+ * Tools use this to gracefully degrade or provide helpful error messages.
7
+ */
8
+
9
+ import { existsSync } from 'node:fs';
10
+ import { execSync } from 'node:child_process';
11
+
12
+ export type DeploymentType = 'container' | 'vm' | 'local' | 'unknown';
13
+
14
+ export interface SystemCapabilities {
15
+ /** Deployment type detected */
16
+ deployment: DeploymentType;
17
+ /** Browser available (Chromium/Chrome installed) */
18
+ hasBrowser: boolean;
19
+ /** Browser executable path (if found) */
20
+ browserPath: string | null;
21
+ /** Display server available (X11/Wayland/macOS) */
22
+ hasDisplay: boolean;
23
+ /** Audio subsystem available (PulseAudio/PipeWire/ALSA/macOS CoreAudio) */
24
+ hasAudio: boolean;
25
+ /** Virtual camera available (v4l2loopback or macOS) */
26
+ hasVirtualCamera: boolean;
27
+ /** Can run headed browser (display + browser) */
28
+ canRunHeadedBrowser: boolean;
29
+ /** Can join video calls (display + browser + audio) */
30
+ canJoinMeetings: boolean;
31
+ /** Can record meetings (display + browser + audio + ffmpeg) */
32
+ canRecordMeetings: boolean;
33
+ /** ffmpeg available */
34
+ hasFfmpeg: boolean;
35
+ /** Persistent filesystem (not ephemeral container) */
36
+ hasPersistentDisk: boolean;
37
+ /** GPU available */
38
+ hasGpu: boolean;
39
+ /** Platform details */
40
+ platform: {
41
+ os: string;
42
+ arch: string;
43
+ isDocker: boolean;
44
+ isFlyio: boolean;
45
+ isRailway: boolean;
46
+ isRender: boolean;
47
+ isAWS: boolean;
48
+ isGCP: boolean;
49
+ isHetzner: boolean;
50
+ };
51
+ }
52
+
53
+ /** Cached result */
54
+ let _cachedCapabilities: SystemCapabilities | null = null;
55
+
56
+ function commandExists(cmd: string): boolean {
57
+ try {
58
+ execSync(`which ${cmd} 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 });
59
+ return true;
60
+ } catch { return false; }
61
+ }
62
+
63
+ function envSet(key: string): boolean {
64
+ return !!process.env[key];
65
+ }
66
+
67
+ function detectDeploymentType(): DeploymentType {
68
+ // Fly.io
69
+ if (envSet('FLY_APP_NAME') || envSet('FLY_MACHINE_ID')) return 'container';
70
+ // Railway
71
+ if (envSet('RAILWAY_ENVIRONMENT') || envSet('RAILWAY_SERVICE_ID')) return 'container';
72
+ // Render
73
+ if (envSet('RENDER_SERVICE_ID') || envSet('RENDER')) return 'container';
74
+ // Generic Docker
75
+ if (existsSync('/.dockerenv') || existsSync('/run/.containerenv')) return 'container';
76
+ // Kubernetes
77
+ if (envSet('KUBERNETES_SERVICE_HOST')) return 'container';
78
+ // AWS ECS
79
+ if (envSet('ECS_CONTAINER_METADATA_URI')) return 'container';
80
+
81
+ // VM indicators
82
+ if (envSet('SSH_CONNECTION') || envSet('SSH_CLIENT')) return 'vm';
83
+ // systemd on Linux without Docker = likely VM
84
+ if (process.platform === 'linux' && existsSync('/run/systemd/system') && !existsSync('/.dockerenv')) return 'vm';
85
+
86
+ // macOS / Windows with display = local dev
87
+ if (process.platform === 'darwin' || process.platform === 'win32') return 'local';
88
+
89
+ // Linux with DISPLAY or Wayland = probably local or VM with desktop
90
+ if (process.platform === 'linux' && (envSet('DISPLAY') || envSet('WAYLAND_DISPLAY'))) return 'vm';
91
+
92
+ return 'unknown';
93
+ }
94
+
95
+ function findBrowser(): string | null {
96
+ // Check env var first (set in Docker)
97
+ const envPath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || process.env.CHROME_PATH;
98
+ if (envPath && existsSync(envPath)) return envPath;
99
+
100
+ const candidates = [
101
+ // Linux
102
+ '/usr/bin/chromium', '/usr/bin/chromium-browser',
103
+ '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable',
104
+ // macOS
105
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
106
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
107
+ // Snap
108
+ '/snap/bin/chromium',
109
+ ];
110
+
111
+ for (const p of candidates) {
112
+ if (existsSync(p)) return p;
113
+ }
114
+
115
+ // Try which
116
+ try {
117
+ const path = execSync('which chromium || which chromium-browser || which google-chrome 2>/dev/null', {
118
+ encoding: 'utf-8', timeout: 3000,
119
+ }).trim();
120
+ if (path) return path;
121
+ } catch { /* ignore */ }
122
+
123
+ return null;
124
+ }
125
+
126
+ function hasDisplayServer(): boolean {
127
+ if (process.platform === 'darwin' || process.platform === 'win32') return true;
128
+ if (envSet('DISPLAY') || envSet('WAYLAND_DISPLAY')) return true;
129
+ // Check for Xvfb
130
+ if (commandExists('Xvfb') || commandExists('xvfb-run')) return true;
131
+ // Check if Xvfb is running
132
+ try {
133
+ execSync('pgrep -x Xvfb', { encoding: 'utf-8', timeout: 2000 });
134
+ return true;
135
+ } catch { /* not running */ }
136
+ return false;
137
+ }
138
+
139
+ function hasAudioSystem(): boolean {
140
+ if (process.platform === 'darwin') return true; // CoreAudio always available
141
+ // PulseAudio
142
+ if (commandExists('pulseaudio') || commandExists('pactl')) {
143
+ try {
144
+ execSync('pactl info 2>/dev/null', { encoding: 'utf-8', timeout: 3000 });
145
+ return true;
146
+ } catch { /* not running */ }
147
+ }
148
+ // PipeWire
149
+ if (commandExists('pw-cli')) {
150
+ try {
151
+ execSync('pw-cli info 2>/dev/null', { encoding: 'utf-8', timeout: 3000 });
152
+ return true;
153
+ } catch { /* not running */ }
154
+ }
155
+ return false;
156
+ }
157
+
158
+ function hasVCam(): boolean {
159
+ if (process.platform === 'darwin') return false; // Need OBS virtual cam or similar
160
+ // v4l2loopback
161
+ try {
162
+ execSync('ls /dev/video* 2>/dev/null', { encoding: 'utf-8', timeout: 2000 });
163
+ return true;
164
+ } catch { return false; }
165
+ }
166
+
167
+ /**
168
+ * Detect system capabilities. Results are cached after first call.
169
+ */
170
+ export function detectCapabilities(): SystemCapabilities {
171
+ if (_cachedCapabilities) return _cachedCapabilities;
172
+
173
+ const deployment = detectDeploymentType();
174
+ const browserPath = findBrowser();
175
+ const hasBrowser = !!browserPath;
176
+ const hasDisplay = hasDisplayServer();
177
+ const hasAudio = hasAudioSystem();
178
+ const hasVirtualCamera = hasVCam();
179
+ const hasFfmpeg = commandExists('ffmpeg');
180
+
181
+ // Persistent disk: containers are typically ephemeral
182
+ const hasPersistentDisk = deployment !== 'container' || envSet('FLY_VOLUME_NAME') || envSet('RAILWAY_VOLUME_MOUNT_PATH');
183
+
184
+ // GPU check
185
+ let hasGpu = false;
186
+ try {
187
+ if (commandExists('nvidia-smi')) { execSync('nvidia-smi', { timeout: 3000 }); hasGpu = true; }
188
+ } catch { /* no GPU */ }
189
+
190
+ const caps: SystemCapabilities = {
191
+ deployment,
192
+ hasBrowser,
193
+ browserPath,
194
+ hasDisplay,
195
+ hasAudio,
196
+ hasVirtualCamera,
197
+ canRunHeadedBrowser: hasBrowser && hasDisplay,
198
+ canJoinMeetings: hasBrowser && hasDisplay && hasAudio,
199
+ canRecordMeetings: hasBrowser && hasDisplay && hasAudio && hasFfmpeg,
200
+ hasFfmpeg,
201
+ hasPersistentDisk: !!hasPersistentDisk,
202
+ hasGpu,
203
+ platform: {
204
+ os: process.platform,
205
+ arch: process.arch,
206
+ isDocker: existsSync('/.dockerenv') || existsSync('/run/.containerenv'),
207
+ isFlyio: envSet('FLY_APP_NAME'),
208
+ isRailway: envSet('RAILWAY_ENVIRONMENT'),
209
+ isRender: envSet('RENDER'),
210
+ isAWS: envSet('AWS_REGION') || envSet('ECS_CONTAINER_METADATA_URI'),
211
+ isGCP: envSet('GOOGLE_CLOUD_PROJECT') || envSet('GCP_PROJECT'),
212
+ isHetzner: false, // No standard env var
213
+ },
214
+ };
215
+
216
+ _cachedCapabilities = caps;
217
+ return caps;
218
+ }
219
+
220
+ /** Reset cache (for testing) */
221
+ export function resetCapabilitiesCache(): void {
222
+ _cachedCapabilities = null;
223
+ }
224
+
225
+ /**
226
+ * Get a human-readable summary of what this deployment can and cannot do.
227
+ * Used in tool error messages and dashboard status.
228
+ */
229
+ export function getCapabilitySummary(caps?: SystemCapabilities): {
230
+ deployment: string;
231
+ available: string[];
232
+ unavailable: string[];
233
+ recommendations: string[];
234
+ } {
235
+ const c = caps || detectCapabilities();
236
+ const available: string[] = [];
237
+ const unavailable: string[] = [];
238
+ const recommendations: string[] = [];
239
+
240
+ if (c.hasBrowser) available.push('Browser (headless)');
241
+ else unavailable.push('Browser — no Chromium/Chrome found');
242
+
243
+ if (c.canRunHeadedBrowser) available.push('Browser (headed/visible)');
244
+ else if (c.hasBrowser) unavailable.push('Headed browser — no display server (install Xvfb)');
245
+
246
+ if (c.canJoinMeetings) available.push('Video meetings (Google Meet, Zoom, Teams)');
247
+ else unavailable.push('Video meetings — requires display + browser + audio');
248
+
249
+ if (c.canRecordMeetings) available.push('Meeting recording');
250
+ else if (c.canJoinMeetings) unavailable.push('Meeting recording — install ffmpeg');
251
+
252
+ if (c.hasAudio) available.push('Audio subsystem');
253
+ else unavailable.push('Audio — no PulseAudio/PipeWire');
254
+
255
+ if (c.hasVirtualCamera) available.push('Virtual camera');
256
+ else unavailable.push('Virtual camera — no v4l2loopback');
257
+
258
+ if (c.hasFfmpeg) available.push('FFmpeg (video/audio processing)');
259
+ else unavailable.push('FFmpeg — install for recording/transcoding');
260
+
261
+ if (c.hasPersistentDisk) available.push('Persistent storage');
262
+ else unavailable.push('Persistent storage — ephemeral container filesystem');
263
+
264
+ // Recommendations based on deployment
265
+ if (c.deployment === 'container' && !c.canJoinMeetings) {
266
+ recommendations.push(
267
+ 'This is a container deployment. For video meetings, deploy on a VM instead.',
268
+ 'Recommended: Hetzner CPX31 ($15/mo) or GCP e2-standard-2 ($50/mo) with the VM setup script.',
269
+ 'Container deployments work great for API-only tasks: email, calendar, docs, drive, sheets.'
270
+ );
271
+ }
272
+
273
+ if (c.deployment === 'vm' && !c.hasDisplay) {
274
+ recommendations.push('Install Xvfb for virtual display: apt install xvfb');
275
+ }
276
+ if (c.deployment === 'vm' && !c.hasAudio) {
277
+ recommendations.push('Install PulseAudio for audio: apt install pulseaudio');
278
+ }
279
+ if (c.deployment === 'vm' && !c.hasBrowser) {
280
+ recommendations.push('Install Chromium: apt install chromium-browser');
281
+ }
282
+
283
+ let deployLabel = c.deployment;
284
+ if (c.platform.isFlyio) deployLabel = 'Fly.io (container)';
285
+ else if (c.platform.isRailway) deployLabel = 'Railway (container)';
286
+ else if (c.platform.isRender) deployLabel = 'Render (container)';
287
+ else if (c.deployment === 'local') deployLabel = `Local (${c.platform.os})`;
288
+
289
+ return { deployment: deployLabel, available, unavailable, recommendations };
290
+ }
@@ -256,7 +256,7 @@ export class AgentRuntime {
256
256
  var session = await this.sessionManager!.createSession(agentId, orgId, opts.parentSessionId);
257
257
 
258
258
  // Build agent config
259
- var tools = opts.tools || createAllTools(this.buildToolOptions(agentId));
259
+ var tools = opts.tools || await createAllTools(this.buildToolOptions(agentId));
260
260
 
261
261
  var systemPrompt = opts.systemPrompt || buildDefaultSystemPrompt(agentId);
262
262
 
@@ -298,7 +298,7 @@ export class AgentRuntime {
298
298
  var apiKey = this.resolveApiKey(model.provider);
299
299
  if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
300
300
 
301
- var tools = createAllTools(this.buildToolOptions(session.agentId));
301
+ var tools = await createAllTools(this.buildToolOptions(session.agentId));
302
302
 
303
303
  var agentConfig: AgentConfig = {
304
304
  agentId: session.agentId,
@@ -589,7 +589,7 @@ export class AgentRuntime {
589
589
  continue;
590
590
  }
591
591
 
592
- var tools = createAllTools(this.buildToolOptions(session.agentId));
592
+ var tools = await createAllTools(this.buildToolOptions(session.agentId));
593
593
 
594
594
  var agentConfig: AgentConfig = {
595
595
  agentId: session.agentId,