neoagent 3.2.1-beta.1 → 3.2.1-beta.10

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 (172) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +111 -20
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +3 -0
  8. package/flutter_app/lib/main_operations.dart +334 -321
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +4 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +78 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +11 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +73083 -72209
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/browser.js +23 -15
  33. package/server/routes/desktop.js +18 -1
  34. package/server/routes/integrations.js +107 -1
  35. package/server/routes/memory.js +1 -0
  36. package/server/routes/settings.js +16 -5
  37. package/server/routes/social_reach.js +12 -3
  38. package/server/routes/social_video.js +4 -0
  39. package/server/services/agents/manager.js +1 -1
  40. package/server/services/ai/capabilityHealth.js +62 -96
  41. package/server/services/ai/compaction.js +7 -2
  42. package/server/services/ai/history.js +45 -6
  43. package/server/services/ai/integrated_tools/http_request.js +8 -0
  44. package/server/services/ai/loop/agent_engine_core.js +390 -162
  45. package/server/services/ai/loop/blank_recovery.js +5 -4
  46. package/server/services/ai/loop/callbacks.js +1 -0
  47. package/server/services/ai/loop/completion_judge.js +121 -5
  48. package/server/services/ai/loop/conversation_loop.js +510 -332
  49. package/server/services/ai/loop/messaging_delivery.js +129 -57
  50. package/server/services/ai/loop/model_call_guard.js +91 -0
  51. package/server/services/ai/loop/model_io.js +20 -45
  52. package/server/services/ai/loop/progress_classification.js +2 -0
  53. package/server/services/ai/loop/tool_dispatch.js +19 -8
  54. package/server/services/ai/loopPolicy.js +48 -21
  55. package/server/services/ai/messagingFallback.js +17 -17
  56. package/server/services/ai/model_discovery.js +227 -0
  57. package/server/services/ai/model_failure_cache.js +108 -0
  58. package/server/services/ai/model_identity.js +71 -0
  59. package/server/services/ai/models.js +68 -163
  60. package/server/services/ai/providerRetry.js +17 -59
  61. package/server/services/ai/provider_selector.js +166 -0
  62. package/server/services/ai/providers/anthropic.js +2 -2
  63. package/server/services/ai/providers/claudeCode.js +21 -33
  64. package/server/services/ai/providers/githubCopilot.js +41 -20
  65. package/server/services/ai/providers/google.js +135 -97
  66. package/server/services/ai/providers/grok.js +4 -3
  67. package/server/services/ai/providers/grokOauth.js +19 -27
  68. package/server/services/ai/providers/nvidia.js +10 -5
  69. package/server/services/ai/providers/ollama.js +111 -84
  70. package/server/services/ai/providers/ollama_stream.js +142 -0
  71. package/server/services/ai/providers/openai.js +39 -5
  72. package/server/services/ai/providers/openaiCodex.js +11 -4
  73. package/server/services/ai/providers/openrouter.js +29 -7
  74. package/server/services/ai/providers/provider_error.js +36 -0
  75. package/server/services/ai/settings.js +26 -2
  76. package/server/services/ai/systemPrompt.js +19 -12
  77. package/server/services/ai/taskAnalysis.js +58 -10
  78. package/server/services/ai/terminal_reply.js +18 -0
  79. package/server/services/ai/toolEvidence.js +350 -29
  80. package/server/services/ai/tools.js +190 -111
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/browser/controller.js +843 -385
  87. package/server/services/browser/extension/gateway.js +40 -16
  88. package/server/services/browser/extension/protocol.js +15 -1
  89. package/server/services/browser/extension/provider.js +71 -47
  90. package/server/services/browser/extension/registry.js +155 -34
  91. package/server/services/cli/executor.js +62 -9
  92. package/server/services/credentials/bitwarden_cli.js +322 -0
  93. package/server/services/credentials/broker.js +594 -0
  94. package/server/services/desktop/gateway.js +41 -4
  95. package/server/services/desktop/protocol.js +3 -0
  96. package/server/services/desktop/provider.js +39 -42
  97. package/server/services/desktop/registry.js +137 -52
  98. package/server/services/integrations/bitwarden/constants.js +14 -0
  99. package/server/services/integrations/bitwarden/provider.js +197 -0
  100. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  101. package/server/services/integrations/figma/provider.js +78 -12
  102. package/server/services/integrations/github/common.js +11 -6
  103. package/server/services/integrations/github/provider.js +52 -53
  104. package/server/services/integrations/google/provider.js +55 -19
  105. package/server/services/integrations/home_assistant/network.js +17 -20
  106. package/server/services/integrations/home_assistant/provider.js +7 -5
  107. package/server/services/integrations/home_assistant/tools.js +17 -5
  108. package/server/services/integrations/http.js +51 -0
  109. package/server/services/integrations/manager.js +159 -53
  110. package/server/services/integrations/microsoft/provider.js +80 -13
  111. package/server/services/integrations/neoarchive/provider.js +55 -29
  112. package/server/services/integrations/neorecall/client.js +17 -10
  113. package/server/services/integrations/neorecall/provider.js +20 -11
  114. package/server/services/integrations/notion/provider.js +16 -13
  115. package/server/services/integrations/oauth_provider.js +115 -51
  116. package/server/services/integrations/registry.js +2 -0
  117. package/server/services/integrations/slack/provider.js +98 -9
  118. package/server/services/integrations/spotify/provider.js +67 -71
  119. package/server/services/integrations/trello/provider.js +21 -7
  120. package/server/services/integrations/weather/provider.js +18 -12
  121. package/server/services/integrations/whatsapp/provider.js +76 -16
  122. package/server/services/manager.js +110 -1
  123. package/server/services/memory/embedding_index.js +20 -8
  124. package/server/services/memory/embeddings.js +151 -90
  125. package/server/services/memory/ingestion.js +50 -9
  126. package/server/services/memory/ingestion_documents.js +13 -3
  127. package/server/services/memory/manager.js +52 -19
  128. package/server/services/messaging/automation.js +85 -10
  129. package/server/services/messaging/formatting_guides.js +7 -4
  130. package/server/services/messaging/http_platforms.js +33 -13
  131. package/server/services/messaging/inbound_queue.js +78 -24
  132. package/server/services/messaging/inbound_store.js +224 -0
  133. package/server/services/messaging/manager.js +326 -51
  134. package/server/services/messaging/typing_keepalive.js +5 -2
  135. package/server/services/messaging/whatsapp.js +22 -14
  136. package/server/services/network/http.js +210 -0
  137. package/server/services/network/safe_request.js +307 -0
  138. package/server/services/runtime/backends/local-vm.js +227 -67
  139. package/server/services/runtime/docker-vm-manager.js +9 -0
  140. package/server/services/runtime/guest_bootstrap.js +30 -4
  141. package/server/services/runtime/guest_image.js +43 -12
  142. package/server/services/runtime/manager.js +77 -23
  143. package/server/services/runtime/validation.js +7 -6
  144. package/server/services/security/tool_categories.js +6 -0
  145. package/server/services/social_reach/channels/github.js +10 -4
  146. package/server/services/social_reach/channels/reddit.js +4 -4
  147. package/server/services/social_reach/channels/rss.js +2 -2
  148. package/server/services/social_reach/channels/social_video.js +12 -7
  149. package/server/services/social_reach/channels/v2ex.js +21 -8
  150. package/server/services/social_reach/channels/x.js +2 -2
  151. package/server/services/social_reach/channels/xueqiu.js +5 -5
  152. package/server/services/social_reach/service.js +9 -6
  153. package/server/services/social_reach/utils.js +65 -14
  154. package/server/services/social_video/service.js +160 -50
  155. package/server/services/tasks/integration_runtime.js +18 -8
  156. package/server/services/tasks/runtime.js +39 -4
  157. package/server/services/voice/agentBridge.js +17 -4
  158. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  159. package/server/services/voice/liveSession.js +31 -0
  160. package/server/services/voice/message.js +1 -1
  161. package/server/services/voice/openaiSpeech.js +33 -8
  162. package/server/services/voice/providers.js +233 -151
  163. package/server/services/voice/runtime.js +2 -2
  164. package/server/services/voice/runtimeManager.js +118 -20
  165. package/server/services/voice/turnRunner.js +6 -0
  166. package/server/services/wearable/firmware_manifest.js +51 -13
  167. package/server/services/wearable/service.js +1 -0
  168. package/server/utils/abort.js +96 -0
  169. package/server/utils/cloud-security.js +110 -3
  170. package/server/utils/files.js +31 -0
  171. package/server/utils/image_payload.js +95 -0
  172. package/server/utils/retry.js +107 -0
