@agenticmail/enterprise 0.5.81 → 0.5.83

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,47 @@
1
+ import {
2
+ AgentRuntime,
3
+ EmailChannel,
4
+ FollowUpScheduler,
5
+ SessionManager,
6
+ SubAgentManager,
7
+ ToolRegistry,
8
+ callLLM,
9
+ createAgentRuntime,
10
+ createNoopHooks,
11
+ createRuntimeHooks,
12
+ estimateMessageTokens,
13
+ estimateTokens,
14
+ executeTool,
15
+ runAgentLoop,
16
+ toolsToDefinitions
17
+ } from "./chunk-HP2BGRVX.js";
18
+ import "./chunk-TYW5XTOW.js";
19
+ import "./chunk-JLSQOQ5L.js";
20
+ import {
21
+ PROVIDER_REGISTRY,
22
+ listAllProviders,
23
+ resolveApiKeyForProvider,
24
+ resolveProvider
25
+ } from "./chunk-67KZYSLU.js";
26
+ import "./chunk-KFQGP6VL.js";
27
+ export {
28
+ AgentRuntime,
29
+ EmailChannel,
30
+ FollowUpScheduler,
31
+ PROVIDER_REGISTRY,
32
+ SessionManager,
33
+ SubAgentManager,
34
+ ToolRegistry,
35
+ callLLM,
36
+ createAgentRuntime,
37
+ createNoopHooks,
38
+ createRuntimeHooks,
39
+ estimateMessageTokens,
40
+ estimateTokens,
41
+ executeTool,
42
+ listAllProviders,
43
+ resolveApiKeyForProvider,
44
+ resolveProvider,
45
+ runAgentLoop,
46
+ toolsToDefinitions
47
+ };
@@ -0,0 +1,47 @@
1
+ import {
2
+ AgentRuntime,
3
+ EmailChannel,
4
+ FollowUpScheduler,
5
+ SessionManager,
6
+ SubAgentManager,
7
+ ToolRegistry,
8
+ callLLM,
9
+ createAgentRuntime,
10
+ createNoopHooks,
11
+ createRuntimeHooks,
12
+ estimateMessageTokens,
13
+ estimateTokens,
14
+ executeTool,
15
+ runAgentLoop,
16
+ toolsToDefinitions
17
+ } from "./chunk-56ZDS6X4.js";
18
+ import "./chunk-TYW5XTOW.js";
19
+ import "./chunk-JLSQOQ5L.js";
20
+ import {
21
+ PROVIDER_REGISTRY,
22
+ listAllProviders,
23
+ resolveApiKeyForProvider,
24
+ resolveProvider
25
+ } from "./chunk-67KZYSLU.js";
26
+ import "./chunk-KFQGP6VL.js";
27
+ export {
28
+ AgentRuntime,
29
+ EmailChannel,
30
+ FollowUpScheduler,
31
+ PROVIDER_REGISTRY,
32
+ SessionManager,
33
+ SubAgentManager,
34
+ ToolRegistry,
35
+ callLLM,
36
+ createAgentRuntime,
37
+ createNoopHooks,
38
+ createRuntimeHooks,
39
+ estimateMessageTokens,
40
+ estimateTokens,
41
+ executeTool,
42
+ listAllProviders,
43
+ resolveApiKeyForProvider,
44
+ resolveProvider,
45
+ runAgentLoop,
46
+ toolsToDefinitions
47
+ };
@@ -0,0 +1,12 @@
1
+ import {
2
+ createServer
3
+ } from "./chunk-UJFEKPLU.js";
4
+ import "./chunk-3SMTCIR4.js";
5
+ import "./chunk-JLSQOQ5L.js";
6
+ import "./chunk-RO537U6H.js";
7
+ import "./chunk-DRXMYYKN.js";
8
+ import "./chunk-67KZYSLU.js";
9
+ import "./chunk-KFQGP6VL.js";
10
+ export {
11
+ createServer
12
+ };
@@ -0,0 +1,12 @@
1
+ import {
2
+ createServer
3
+ } from "./chunk-GWRYZJNJ.js";
4
+ import "./chunk-3SMTCIR4.js";
5
+ import "./chunk-JLSQOQ5L.js";
6
+ import "./chunk-RO537U6H.js";
7
+ import "./chunk-DRXMYYKN.js";
8
+ import "./chunk-67KZYSLU.js";
9
+ import "./chunk-KFQGP6VL.js";
10
+ export {
11
+ createServer
12
+ };
@@ -0,0 +1,20 @@
1
+ import {
2
+ promptCompanyInfo,
3
+ promptDatabase,
4
+ promptDeployment,
5
+ promptDomain,
6
+ promptRegistration,
7
+ provision,
8
+ runSetupWizard
9
+ } from "./chunk-KRXDNJUR.js";
10
+ import "./chunk-QDXUZP7Y.js";
11
+ import "./chunk-KFQGP6VL.js";
12
+ export {
13
+ promptCompanyInfo,
14
+ promptDatabase,
15
+ promptDeployment,
16
+ promptDomain,
17
+ promptRegistration,
18
+ provision,
19
+ runSetupWizard
20
+ };
@@ -0,0 +1,20 @@
1
+ import {
2
+ promptCompanyInfo,
3
+ promptDatabase,
4
+ promptDeployment,
5
+ promptDomain,
6
+ promptRegistration,
7
+ provision,
8
+ runSetupWizard
9
+ } from "./chunk-CFEPEV5P.js";
10
+ import "./chunk-QDXUZP7Y.js";
11
+ import "./chunk-KFQGP6VL.js";
12
+ export {
13
+ promptCompanyInfo,
14
+ promptDatabase,
15
+ promptDeployment,
16
+ promptDomain,
17
+ promptRegistration,
18
+ provision,
19
+ runSetupWizard
20
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/enterprise",
3
- "version": "0.5.81",
3
+ "version": "0.5.83",
4
4
  "description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3988,6 +3988,190 @@ function ToolsSection(props) {
3988
3988
  // BROWSER CONFIG CARD — Configurable browser settings per agent
3989
3989
  // ════════════════════════════════════════════════════════════
3990
3990
 
3991
+ // ════════════════════════════════════════════════════════════
3992
+ // MEETING CAPABILITIES — Simple toggle, everything auto-managed
3993
+ // ════════════════════════════════════════════════════════════
3994
+
3995
+ function MeetingCapabilitiesSection(props) {
3996
+ var agentId = props.agentId;
3997
+ var cfg = props.cfg;
3998
+ var update = props.update;
3999
+ var sectionStyle = props.sectionStyle;
4000
+ var sectionTitle = props.sectionTitle;
4001
+ var labelStyle = props.labelStyle;
4002
+ var helpStyle = props.helpStyle;
4003
+ var _d = useApp(); var toast = _d.toast;
4004
+ var _launching = useState(false); var launching = _launching[0]; var setLaunching = _launching[1];
4005
+ var _browserStatus = useState(null); var browserStatus = _browserStatus[0]; var setBrowserStatus = _browserStatus[1];
4006
+
4007
+ function checkMeetingBrowser() {
4008
+ engineCall('/bridge/agents/' + agentId + '/browser-config/test', { method: 'POST' })
4009
+ .then(function(d) { setBrowserStatus(d); })
4010
+ .catch(function() { setBrowserStatus(null); });
4011
+ }
4012
+
4013
+ useEffect(function() {
4014
+ if (cfg.meetingsEnabled) checkMeetingBrowser();
4015
+ }, [cfg.meetingsEnabled]);
4016
+
4017
+ function launchMeetingBrowser() {
4018
+ setLaunching(true);
4019
+ engineCall('/bridge/agents/' + agentId + '/browser-config/launch-meeting-browser', { method: 'POST' })
4020
+ .then(function(d) {
4021
+ if (d.error) { toast(d.error, 'error'); }
4022
+ else { toast('Meeting browser ready', 'success'); setBrowserStatus(d); }
4023
+ setLaunching(false);
4024
+ })
4025
+ .catch(function(e) { toast(e.message, 'error'); setLaunching(false); });
4026
+ }
4027
+
4028
+ var meetingsOn = cfg.meetingsEnabled === true;
4029
+
4030
+ return h('div', { style: sectionStyle },
4031
+ sectionTitle('\uD83C\uDFA5', 'Meetings & Video Calls'),
4032
+
4033
+ // Main toggle
4034
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 16, marginBottom: meetingsOn ? 16 : 0 } },
4035
+ h('div', {
4036
+ onClick: function() { update('meetingsEnabled', !meetingsOn); },
4037
+ style: {
4038
+ width: 52, height: 28, borderRadius: 14, position: 'relative', cursor: 'pointer',
4039
+ background: meetingsOn ? 'var(--accent)' : 'var(--border)', transition: 'background 0.2s', flexShrink: 0,
4040
+ },
4041
+ },
4042
+ h('div', { style: {
4043
+ width: 24, height: 24, borderRadius: 12, background: '#fff', position: 'absolute', top: 2,
4044
+ left: meetingsOn ? 26 : 2, transition: 'left 0.2s',
4045
+ boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
4046
+ } })
4047
+ ),
4048
+ h('div', null,
4049
+ h('div', { style: { fontWeight: 600, fontSize: 13 } }, meetingsOn ? 'Meeting participation enabled' : 'Meeting participation disabled'),
4050
+ h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginTop: 2 } },
4051
+ meetingsOn
4052
+ ? 'Agent can join Google Meet, Microsoft Teams, and Zoom calls automatically'
4053
+ : 'Enable to let this agent join video calls and meetings on behalf of your organization'
4054
+ )
4055
+ )
4056
+ ),
4057
+
4058
+ // When enabled, show status + options
4059
+ meetingsOn && h('div', null,
4060
+
4061
+ // Status card
4062
+ h('div', { style: { display: 'flex', gap: 12, marginBottom: 16 } },
4063
+ h('div', { className: 'card', style: { flex: 1, padding: '12px 16px' } },
4064
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 } },
4065
+ h('div', { style: {
4066
+ width: 8, height: 8, borderRadius: 4,
4067
+ background: browserStatus?.ok ? 'var(--success)' : 'var(--warning)',
4068
+ } }),
4069
+ h('span', { style: { fontSize: 13, fontWeight: 600 } }, 'Meeting Browser'),
4070
+ browserStatus?.ok && h('span', { className: 'badge', style: { fontSize: 10, padding: '1px 6px', background: 'var(--success-soft)', color: 'var(--success)' } }, 'Running')
4071
+ ),
4072
+ browserStatus?.ok
4073
+ ? h('div', { style: { fontSize: 12, color: 'var(--text-muted)' } }, browserStatus.browserVersion || 'Chromium ready')
4074
+ : h('div', null,
4075
+ h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 8 } }, 'A dedicated browser instance will be launched for video calls with virtual display and audio.'),
4076
+ h('button', { className: 'btn btn-sm', disabled: launching, onClick: launchMeetingBrowser },
4077
+ launching ? 'Launching...' : '\u25B6\uFE0F Launch Meeting Browser'
4078
+ )
4079
+ )
4080
+ )
4081
+ ),
4082
+
4083
+ // Supported platforms
4084
+ h('div', { style: { display: 'grid', gap: 8, gridTemplateColumns: '1fr 1fr 1fr', marginBottom: 16 } },
4085
+ [
4086
+ { name: 'Google Meet', icon: '\uD83D\uDFE2', enabled: cfg.meetingGoogleMeet !== false, key: 'meetingGoogleMeet', desc: 'Join via Google Calendar integration' },
4087
+ { name: 'Microsoft Teams', icon: '\uD83D\uDFE3', enabled: cfg.meetingTeams !== false, key: 'meetingTeams', desc: 'Join via meeting links' },
4088
+ { name: 'Zoom', icon: '\uD83D\uDD35', enabled: cfg.meetingZoom !== false, key: 'meetingZoom', desc: 'Join via meeting links' },
4089
+ ].map(function(p) {
4090
+ return h('div', { key: p.key, className: 'card', style: { padding: '10px 12px', cursor: 'pointer', border: '1px solid ' + (p.enabled ? 'var(--accent)' : 'var(--border)') },
4091
+ onClick: function() { update(p.key, !p.enabled); }
4092
+ },
4093
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 } },
4094
+ h('span', null, p.icon),
4095
+ h('span', { style: { fontWeight: 600, fontSize: 12 } }, p.name),
4096
+ h('span', { style: { marginLeft: 'auto', fontSize: 11, color: p.enabled ? 'var(--success)' : 'var(--text-muted)' } }, p.enabled ? 'ON' : 'OFF')
4097
+ ),
4098
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, p.desc)
4099
+ );
4100
+ })
4101
+ ),
4102
+
4103
+ // Meeting behavior
4104
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4105
+ h('div', { className: 'form-group' },
4106
+ h('label', { style: labelStyle }, 'Auto-Join Calendar Meetings'),
4107
+ h('select', { className: 'input', value: cfg.meetingAutoJoin || 'ask',
4108
+ onChange: function(e) { update('meetingAutoJoin', e.target.value); }
4109
+ },
4110
+ h('option', { value: 'always' }, 'Always — Join all meetings automatically'),
4111
+ h('option', { value: 'invited' }, 'When Invited — Only join meetings the agent is invited to'),
4112
+ h('option', { value: 'ask' }, 'Ask First — Request approval before joining'),
4113
+ h('option', { value: 'never' }, 'Manual Only — Agent only joins when explicitly told')
4114
+ ),
4115
+ h('div', { style: helpStyle }, 'How the agent decides when to join meetings.')
4116
+ ),
4117
+ h('div', { className: 'form-group' },
4118
+ h('label', { style: labelStyle }, 'Meeting Role'),
4119
+ h('select', { className: 'input', value: cfg.meetingRole || 'observer',
4120
+ onChange: function(e) { update('meetingRole', e.target.value); }
4121
+ },
4122
+ h('option', { value: 'observer' }, 'Observer — Listen and take notes only'),
4123
+ h('option', { value: 'participant' }, 'Participant — Can speak and interact'),
4124
+ h('option', { value: 'presenter' }, 'Presenter — Can share screen and present')
4125
+ ),
4126
+ h('div', { style: helpStyle }, 'What the agent is allowed to do in meetings.')
4127
+ ),
4128
+ h('div', { className: 'form-group' },
4129
+ h('label', { style: labelStyle }, 'Join Timing'),
4130
+ h('select', { className: 'input', value: cfg.meetingJoinTiming || 'ontime',
4131
+ onChange: function(e) { update('meetingJoinTiming', e.target.value); }
4132
+ },
4133
+ h('option', { value: 'early' }, 'Early — Join 2 minutes before start'),
4134
+ h('option', { value: 'ontime' }, 'On Time — Join at scheduled start'),
4135
+ h('option', { value: 'late' }, 'Fashionably Late — Join 2 minutes after start')
4136
+ )
4137
+ ),
4138
+ h('div', { className: 'form-group' },
4139
+ h('label', { style: labelStyle }, 'After Meeting'),
4140
+ h('select', { className: 'input', value: cfg.meetingAfterAction || 'notes',
4141
+ onChange: function(e) { update('meetingAfterAction', e.target.value); }
4142
+ },
4143
+ h('option', { value: 'notes' }, 'Send meeting notes to organizer'),
4144
+ h('option', { value: 'summary' }, 'Post summary to team channel'),
4145
+ h('option', { value: 'transcript' }, 'Save full transcript'),
4146
+ h('option', { value: 'nothing' }, 'Do nothing')
4147
+ ),
4148
+ h('div', { style: helpStyle }, 'What happens after the meeting ends.')
4149
+ )
4150
+ ),
4151
+
4152
+ // Display name in meetings
4153
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr', marginTop: 12 } },
4154
+ h('div', { className: 'form-group' },
4155
+ h('label', { style: labelStyle }, 'Display Name in Meetings'),
4156
+ h('input', { className: 'input', placeholder: 'Agent name (e.g. "Fola - AI Assistant")',
4157
+ value: cfg.meetingDisplayName || '',
4158
+ onChange: function(e) { update('meetingDisplayName', e.target.value || undefined); }
4159
+ }),
4160
+ h('div', { style: helpStyle }, 'How the agent appears to other participants.')
4161
+ ),
4162
+ h('div', { className: 'form-group' },
4163
+ h('label', { style: labelStyle }, 'Max Meeting Duration (minutes)'),
4164
+ h('input', { className: 'input', type: 'number', min: 5, max: 480,
4165
+ value: cfg.meetingMaxDuration || 120,
4166
+ onChange: function(e) { update('meetingMaxDuration', parseInt(e.target.value) || 120); }
4167
+ }),
4168
+ h('div', { style: helpStyle }, 'Agent will leave after this duration to prevent runaway sessions.')
4169
+ )
4170
+ )
4171
+ )
4172
+ );
4173
+ }
4174
+
3991
4175
  function BrowserConfigCard(props) {
3992
4176
  var agentId = props.agentId;
3993
4177
  var _d = useApp(); var toast = _d.toast;
@@ -4420,57 +4604,7 @@ function BrowserConfigCard(props) {
4420
4604
  ),
4421
4605
 
4422
4606
  // ─── Section 4: Meeting & Video Capabilities ─────
4423
- h('div', { style: sectionStyle },
4424
- sectionTitle('\uD83C\uDFA5', 'Meeting & Video Call Capabilities'),
4425
- h('div', { style: { padding: '10px 14px', background: 'var(--bg-secondary)', borderRadius: 'var(--radius)', marginBottom: 12, fontSize: 12, lineHeight: 1.6 } },
4426
- h('div', { style: { fontWeight: 600, marginBottom: 6 } }, 'For Google Meet, Teams, and Zoom:'),
4427
- h('div', null, '\u2022 Use ', h('strong', null, 'Remote Browser (CDP)'), ' provider pointed at a VM with display + virtual camera'),
4428
- h('div', null, '\u2022 The remote machine needs: X11/Wayland display, PulseAudio/PipeWire (audio), v4l2loopback (virtual camera)'),
4429
- h('div', null, '\u2022 Agent will navigate to meeting URL, handle permissions, and interact with the meeting UI'),
4430
- h('div', null, '\u2022 For voice participation: integrate with a Speech-to-Text / Text-to-Speech service'),
4431
- h('div', { style: { marginTop: 8 } },
4432
- h('strong', null, 'Quick VM Setup (Linux):'),
4433
- h('pre', { style: { margin: '6px 0', padding: 8, background: 'var(--bg-tertiary)', borderRadius: 4, fontSize: 11, overflow: 'auto' } },
4434
- '# Install deps\nsudo apt install -y xvfb pulseaudio chromium v4l2loopback-dkms\n\n# Start virtual display + audio\nXvfb :99 -screen 0 1920x1080x24 &\nexport DISPLAY=:99\npulseaudio --start\n\n# Launch Chrome with remote debugging\nchromium --remote-debugging-port=9222 \\\n --no-first-run --disable-gpu \\\n --use-fake-ui-for-media-stream \\\n --use-fake-device-for-media-stream \\\n --auto-accept-camera-and-microphone-capture'
4435
- )
4436
- )
4437
- ),
4438
- h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4439
- h('div', { className: 'form-group' },
4440
- h('label', { style: labelStyle }, 'Auto-Accept Permissions'),
4441
- h('select', { className: 'input', value: cfg.autoAcceptPermissions !== false ? 'true' : 'false',
4442
- onChange: function(e) { update('autoAcceptPermissions', e.target.value === 'true'); }
4443
- },
4444
- h('option', { value: 'true' }, 'Yes — Auto-grant camera/microphone access'),
4445
- h('option', { value: 'false' }, 'No — Require manual permission grants')
4446
- )
4447
- ),
4448
- h('div', { className: 'form-group' },
4449
- h('label', { style: labelStyle }, 'Virtual Camera Feed'),
4450
- h('input', { className: 'input', placeholder: '/dev/video0 or URL to video feed',
4451
- value: cfg.virtualCameraSource || '',
4452
- onChange: function(e) { update('virtualCameraSource', e.target.value || undefined); }
4453
- }),
4454
- h('div', { style: helpStyle }, 'Video source for the agent\'s "camera" in meetings.')
4455
- ),
4456
- h('div', { className: 'form-group' },
4457
- h('label', { style: labelStyle }, 'Audio Input'),
4458
- h('input', { className: 'input', placeholder: 'PulseAudio source (e.g. virtual_mic)',
4459
- value: cfg.audioInput || '',
4460
- onChange: function(e) { update('audioInput', e.target.value || undefined); }
4461
- }),
4462
- h('div', { style: helpStyle }, 'Audio source for the agent to speak in meetings.')
4463
- ),
4464
- h('div', { className: 'form-group' },
4465
- h('label', { style: labelStyle }, 'Audio Output'),
4466
- h('input', { className: 'input', placeholder: 'PulseAudio sink (e.g. virtual_speaker)',
4467
- value: cfg.audioOutput || '',
4468
- onChange: function(e) { update('audioOutput', e.target.value || undefined); }
4469
- }),
4470
- h('div', { style: helpStyle }, 'Audio sink for the agent to hear meeting participants.')
4471
- )
4472
- )
4473
- ),
4607
+ h(MeetingCapabilitiesSection, { agentId: agentId, cfg: cfg, update: update, labelStyle: labelStyle, helpStyle: helpStyle, sectionStyle: sectionStyle, sectionTitle: sectionTitle }),
4474
4608
 