@@ -1025,7 +1025,7 @@ router.put('/api/config/rate-limits', requireAdminAuth, express.json(), (req, re
1025
1025
  router.get('/api/models', requireAdminAuth, async (req, res) => {
1026
1026
  const { getSupportedModels } = require('../services/ai/models');
1027
1027
  try {
1028
- const models = await getSupportedModels(null, null);
1028
+ const models = await getSupportedModels(null, null, { signal: req.signal });
1029
1029
  const disabledStr = process.env.NEOAGENT_DISABLED_MODELS || '';
1030
1030
  const disabledModels = disabledStr ? disabledStr.split(',').map(s => s.trim()).filter(Boolean) : [];
1031
1031
  res.json({ models, disabledModels });
@@ -1,3 +1,5 @@
1
+ 'use strict';
2
+
1
3
  const express = require('express');
2
4
  const fs = require('fs');
3
5
  const path = require('path');
@@ -6,8 +8,7 @@ const router = express.Router();
6
8
  const { DATA_DIR } = require('../../runtime/paths');
7
9
  const { requireAuth } = require('../middleware/auth');
8
10
  const { sanitizeError } = require('../utils/security');
9
- const { validateCloudUrl } = require('../utils/cloud-security');
10
- const { getRuntimeValidation } = require('../services/runtime/validation');
11
+ const { validateAndroidIntentUrl } = require('../utils/cloud-security');
11
12
 
12
13
  router.use(requireAuth);
13
14
 
@@ -52,18 +53,6 @@ async function getAndroidController(req) {
52
53
  throw new Error('Android controller is unavailable.');
53
54
  }
54
55
 
55
- function getAndroidStatusSnapshot(req) {
56
- const runtimeValidation = getRuntimeValidation(req.app?.locals?.runtimeManager);
57
- const ready = Boolean(runtimeValidation?.ready);
58
- return {
59
- bootstrapped: false,
60
- canBootstrap: ready,
61
- devices: [],
62
- lastStartError: ready ? null : (runtimeValidation?.issues?.[0] || 'VM runtime is not ready.'),
63
- runtimeReady: ready,
64
- };
65
- }
66
-
67
56
  function handleAndroidAction(action) {
68
57
  return async (req, res) => {
69
58
  try {
@@ -79,45 +68,49 @@ function handleAndroidAction(action) {
79
68
  router.get('/status', async (req, res) => {
80
69
  try {
81
70
  const controller = await getAndroidController(req);
82
- res.json(await controller.getStatus().catch(() => getAndroidStatusSnapshot(req)));
71
+ res.json(await controller.getStatus({ signal: req.signal }));
83
72
  } catch (err) {
84
73
  res.status(500).json({ error: sanitizeError(err) });
85
74
  }
86
75
  });
87
76
 
88
77
  router.post('/start', handleAndroidAction((controller, req) =>
89
- controller.requestStartEmulator(req.body || {})));
78
+ controller.requestStartEmulator({ ...(req.body || {}), signal: req.signal })));
90
79
 
91
- router.post('/stop', handleAndroidAction((controller) => controller.stopEmulator()));
80
+ router.post('/stop', handleAndroidAction((controller, req) => controller.stopEmulator({ signal: req.signal })));
92
81
 
93
- router.get('/devices', handleAndroidAction(async (controller) => ({
94
- devices: await controller.listDevices(),
82
+ router.get('/devices', handleAndroidAction(async (controller, req) => ({
83
+ devices: await controller.listDevices({ signal: req.signal }),
95
84
  })));
96
85
 
97
86
  router.post('/screenshot', handleAndroidAction((controller, req) =>
98
- controller.screenshot(req.body || {})));
87
+ controller.screenshot({ ...(req.body || {}), signal: req.signal })));
99
88
 
100
89
  router.post('/observe', handleAndroidAction((controller, req) =>
101
- controller.observe(req.body || {})));
90
+ controller.observe({ ...(req.body || {}), signal: req.signal })));
102
91
 
103
92
  router.post('/ui-dump', handleAndroidAction((controller, req) =>
104
- controller.dumpUi(req.body || {})));
93
+ controller.dumpUi({ ...(req.body || {}), signal: req.signal })));
105
94
 
106
95
  router.get('/apps', handleAndroidAction((controller, req) =>
107
- controller.listApps({ includeSystem: req.query.includeSystem === 'true' })));
96
+ controller.listApps({ includeSystem: req.query.includeSystem === 'true', signal: req.signal })));
108
97
 
109
98
  router.post('/open-app', handleAndroidAction((controller, req) =>
110
- controller.openApp(req.body || {})));
99
+ controller.openApp({ ...(req.body || {}), signal: req.signal })));
111
100
 
112
101
  router.post('/open-intent', async (req, res) => {
113
102
  try {
114
103
  const body = req.body || {};
115
104
  const intentUrl = body.data || body.url || body.uri;
116
- if (intentUrl && typeof intentUrl === 'string' && !validateCloudUrl(intentUrl).allowed) {
105
+ if (
106
+ intentUrl
107
+ && typeof intentUrl === 'string'
108
+ && !(await validateAndroidIntentUrl(intentUrl, { signal: req.signal })).allowed
109
+ ) {
117
110
  return res.status(403).json({ error: 'This URL is not permitted.' });
118
111
  }
119
112
  const controller = await getAndroidController(req);
120
- const result = await controller.openIntent(body);
113
+ const result = await controller.openIntent({ ...body, signal: req.signal });
121
114
  res.json(result);
122
115
  } catch (err) {
123
116
  res.status(500).json({ error: sanitizeError(err) });
@@ -125,22 +118,22 @@ router.post('/open-intent', async (req, res) => {
125
118
  });
126
119
 
127
120
  router.post('/tap', handleAndroidAction((controller, req) =>
128
- controller.tap(req.body || {})));
121
+ controller.tap({ ...(req.body || {}), signal: req.signal })));
129
122
 
130
123
  router.post('/long-press', handleAndroidAction((controller, req) =>
131
- controller.longPress(req.body || {})));
124
+ controller.longPress({ ...(req.body || {}), signal: req.signal })));
132
125
 
133
126
  router.post('/type', handleAndroidAction((controller, req) =>
134
- controller.type(req.body || {})));
127
+ controller.type({ ...(req.body || {}), signal: req.signal })));
135
128
 