4475
4609
  // ─── Section 5: Persistent Sessions ──────────────
4476
4610
  h('div', { style: { paddingTop: 12 } },
@@ -927,6 +927,106 @@ export function createAgentRoutes(opts: {
927
927
  return c.json({ config: managed.config?.browserConfig || {} });
928
928
  });
929
929
 
930
+ /**
931
+ * POST /bridge/agents/:id/browser-config/launch-meeting-browser
932
+ * Launches a meeting-ready headed Chrome instance with virtual display + audio.
933
+ * Returns the CDP URL for the agent to connect to.
934
+ */
935
+ router.post('/bridge/agents/:id/browser-config/launch-meeting-browser', async (c) => {
936
+ const agentId = c.req.param('id');
937
+ const managed = lifecycle.getAgent(agentId);
938
+ if (!managed) return c.json({ error: 'Agent not found' }, 404);
939
+
940
+ try {
941
+ const { execSync, spawn } = await import('node:child_process');
942
+ const chromePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || '/usr/bin/chromium';
943
+
944
+ // Check if a meeting browser is already running for this agent
945
+ const existingPort = (managed.config as any)?.meetingBrowserPort;
946
+ if (existingPort) {
947
+ try {
948
+ const resp = await fetch(`http://127.0.0.1:${existingPort}/json/version`, { signal: AbortSignal.timeout(2000) });
949
+ if (resp.ok) {
950
+ const data = await resp.json() as any;
951
+ return c.json({ ok: true, alreadyRunning: true, cdpUrl: data.webSocketDebuggerUrl, port: existingPort, browserVersion: data.Browser });
952
+ }
953
+ } catch { /* not running, will launch new one */ }
954
+ }
955
+
956
+ // Find available port
957
+ const net = await import('node:net');
958
+ const port = await new Promise<number>((resolve, reject) => {
959
+ const srv = net.createServer();
960
+ srv.listen(0, '127.0.0.1', () => {
961
+ const p = (srv.address() as any).port;
962
+ srv.close(() => resolve(p));
963
+ });
964
+ srv.on('error', reject);
965
+ });
966
+
967
+ // Launch Chrome with meeting-optimized flags
968
+ const chromeArgs = [
969
+ `--remote-debugging-port=${port}`,
970
+ '--remote-debugging-address=127.0.0.1',
971
+ '--no-first-run',
972
+ '--no-default-browser-check',
973
+ '--disable-background-networking',
974
+ '--disable-sync',
975
+ '--disable-translate',
976
+ '--metrics-recording-only',
977
+ '--no-sandbox',
978
+ // Meeting-specific: auto-grant camera/mic permissions
979
+ '--use-fake-ui-for-media-stream',
980
+ '--auto-accept-camera-and-microphone-capture',
981
+ // Use virtual audio
982
+ '--use-fake-device-for-media-stream',
983
+ // Window size for meeting UI
984
+ '--window-size=1920,1080',
985
+ '--start-maximized',
986
+ // User data dir for persistent logins
987
+ `/tmp/meeting-browser-${agentId.slice(0, 8)}`,
988
+ ];
989
+
990
+ const child = spawn(chromePath, chromeArgs, {
991
+ detached: true,
992
+ stdio: 'ignore',
993
+ env: { ...process.env, DISPLAY: ':99' },
994
+ });
995
+ child.unref();
996
+
997
+ // Wait for Chrome to be ready
998
+ let cdpUrl = '';
999
+ let browserVersion = '';
1000
+ for (let i = 0; i < 30; i++) {
1001
+ await new Promise(r => setTimeout(r, 500));
1002
+ try {
1003
+ const resp = await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(2000) });
1004
+ if (resp.ok) {
1005
+ const data = await resp.json() as any;
1006
+ cdpUrl = data.webSocketDebuggerUrl;
1007
+ browserVersion = data.Browser;
1008
+ break;
1009
+ }
1010
+ } catch { /* retry */ }
1011
+ }
1012
+
1013
+ if (!cdpUrl) {
1014
+ return c.json({ error: 'Chrome launched but CDP not responding after 15s' });
1015
+ }
1016
+
1017
+ // Save port to agent config for reuse
1018
+ if (!managed.config) managed.config = {} as any;
1019
+ (managed.config as any).meetingBrowserPort = port;
1020
+ (managed.config as any).meetingBrowserCdpUrl = cdpUrl;
1021
+ managed.updatedAt = new Date().toISOString();
1022
+ await lifecycle.saveAgent(agentId);
1023
+
1024
+ return c.json({ ok: true, cdpUrl, port, browserVersion, pid: child.pid });
1025
+ } catch (e: any) {
1026
+ return c.json({ error: 'Failed to launch meeting browser: ' + e.message });
1027
+ }
1028
+ });
1029
+
930
1030
  router.post('/bridge/agents/:id/browser-config/test', async (c) => {
931
1031
  const agentId = c.req.param('id');
932
1032
  const managed = lifecycle.getAgent(agentId);
@@ -1022,10 +1022,11 @@ export class EngineDatabase {
1022
1022
  }
1023
1023
 
1024
1024
  private rowToManagedAgent(row: any): ManagedAgent {
1025
- return {
1025
+ const config = sj(row.config);
1026
+ const agent: ManagedAgent = {
1026
1027
  id: row.id,
1027
1028
  orgId: row.org_id,
1028
- config: sj(row.config),
1029
+ config,
1029
1030
  state: row.state,
1030
1031
  stateHistory: [], // Loaded separately via getStateHistory
1031
1032
  health: sj(row.health || '{}'),
@@ -1036,6 +1037,11 @@ export class EngineDatabase {
1036
1037
  lastHealthCheckAt: row.last_health_check_at,
1037
1038
  version: row.version,
1038
1039
  };
1040
+ // Restore budgetConfig from config JSON
1041
+ if ((config as any)?.budgetConfig) {
1042
+ agent.budgetConfig = (config as any).budgetConfig;
1043
+ }
1044
+ return agent;
1039
1045
  }
1040
1046
 
1041
1047
  private rowToOrg(row: any): Organization {
@@ -708,6 +708,9 @@ export class AgentLifecycleManager {
708
708
  const agent = this.agents.get(agentId);
709
709
  if (!agent) throw new Error(`Agent ${agentId} not found`);
710
710
  agent.budgetConfig = config;
711
+ // Also store in config JSON so it survives DB round-trips
712
+ if (!agent.config) agent.config = {} as any;
713
+ (agent.config as any).budgetConfig = config;
711
714
  agent.updatedAt = new Date().toISOString();
712
715
  await this.persistAgent(agent);
713
716
  }
@@ -716,7 +719,13 @@ export class AgentLifecycleManager {
716
719
  * Get per-agent budget configuration
717
720
  */
718
721
  getBudgetConfig(agentId: string): AgentBudgetConfig | undefined {
719
- return this.agents.get(agentId)?.budgetConfig;
722
+ const agent = this.agents.get(agentId);
723
+ if (!agent) return undefined;
724
+ // Restore from config JSON if not on top-level
725
+ if (!agent.budgetConfig && (agent.config as any)?.budgetConfig) {
726
+ agent.budgetConfig = (agent.config as any).budgetConfig;
727
+ }
728
+ return agent.budgetConfig;
720
729
  }
721
730
 
722
731
  /**