136
129
  router.post('/swipe', handleAndroidAction((controller, req) =>
137
- controller.swipe(req.body || {})));
130
+ controller.swipe({ ...(req.body || {}), signal: req.signal })));
138
131
 
139
132
  router.post('/press-key', handleAndroidAction((controller, req) =>
140
- controller.pressKey(req.body || {})));
133
+ controller.pressKey({ ...(req.body || {}), signal: req.signal })));
141
134
 
142
135
  router.post('/wait-for', handleAndroidAction((controller, req) =>
143
- controller.waitFor(req.body || {})));
136
+ controller.waitFor({ ...(req.body || {}), signal: req.signal })));
144
137
 
145
138
  router.post('/install-apk', (req, res) => {
146
139
  androidApkUpload.single('apk')(req, res, async (uploadError) => {
@@ -162,7 +155,7 @@ router.post('/install-apk', (req, res) => {
162
155
 
163
156
  try {
164
157
  const controller = await getAndroidController(req);
165
- const result = await controller.installApk({ apkPath: uploadedApkPath });
158
+ const result = await controller.installApk({ apkPath: uploadedApkPath, signal: req.signal });
166
159
  res.json({
167
160
  ...result,
168
161
  filename: req.file.originalname,
@@ -176,6 +169,9 @@ router.post('/install-apk', (req, res) => {
176
169
  });
177
170
  });
178
171
 
179
- router.post('/shell', handleAndroidAction((controller, req) => controller.shell(req.body || {})));
172
+ router.post('/shell', handleAndroidAction((controller, req) => controller.shell({
173
+ ...(req.body || {}),
174
+ signal: req.signal,
175
+ })));
180
176
 
181
177
  module.exports = router;
@@ -1,8 +1,10 @@
1
+ 'use strict';
2
+
1
3
  const express = require('express');
2
4
  const router = express.Router();
3
5
  const { requireAuth } = require('../middleware/auth');
4
6
  const { sanitizeError } = require('../utils/security');
5
- const { validateCloudUrl } = require('../utils/cloud-security');
7
+ const { validateCloudUrlWithDns } = require('../utils/cloud-security');
6
8
  const { getRuntimeValidation } = require('../services/runtime/validation');
7
9
 
8
10
  router.use(requireAuth);
@@ -11,7 +13,9 @@ async function getBrowserController(req) {
11
13
  const runtimeManager = req.app?.locals?.runtimeManager;
12
14
  if (runtimeManager && typeof runtimeManager.getBrowserProviderForUser === 'function') {
13
15
  const start = Date.now();
14
- const runtimeController = await runtimeManager.getBrowserProviderForUser(req.session?.userId);
16
+ const runtimeController = await runtimeManager.getBrowserProviderForUser(req.session?.userId, {
17
+ signal: req.signal,
18
+ });
15
19
  const duration = Date.now() - start;
16
20
  if (duration > 1000) {
17
21
  console.log(`[HTTP] Browser controller acquired for user ${req.session?.userId} in ${duration}ms`);
@@ -73,7 +77,7 @@ router.get('/cookies', async (req, res) => {
73
77
  if (typeof bc.getCookies !== 'function') {
74
78
  return res.status(501).json({ error: 'Cookie export is unavailable for this browser provider.' });
75
79
  }
76
- const result = await bc.getCookies();
80
+ const result = await bc.getCookies({ signal: req.signal });
77
81
  res.json(result);
78
82
  } catch (err) {
79
83
  res.status(500).json({ error: sanitizeError(err) });
@@ -84,7 +88,7 @@ router.get('/cookies', async (req, res) => {
84
88
  router.post('/launch', async (req, res) => {
85
89
  try {
86
90
  const bc = await getBrowserController(req);
87
- await bc.launch(req.body || {});
91
+ await bc.launch({ ...(req.body || {}), signal: req.signal });
88
92
  res.json({ success: true });
89
93
  } catch (err) {
90
94
  res.status(500).json({ error: sanitizeError(err) });
@@ -97,12 +101,12 @@ router.post('/navigate', async (req, res) => {
97
101
  const { url, waitFor } = req.body;
98
102
  if (!url) return res.status(400).json({ error: 'url required' });
99
103
 
100
- const bc = await getBrowserController(req);
101
-
102
- if (bc.providerType === 'vm' && !validateCloudUrl(url).allowed) {
104
+ const urlValidation = await validateCloudUrlWithDns(url, { signal: req.signal });
105
+ if (!urlValidation.allowed) {
103
106
  return res.status(403).json({ error: 'This URL is not permitted.' });
104
107
  }
105
108
 
109
+ const bc = await getBrowserController(req);
106
110
  const result = await bc.navigate(url, {
107
111
  waitUntil: waitFor || req.body?.waitUntil || 'domcontentloaded',
108
112
  waitFor,
@@ -110,6 +114,7 @@ router.post('/navigate', async (req, res) => {
110
114
  fullPage: req.body?.fullPage === true,
111
115
  referrerMode: req.body?.referrerMode,
112
116
  challengeRetry: req.body?.challengeRetry,
117
+ signal: req.signal,
113
118
  });
114
119
  res.json(result);
115
120
  } catch (err) {
@@ -121,7 +126,7 @@ router.post('/navigate', async (req, res) => {
121
126
  router.post('/screenshot', async (req, res) => {
122
127
  try {
123
128
  const bc = await getBrowserController(req);
124
- const result = await bc.screenshot(req.body || {});
129
+ const result = await bc.screenshot({ ...(req.body || {}), signal: req.signal });
125
130
  res.json(result);
126
131
  } catch (err) {
127
132
  res.status(500).json({ error: sanitizeError(err) });
@@ -133,7 +138,7 @@ router.post('/click', async (req, res) => {
133
138
  try {
134
139
  const { selector, text } = req.body;
135
140
  const bc = await getBrowserController(req);
136
- const result = await bc.click(selector, text, req.body?.screenshot !== false);
141
+ const result = await bc.click(selector, text, req.body?.screenshot !== false, { signal: req.signal });
137
142
  res.json(result);
138
143
  } catch (err) {
139
144
  res.status(500).json({ error: sanitizeError(err) });
@@ -147,7 +152,7 @@ router.post('/click-point', async (req, res) => {
147
152
  return res.status(400).json({ error: 'x and y required' });
148
153
  }
149
154
  const bc = await getBrowserController(req);
150
- const result = await bc.clickPoint(x, y, req.body?.screenshot !== false);
155
+ const result = await bc.clickPoint(x, y, req.body?.screenshot !== false, { signal: req.signal });
151
156
  res.json(result);
152
157
  } catch (err) {
153
158
  res.status(500).json({ error: sanitizeError(err) });
@@ -161,7 +166,7 @@ router.post('/mouse-move', async (req, res) => {
161
166
  return res.status(400).json({ error: 'x and y required' });
162
167
  }
163
168
  const bc = await getBrowserController(req);
164
- const result = await bc.hoverPoint(x, y, req.body || {});
169
+ const result = await bc.hoverPoint(x, y, { ...(req.body || {}), signal: req.signal });
165
170
  res.json(result);
166
171
  } catch (err) {
167
172
  res.status(500).json({ error: sanitizeError(err) });
@@ -179,6 +184,7 @@ router.post('/fill', async (req, res) => {
179
184
  clear: req.body?.clear !== false,
180
185
  pressEnter: req.body?.pressEnter === true,
181
186
  screenshot: req.body?.screenshot !== false,
187
+ signal: req.signal,
182
188
  });
183
189
  res.json(result);
184
190
  } catch (err) {
@@ -193,6 +199,7 @@ router.post('/type-text', async (req, res) => {
193
199
  const result = await bc.typeText(String(text || ''), {
194
200
  pressEnter: req.body?.pressEnter === true,
195
201
  screenshot: req.body?.screenshot !== false,
202
+ signal: req.signal,
196
203
  });
197
204
  res.json(result);
198
205
  } catch (err) {
@@ -205,7 +212,7 @@ router.post('/press-key', async (req, res) => {
205
212
  const { key } = req.body || {};
206
213
  if (!key) return res.status(400).json({ error: 'key required' });
207
214
  const bc = await getBrowserController(req);
208
- const result = await bc.pressKey(key, req.body?.screenshot !== false);
215
+ const result = await bc.pressKey(key, req.body?.screenshot !== false, { signal: req.signal });
209
216
  res.json(result);
210
217
  } catch (err) {
211
218
  res.status(500).json({ error: sanitizeError(err) });
@@ -219,6 +226,7 @@ router.post('/scroll', async (req, res) => {
219
226
  req.body?.deltaX ?? 0,
220
227
  req.body?.deltaY ?? 0,
221
228
  req.body?.screenshot !== false,
229
+ { signal: req.signal },
222
230
  );
223
231
  res.json(result);
224
232
  } catch (err) {
@@ -230,7 +238,7 @@ router.post('/scroll', async (req, res) => {
230
238
  router.post('/extract', async (req, res) => {
231
239
  try {
232
240
  const bc = await getBrowserController(req);
233
- const result = await bc.extractContent(req.body || {});
241
+ const result = await bc.extractContent({ ...(req.body || {}), signal: req.signal });
234
242
  res.json(result);
235
243
  } catch (err) {
236
244
  res.status(500).json({ error: sanitizeError(err) });
@@ -252,7 +260,7 @@ router.post('/execute', async (req, res) => {
252
260
  }
253
261
 
254
262
  const bc = await getBrowserController(req);
255
- const result = await bc.executeJS(code);
263
+ const result = await bc.executeJS(code, { signal: req.signal });
256
264
  res.json({ result });
257
265
  } catch (err) {
258
266
  res.status(500).json({ error: sanitizeError(err) });
@@ -263,7 +271,7 @@ router.post('/execute', async (req, res) => {
263
271
  router.post('/close', async (req, res) => {
264
272
  try {
265
273
  const bc = await getBrowserController(req);
266
- await bc.closeBrowser();
274
+ await bc.closeBrowser({ signal: req.signal });
267
275
  res.json({ success: true });
268
276
  } catch (err) {
269
277
  res.status(500).json({ error: sanitizeError(err) });
@@ -1,3 +1,5 @@
1
+ 'use strict';
2
+
1
3
  const express = require('express');
2
4
  const router = express.Router();
3
5
  const { requireAuth } = require('../middleware/auth');
@@ -144,6 +146,7 @@ router.post('/screenshot', (req, res) =>
144
146
  handleDesktopAction(req, res, (provider, request) => provider.screenshot({
145
147
  ...(request.body || {}),
146
148
  deviceId: parseDeviceId(request.body?.deviceId),
149
+ signal: request.signal,
147
150
  })));
148
151
 
149
152
  router.post('/observe', (req, res) =>
@@ -151,6 +154,7 @@ router.post('/observe', (req, res) =>
151
154
  ...(request.body || {}),
152
155
  deviceId: parseDeviceId(request.body?.deviceId),
153
156
  includeTree: parseOptionalBoolean(request.body?.includeTree, false),
157
+ signal: request.signal,
154
158
  })));
155
159
 
156
160
  router.post('/click', (req, res) =>
@@ -160,6 +164,7 @@ router.post('/click', (req, res) =>
160
164
  {
161
165
  ...(request.body || {}),
162
166
  deviceId: parseDeviceId(request.body?.deviceId),
167
+ signal: request.signal,
163
168
  },
164
169
  )));
165
170
 
@@ -170,6 +175,7 @@ router.post('/mouse-move', (req, res) =>
170
175
  {
171
176
  ...(request.body || {}),
172
177
  deviceId: parseDeviceId(request.body?.deviceId),
178
+ signal: request.signal,
173
179
  },
174
180
  )));
175
181
 
@@ -182,6 +188,7 @@ router.post('/drag', (req, res) =>
182
188
  x2: parseFiniteNumber(request.body?.x2, 'x2', { min: -100000, max: 100000 }),
183
189
  y2: parseFiniteNumber(request.body?.y2, 'y2', { min: -100000, max: 100000 }),
184
190
  durationMs: Math.round(parseFiniteNumber(request.body?.durationMs ?? 280, 'durationMs', { min: 20, max: 5000 })),
191
+ signal: request.signal,
185
192
  })));
186
193
 
187
194
  router.post('/scroll', (req, res) =>
@@ -190,6 +197,7 @@ router.post('/scroll', (req, res) =>
190
197
  deviceId: parseDeviceId(request.body?.deviceId),
191
198
  deltaX: Math.round(parseFiniteNumber(request.body?.deltaX ?? 0, 'deltaX', { min: -5000, max: 5000 })),
192
199
  deltaY: Math.round(parseFiniteNumber(request.body?.deltaY ?? 0, 'deltaY', { min: -5000, max: 5000 })),
200
+ signal: request.signal,
193
201
  })));
194
202
 
195
203
  router.post('/type-text', (req, res) =>
@@ -199,6 +207,7 @@ router.post('/type-text', (req, res) =>
199
207
  ...(request.body || {}),
200
208
  deviceId: parseDeviceId(request.body?.deviceId),
201
209
  pressEnter: parseOptionalBoolean(request.body?.pressEnter, false),
210
+ signal: request.signal,
202
211
  },
203
212
  )));
204
213
 
@@ -208,6 +217,7 @@ router.post('/press-key', (req, res) =>
208
217
  {
209
218
  ...(request.body || {}),
210
219
  deviceId: parseDeviceId(request.body?.deviceId),
220
+ signal: request.signal,
211
221
  },
212
222
  )));
213
223
 
@@ -216,11 +226,15 @@ router.post('/launch-app', (req, res) =>
216
226
  ...(request.body || {}),
217
227
  deviceId: parseDeviceId(request.body?.deviceId),
218
228
  app: parseRequiredString(request.body?.app, 'app', MAX_APP_LENGTH),
229
+ signal: request.signal,
219
230
  })));
220
231
 
221
232
  router.get('/displays', (req, res) =>
222
233
  handleDesktopAction(req, res, (provider, request) =>
223
- provider.listDisplays(parseTypedDisplayQuery(request.query))));
234
+ provider.listDisplays({
235
+ ...parseTypedDisplayQuery(request.query),
236
+ signal: request.signal,
237
+ })));
224
238
 
225
239
  router.post('/select-display', (req, res) =>
226
240
  handleDesktopAction(req, res, (provider, request) => provider.selectDisplay(
@@ -228,6 +242,7 @@ router.post('/select-display', (req, res) =>
228
242
  {
229
243
  ...(request.body || {}),
230
244
  deviceId: parseDeviceId(request.body?.deviceId),
245
+ signal: request.signal,
231
246
  },
232
247
  )));
233
248
 
@@ -239,12 +254,14 @@ router.post('/pause-device', (req, res) =>
239
254
  handleDesktopAction(req, res, (provider, request) => provider.pauseDevice(
240
255
  parseRequiredString(request.body?.deviceId, 'deviceId', 256),
241
256
  parseOptionalBoolean(request.body?.paused, true),
257
+ { signal: request.signal },
242
258
  )));
243
259
 
244
260
  router.post('/tree', (req, res) =>
245
261
  handleDesktopAction(req, res, (provider, request) => provider.getAccessibilityTree({
246
262
  ...(request.body || {}),
247
263
  deviceId: parseDeviceId(request.body?.deviceId),
264
+ signal: request.signal,
248
265
  })));
249
266
 
250
267
  module.exports = router;
@@ -17,6 +17,14 @@ function getAuthProviderManager(req) {
17
17
  return req.app?.locals?.authProviderManager;
18
18
  }
19
19
 
20
+ function getCredentialScope(req) {
21
+ const userId = Number(req.session.userId);
22
+ return {
23
+ userId,
24
+ agentId: resolveAgentId(userId, getAgentIdFromRequest(req)),
25
+ };
26
+ }
27
+
20
28
  function escapeHtml(value) {
21
29
  return String(value || '')
22
30
  .replace(/&/g, '&')
@@ -101,7 +109,7 @@ router.get('/oauth/callback', async (req, res) => {
101
109
  if (!manager) {
102
110
  throw new Error('Official integration manager is not available on app.locals.integrationManager.');
103
111
  }
104
- const result = await manager.finishOAuth(state, code);
112
+ const result = await manager.finishOAuth(state, code, { signal: req.signal });
105
113
  const payload = JSON.stringify({
106
114
  type: 'integration_oauth_success',
107
115
  provider: result.provider,
@@ -144,6 +152,100 @@ router.get('/oauth/callback', async (req, res) => {
144
152
 
145
153
  router.use(requireAuth);
146
154
 
155
+ router.post('/bitwarden/unlock', async (req, res) => {
156
+ try {
157
+ const cli = req.app?.locals?.bitwardenCli;
158
+ if (!cli) throw new Error('Bitwarden credential service is unavailable.');
159
+ const scope = getCredentialScope(req);
160
+ const status = await cli.unlock(
161
+ scope.userId,
162
+ scope.agentId,
163
+ req.body?.masterPassword,
164
+ req.body?.idleTimeoutMinutes,
165
+ { signal: req.signal },
166
+ );
167
+ res.json(status);
168
+ } catch (err) {
169
+ res.status(400).json({ error: sanitizeError(err), code: err.code || null });
170
+ }
171
+ });
172
+
173
+ router.post('/bitwarden/lock', async (req, res) => {
174
+ try {
175
+ const cli = req.app?.locals?.bitwardenCli;
176
+ if (!cli) throw new Error('Bitwarden credential service is unavailable.');
177
+ const scope = getCredentialScope(req);
178
+ res.json(await cli.lock(scope.userId, scope.agentId));
179
+ } catch (err) {
180
+ res.status(400).json({ error: sanitizeError(err), code: err.code || null });
181
+ }
182
+ });
183
+
184
+ router.get('/bitwarden/items', async (req, res) => {
185
+ try {
186
+ const broker = req.app?.locals?.credentialBroker;
187
+ if (!broker) throw new Error('Credential broker is unavailable.');
188
+ const scope = getCredentialScope(req);
189
+ res.json({ items: await broker.listVaultItems(scope.userId, scope.agentId, { signal: req.signal }) });
190
+ } catch (err) {
191
+ res.status(400).json({ error: sanitizeError(err), code: err.code || null });
192
+ }
193
+ });
194
+
195
+ router.get('/bitwarden/bindings', (req, res) => {
196
+ try {
197
+ const broker = req.app?.locals?.credentialBroker;
198
+ if (!broker) throw new Error('Credential broker is unavailable.');
199
+ const scope = getCredentialScope(req);
200
+ res.json({ bindings: broker.listBindings(scope.userId, scope.agentId) });
201
+ } catch (err) {
202
+ res.status(400).json({ error: sanitizeError(err), code: err.code || null });
203
+ }
204
+ });
205
+
206
+ router.post('/bitwarden/bindings', async (req, res) => {
207
+ try {
208
+ const broker = req.app?.locals?.credentialBroker;
209
+ if (!broker) throw new Error('Credential broker is unavailable.');
210
+ const scope = getCredentialScope(req);
211
+ const binding = await broker.createBinding(scope.userId, scope.agentId, req.body, {
212
+ signal: req.signal,
213
+ });
214
+ res.status(201).json({ binding });
215
+ } catch (err) {
216
+ res.status(400).json({ error: sanitizeError(err), code: err.code || null });
217
+ }
218
+ });
219
+
220
+ router.put('/bitwarden/bindings/:bindingId', async (req, res) => {
221
+ try {
222
+ const broker = req.app?.locals?.credentialBroker;
223
+ if (!broker) throw new Error('Credential broker is unavailable.');
224
+ const scope = getCredentialScope(req);
225
+ const binding = await broker.updateBinding(
226
+ scope.userId,
227
+ scope.agentId,
228
+ req.params.bindingId,
229
+ req.body,
230
+ { signal: req.signal },
231
+ );
232
+ res.json({ binding });
233
+ } catch (err) {
234
+ res.status(400).json({ error: sanitizeError(err), code: err.code || null });
235
+ }
236
+ });
237
+
238
+ router.delete('/bitwarden/bindings/:bindingId', (req, res) => {
239
+ try {
240
+ const broker = req.app?.locals?.credentialBroker;
241
+ if (!broker) throw new Error('Credential broker is unavailable.');
242
+ const scope = getCredentialScope(req);
243
+ res.json(broker.deleteBinding(scope.userId, scope.agentId, req.params.bindingId));
244
+ } catch (err) {
245
+ res.status(400).json({ error: sanitizeError(err), code: err.code || null });
246
+ }
247
+ });
248
+
147
249
  router.get('/qr-image', async (req, res) => {
148
250
  try {
149
251
  const data = String(req.query.data || '').trim();
@@ -320,6 +422,7 @@ router.post('/:provider/connect', async (req, res) => {
320
422
  {
321
423
  appKey: req.body?.appId,
322
424
  agentId: resolveAgentId(req.session.userId, getAgentIdFromRequest(req)),
425
+ signal: req.signal,
323
426
  },
324
427
  );
325
428
  res.json(result);
@@ -340,6 +443,7 @@ router.post('/:provider/disconnect', async (req, res) => {
340
443
  {
341
444
  connectionId: req.body?.connectionId,
342
445
  agentId: resolveAgentId(req.session.userId, getAgentIdFromRequest(req)),
446
+ signal: req.signal,
343
447
  },
344
448
  );
345
449
  res.json(result);
@@ -429,6 +533,7 @@ router.put('/:provider/config', async (req, res) => {
429
533
  userId: req.session.userId,
430
534
  agentId: resolveAgentId(req.session.userId, getAgentIdFromRequest(req)),
431
535
  config: req.body?.config || req.body || {},
536
+ signal: req.signal,
432
537
  });
433
538
  res.json({
434
539
  provider: provider.key,
@@ -458,6 +563,7 @@ router.delete('/:provider/config', async (req, res) => {
458
563
  await provider.clearUserConfig({
459
564
  userId: req.session.userId,
460
565
  agentId: resolveAgentId(req.session.userId, getAgentIdFromRequest(req)),
566
+ signal: req.signal,
461
567
  });
462
568
  res.json({
463
569
  provider: provider.key,
@@ -198,6 +198,7 @@ router.post('/ingestion/documents', async (req, res) => {
198
198
  connectionId: req.body.connectionId,
199
199
  sourceAccount: req.body.sourceAccount,
200
200
  metadata: normalizeMetadata(req.body.metadata),
201
+ signal: req.signal,
201
202
  });
202
203
  res.json(result);
203
204
  } catch (err) {
@@ -40,6 +40,14 @@ const AGENT_SETTING_KEYS = new Set([
40
40
  'cost_mode',
41
41
  'chat_history_window',
42
42
  'tool_replay_budget_chars',
43
+ 'tool_replay_budget_file_chars',
44
+ 'tool_replay_budget_browser_chars',
45
+ 'tool_replay_budget_command_chars',
46
+ 'max_iterations',
47
+ 'max_consecutive_read_only_iterations',
48
+ 'max_consecutive_tool_failures',
49
+ 'max_model_failure_recoveries',
50
+ 'compaction_threshold',
43
51
  'subagent_max_iterations',
44
52
  'subagent_max_children_per_run',
45
53
  'assistant_behavior_notes',
@@ -194,16 +202,19 @@ async function resetEnvBackedSettingValue(req, key) {
194
202
  // Get supported models metadata
195
203
  router.get('/meta/models', async (req, res) => {
196
204
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
197
- const models = await getSupportedModels(req.session.userId, agentId);
205
+ const models = await getSupportedModels(req.session.userId, agentId, { signal: req.signal });
198
206
  res.json({ models });
199
207
  });
200
208
 
201
209
  router.get('/meta/ai-providers', async (req, res) => {
202
210
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
203
- const [providers, models] = await Promise.all([
204
- getProviderHealthCatalog(req.session.userId, agentId),
205
- getSupportedModels(req.session.userId, agentId),
206
- ]);
211
+ const providers = await getProviderHealthCatalog(req.session.userId, agentId, {
212
+ signal: req.signal,
213
+ });
214
+ const models = await getSupportedModels(req.session.userId, agentId, {
215
+ providerCatalog: providers,
216
+ signal: req.signal,
217
+ });
207
218
 
208
219
  const modelCounts = models.reduce((acc, model) => {
209
220
  acc[model.provider] = (acc[model.provider] || 0) + 1;
@@ -25,7 +25,7 @@ router.get('/status', async (req, res) => {
25
25
  if (!service || typeof service.getStatus !== 'function') {
26
26
  return res.status(503).json({ error: 'Social reach service is unavailable.' });
27
27
  }
28
- return res.json(await service.getStatus(req.session.userId));
28
+ return res.json(await service.getStatus(req.session.userId, { signal: req.signal }));
29
29
  } catch (error) {
30
30
  return sendError(res, error);
31
31
  }
@@ -37,7 +37,11 @@ router.post('/read', async (req, res) => {
37
37
  if (!service || typeof service.read !== 'function') {
38
38
  return res.status(503).json({ error: 'Social reach service is unavailable.' });
39
39
  }
40
- return res.json(await service.read(req.session.userId, req.body || {}));
40
+ return res.json(await service.read(
41
+ req.session.userId,
42
+ req.body || {},
43
+ { signal: req.signal },
44
+ ));
41
45
  } catch (error) {
42
46
  return sendError(res, error);
43
47
  }
@@ -49,7 +53,11 @@ router.post('/search', async (req, res) => {
49
53
  if (!service || typeof service.search !== 'function') {
50
54
  return res.status(503).json({ error: 'Social reach service is unavailable.' });
51
55
  }
52
- return res.json(await service.search(req.session.userId, req.body || {}));
56
+ return res.json(await service.search(
57
+ req.session.userId,
58
+ req.body || {},
59
+ { signal: req.signal },
60
+ ));
53
61
  } catch (error) {
54
62
  return sendError(res, error);
55
63
  }
@@ -67,6 +75,7 @@ router.post('/cookies/import', async (req, res) => {
67
75
  }
68
76
  return res.json(await service.importCookiesFromExtension(req.session.userId, platform, {
69
77
  tokenId: req.body?.tokenId || req.body?.token_id || null,
78
+ signal: req.signal,
70
79
  }));
71
80
  } catch (error) {
72
81
  return sendError(res, error);