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
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
- const { spawnSync } = require('child_process');
6
+ const { spawn, spawnSync } = require('child_process');
7
7
  const { DATA_DIR } = require('../../../runtime/paths');
8
8
  const { stageGuestPayload, normalizeRuntimeProfile } = require('./guest_bootstrap');
9
9
 
@@ -21,11 +21,12 @@ const BUILD_TIMEOUT_MS = Number(process.env.NEOAGENT_GUEST_IMAGE_BUILD_TIMEOUT_M
21
21
  // container start. Copy package.json first so the dependency layer stays cached
22
22
  // across source-only changes.
23
23
  function dockerfileFor(profile) {
24
- if (normalizeRuntimeProfile(profile) === 'android') {
24
+ const normalizedProfile = normalizeRuntimeProfile(profile);
25
+ if (normalizedProfile === 'android' || normalizedProfile === 'cli') {
25
26
  return [
26
27
  `FROM ${BASE_IMAGE}`,
27
28
  'ENV NODE_ENV=production',
28
- 'ENV NEOAGENT_GUEST_PROFILE=android',
29
+ `ENV NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
29
30
  'WORKDIR /opt/neoagent',
30
31
  'COPY package.json ./',
31
32
  'RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force',
@@ -42,7 +43,7 @@ function dockerfileFor(profile) {
42
43
  `FROM ${BASE_IMAGE}`,
43
44
  'ENV NODE_ENV=production',
44
45
  'ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright',
45
- 'ENV NEOAGENT_GUEST_PROFILE=browser_cli',
46
+ `ENV NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
46
47
  'WORKDIR /opt/neoagent',
47
48
  'COPY package.json ./',
48
49
  // Install deps without running package postinstall scripts, then fetch the
@@ -104,6 +105,34 @@ function dockerAvailable() {
104
105
  return !result.error && result.status === 0;
105
106
  }
106
107
 
108
+ function runDockerCommand(args, options = {}) {
109
+ const timeoutMs = Number(options.timeout || BUILD_TIMEOUT_MS);
110
+ const spawnImpl = options.spawnImpl || spawn;
111
+ return new Promise((resolve, reject) => {
112
+ const child = spawnImpl('docker', args, {
113
+ stdio: options.stdio || ['ignore', 'inherit', 'inherit'],
114
+ windowsHide: true,
115
+ });
116
+ let settled = false;
117
+ const finish = (error, status = null) => {
118
+ if (settled) return;
119
+ settled = true;
120
+ clearTimeout(timeout);
121
+ if (error) reject(error);
122
+ else resolve(status);
123
+ };
124
+ const timeout = setTimeout(() => {
125
+ const error = new Error(`docker ${args[0]} timed out after ${timeoutMs}ms`);
126
+ error.code = 'DOCKER_COMMAND_TIMEOUT';
127
+ try { child.kill('SIGKILL'); } catch {}
128
+ finish(error);
129
+ }, timeoutMs);
130
+ timeout.unref?.();
131
+ child.once('error', (error) => finish(error));
132
+ child.once('close', (status) => finish(null, status));
133
+ });
134
+ }
135
+
107
136
  // Builds and caches the per-profile guest-agent Docker image. The image bakes in
108
137
  // the guest agent, its dependencies, and (for browser_cli) the Chromium browser,
109
138
  // so containers start the agent directly with no runtime installation step.
@@ -165,15 +194,16 @@ class GuestImageBuilder {
165
194
  async #build(tag) {
166
195
  console.log(`[GuestImage:${this.profile}] Building guest image ${tag} (one-time; downloads browser + deps)…`);
167
196
  const started = Date.now();
168
- const result = docker(['build', '-t', tag, this.contextDir], {
169
- timeout: BUILD_TIMEOUT_MS,
170
- stdio: ['ignore', 'inherit', 'inherit'],
171
- });
172
- if (result.error) {
173
- throw new Error(`Guest image build failed to start: ${result.error.message}`);
197
+ let status;
198
+ try {
199
+ status = await runDockerCommand(['build', '-t', tag, this.contextDir], {
200
+ timeout: BUILD_TIMEOUT_MS,
201
+ });
202
+ } catch (error) {
203
+ throw new Error(`Guest image build failed: ${error.message}`, { cause: error });
174
204
  }
175
- if (result.status !== 0) {
176
- throw new Error(`Guest image build for ${tag} exited with status ${result.status}`);
205
+ if (status !== 0) {
206
+ throw new Error(`Guest image build for ${tag} exited with status ${status}`);
177
207
  }
178
208
  this.#cachedBuiltAt = Date.now();
179
209
  console.log(`[GuestImage:${this.profile}] Built ${tag} in ${Math.round((Date.now() - started) / 1000)}s`);
@@ -184,5 +214,6 @@ class GuestImageBuilder {
184
214
  module.exports = {
185
215
  GuestImageBuilder,
186
216
  dockerAvailable,
217
+ runDockerCommand,
187
218
  BASE_IMAGE,
188
219
  };
@@ -10,23 +10,36 @@ const { DesktopProvider } = require('../desktop/provider');
10
10
  // Resource defaults for Docker VMs (overridable via env).
11
11
  const DEFAULT_VM_MEMORY_MB = Number(process.env.NEOAGENT_VM_MEMORY_MB ?? 2048);
12
12
  const DEFAULT_VM_CPUS = Number(process.env.NEOAGENT_VM_CPUS ?? 2);
13
+ const DEFAULT_CLI_VM_MEMORY_MB = Number(process.env.NEOAGENT_CLI_VM_MEMORY_MB ?? 1024);
14
+ const DEFAULT_CLI_VM_CPUS = Number(process.env.NEOAGENT_CLI_VM_CPUS ?? 1);
13
15
 
14
16
  class RuntimeManager {
15
17
  constructor(options = {}) {
16
18
  this.browserExtensionRegistry = options.browserExtensionRegistry || null;
17
19
  this.desktopCompanionRegistry = options.desktopCompanionRegistry || null;
18
20
  this.shellWorkerPool = options.shellWorkerPool || null;
21
+ this.androidControllers = new Map();
19
22
 
20
23
  const browserVmManager = options.browserVmManager || new DockerVMManager({
21
- runtimeProfile: 'browser_cli',
24
+ runtimeProfile: 'browser',
22
25
  memoryMb: DEFAULT_VM_MEMORY_MB,
23
26
  cpus: DEFAULT_VM_CPUS,
24
27
  });
25
28
  this.browserBackend = new LocalVmExecutionBackend({
26
- runtimeProfile: 'browser_cli',
29
+ runtimeProfile: 'browser',
27
30
  vmManager: browserVmManager,
28
31
  artifactStore: options.artifactStore,
29
32
  });
33
+ const cliVmManager = options.cliVmManager || new DockerVMManager({
34
+ runtimeProfile: 'cli',
35
+ memoryMb: DEFAULT_CLI_VM_MEMORY_MB,
36
+ cpus: DEFAULT_CLI_VM_CPUS,
37
+ });
38
+ this.cliBackend = new LocalVmExecutionBackend({
39
+ runtimeProfile: 'cli',
40
+ vmManager: cliVmManager,
41
+ artifactStore: options.artifactStore,
42
+ });
30
43
 
31
44
  this.artifactStore = options.artifactStore || null;
32
45
 
@@ -44,6 +57,12 @@ class RuntimeManager {
44
57
  artifactStore: options.artifactStore,
45
58
  userId,
46
59
  }));
60
+
61
+ this.createAndroidController = options.createAndroidController
62
+ || ((userId) => new AndroidController({
63
+ userId,
64
+ artifactStore: this.artifactStore,
65
+ }));
47
66
  }
48
67
 
49
68
 
@@ -68,37 +87,73 @@ class RuntimeManager {
68
87
  );
69
88
  }
70
89
 
90
+ getCapabilitySnapshot(userId) {
91
+ const settings = this.getSettings(userId) || {};
92
+ const key = String(userId || '').trim();
93
+ const extensionConnected = this.hasActiveExtensionBrowser(userId);
94
+ const androidController = key && this.androidControllers instanceof Map
95
+ ? this.androidControllers.get(key)
96
+ : null;
97
+ let androidStatus = null;
98
+ if (androidController && typeof androidController.getStatusSync === 'function') {
99
+ try {
100
+ androidStatus = androidController.getStatusSync();
101
+ } catch {
102
+ androidStatus = null;
103
+ }
104
+ }
105
+
106
+ return {
107
+ browser: {
108
+ preferredBackend: settings.browser_backend,
109
+ activeBackend: extensionConnected ? 'extension' : 'vm',
110
+ extensionConnected,
111
+ vmInitialized: Boolean(
112
+ this.browserBackend?.vmManager?.hasTrackedVm?.(userId),
113
+ ),
114
+ },
115
+ android: {
116
+ initialized: Boolean(androidController),
117
+ status: androidStatus,
118
+ },
119
+ desktop: {
120
+ connected: this.hasActiveDesktopCompanion(userId),
121
+ },
122
+ };
123
+ }
124
+
71
125
  resolveBackend(userId, requested) {
72
126
  void userId;
73
- void requested;
74
- return this.browserBackend;
127
+ return requested === 'browser' ? this.browserBackend : this.cliBackend;
75
128
  }
76
129
 
77
130
  async executeCommand(userId, command, options = {}) {
78
- const backend = this.resolveBackend(userId, 'browser_cli');
131
+ const backend = this.resolveBackend(userId, 'cli');
79
132
  return backend.executeCommand(userId, command, options);
80
133
  }
81
134
 
82
135
  hasVmForUser(userId, capability = 'browser') {
83
- return Boolean(this.browserBackend?.vmManager?.hasVm?.(userId));
136
+ const backend = capability === 'browser' ? this.browserBackend : this.cliBackend;
137
+ return Boolean(backend?.vmManager?.hasVm?.(userId));
84
138
  }
85
139
 
86
140
  async killCommand(userId, pid, reason = 'aborted') {
87
- return this.browserBackend.killCommand(userId, pid, reason);
141
+ return this.cliBackend.killCommand(userId, pid, reason);
88
142
  }
89
143
 
90
144
  async getCommandExecutorForUser(userId) {
91
- return this.browserBackend.getCommandExecutorForUser(userId);
145
+ return this.cliBackend.getCommandExecutorForUser(userId);
92
146
  }
93
147
 
94
- async getBrowserProviderForUser(userId) {
148
+ async getBrowserProviderForUser(userId, options = {}) {
95
149
  const settings = this.getSettings(userId);
96
150
  if (settings.browser_backend === 'extension' && this.hasActiveExtensionBrowser(userId)) {
97
151
  return this.getExtensionBrowserProvider(userId, {
98
152
  tokenId: settings.browser_extension_token_id,
153
+ signal: options.signal,
99
154
  });
100
155
  }
101
- return this.browserBackend.getBrowserProviderForUser(userId);
156
+ return this.browserBackend.getBrowserProviderForUser(userId, options);
102
157
  }
103
158
 
104
159
  async getCliProviderForUser(userId) {
@@ -116,18 +171,12 @@ class RuntimeManager {
116
171
  kill: () => Promise.resolve(false),
117
172
  };
118
173
  }
119
- const executor = await this.browserBackend.getCommandExecutorForUser(userId);
174
+ const executor = await this.cliBackend.getCommandExecutorForUser(userId);
120
175
  return { ...executor, backend: 'vm' };
121
176
  }
122
177
 
123
178
  async executeCliCommand(userId, command, options = {}) {
124
179
  const provider = await this.getCliProviderForUser(userId);
125
- // Route desktop-companion shell commands through the isolated worker pool
126
- // when available. The VM (Docker) path is already isolated.
127
- if (provider.backend === 'desktop-companion' && this.shellWorkerPool && options.pty !== true) {
128
- const result = await this.shellWorkerPool.execute(command, options);
129
- return { ...result, backend: 'desktop-companion-worker' };
130
- }
131
180
  const result = await (options.pty === true && provider.executeInteractive
132
181
  ? provider.executeInteractive(command, options.inputs || [], options)
133
182
  : provider.execute(command, options));
@@ -146,23 +195,28 @@ class RuntimeManager {
146
195
  if (userId == null || String(userId).trim() === '') {
147
196
  throw new Error('Android provider requires a user ID.');
148
197
  }
149
- return new AndroidController({
150
- userId: String(userId).trim(),
151
- artifactStore: this.artifactStore,
152
- });
198
+ const key = String(userId).trim();
199
+ if (!this.androidControllers.has(key)) {
200
+ this.androidControllers.set(key, this.createAndroidController(key));
201
+ }
202
+ return this.androidControllers.get(key);
153
203
  }
154
204
 
155
205
  async isGuestAgentReadyForUser(userId, timeoutMs = 1000, capability = 'browser') {
156
- if (typeof this.browserBackend?.isGuestAgentReadyForUser !== 'function') {
206
+ const backend = capability === 'browser' ? this.browserBackend : this.cliBackend;
207
+ if (typeof backend?.isGuestAgentReadyForUser !== 'function') {
157
208
  return false;
158
209
  }
159
- return this.browserBackend.isGuestAgentReadyForUser(userId, timeoutMs);
210
+ return backend.isGuestAgentReadyForUser(userId, timeoutMs);
160
211
  }
161
212
 
162
213
  async shutdown() {
163
214
  const tasks = [
164
215
  this.browserBackend?.shutdown?.(),
216
+ this.cliBackend?.shutdown?.(),
217
+ ...Array.from(this.androidControllers.values(), (controller) => controller?.close?.()),
165
218
  ];
219
+ this.androidControllers.clear();
166
220
  if (typeof this.shellWorkerPool?.shutdown === 'function') {
167
221
  tasks.push(Promise.resolve().then(() => this.shellWorkerPool.shutdown()));
168
222
  }
@@ -6,14 +6,14 @@ function getRuntimeValidation(runtimeManager) {
6
6
  const policy = getDeploymentPolicy();
7
7
  const nodeEnvIsProd = String(process.env.NODE_ENV || '').trim().toLowerCase() === 'prod';
8
8
  const browserVmReadiness = runtimeManager?.browserBackend?.vmManager?.getReadiness?.() || null;
9
- const vmReadiness = browserVmReadiness || null;
9
+ const cliVmReadiness = runtimeManager?.cliBackend?.vmManager?.getReadiness?.() || null;
10
10
  const issues = [];
11
11
 
12
12
  if (policy.profile === 'prod' || nodeEnvIsProd) {
13
- if (!browserVmReadiness) {
14
- issues.push('prod profile requires a working container runtime for browser/CLI.');
15
- } else if (!browserVmReadiness.dockerAvailable) {
16
- issues.push('prod profile requires Docker to be installed and running for the browser/CLI runtime.');
13
+ if (!browserVmReadiness || !cliVmReadiness) {
14
+ issues.push('prod profile requires working isolated container runtimes for browser and CLI.');
15
+ } else if (!browserVmReadiness.dockerAvailable || !cliVmReadiness.dockerAvailable) {
16
+ issues.push('prod profile requires Docker to be installed and running for the browser and CLI runtimes.');
17
17
  }
18
18
  }
19
19
 
@@ -21,7 +21,8 @@ function getRuntimeValidation(runtimeManager) {
21
21
  ready: issues.length === 0,
22
22
  issues,
23
23
  vm: {
24
- browser: vmReadiness,
24
+ browser: browserVmReadiness,
25
+ cli: cliVmReadiness,
25
26
  android: null,
26
27
  },
27
28
  guestTokenConfigured: true,
@@ -18,6 +18,7 @@ const TOOL_CATEGORIES = {
18
18
  'desktop_observe',
19
19
  ],
20
20
  browser_privileged: ['browser_evaluate'],
21
+ credential_use: ['credential_fill_browser', 'credential_http_request'],
21
22
  network_write: ['http_request'],
22
23
  skill_mutation: [
23
24
  'create_skill',
@@ -78,6 +79,10 @@ const BUILT_IN_TOOLS = new Set([
78
79
  'browser_extract',
79
80
  'browser_screenshot',
80
81
  'browser_evaluate',
82
+ 'credential_fill_browser',
83
+ 'credential_submit_browser',
84
+ 'credential_cancel_browser',
85
+ 'credential_http_request',
81
86
  'desktop_list_devices',
82
87
  'desktop_select_device',
83
88
  'desktop_observe',
@@ -166,6 +171,7 @@ const DEFAULT_POLICY = {
166
171
  android_privileged: 'require_approval',
167
172
  desktop_control: 'require_approval',
168
173
  browser_privileged: 'require_approval',
174
+ credential_use: 'require_approval',
169
175
  network_write: 'require_approval',
170
176
  skill_mutation: 'deny',
171
177
  external: 'require_approval',
@@ -28,9 +28,12 @@ class GithubChannel extends SocialReachChannel {
28
28
  };
29
29
  }
30
30
 
31
- async read({ url }) {
31
+ async read({ url, signal }) {
32
32
  const { owner, repo } = parseRepoUrl(url);
33
- const data = await fetchJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
33
+ const data = await fetchJson(
34
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
35
+ { signal },
36
+ );
34
37
  return {
35
38
  platform: this.id,
36
39
  owner,
@@ -47,14 +50,17 @@ class GithubChannel extends SocialReachChannel {
47
50
  };
48
51
  }
49
52
 
50
- async search({ query, limit }) {
53
+ async search({ query, limit, signal }) {
51
54
  const q = String(query || '').trim();
52
55
  if (!q) {
53
56
  const error = new Error('query is required.');
54
57
  error.status = 400;
55
58
  throw error;
56
59
  }
57
- const data = await fetchJson(`https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=${normalizeLimit(limit, 10, 30)}`);
60
+ const data = await fetchJson(
61
+ `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=${normalizeLimit(limit, 10, 30)}`,
62
+ { signal },
63
+ );
58
64
  return {
59
65
  platform: this.id,
60
66
  query: q,
@@ -67,8 +67,8 @@ class RedditChannel extends SocialReachChannel {
67
67
  };
68
68
  }
69
69
 
70
- async read({ url, limit }) {
71
- const data = await fetchJson(postUrlForJson(url));
70
+ async read({ url, limit, signal }) {
71
+ const data = await fetchJson(postUrlForJson(url), { signal });
72
72
  const post = shapePost(data?.[0]?.data?.children?.[0]);
73
73
  const comments = (data?.[1]?.data?.children || [])
74
74
  .filter((item) => item?.kind === 't1')
@@ -82,7 +82,7 @@ class RedditChannel extends SocialReachChannel {
82
82
  };
83
83
  }
84
84
 
85
- async search({ query, limit }) {
85
+ async search({ query, limit, signal }) {
86
86
  const q = String(query || '').trim();
87
87
  if (!q) {
88
88
  const error = new Error('query is required.');
@@ -90,7 +90,7 @@ class RedditChannel extends SocialReachChannel {
90
90
  throw error;
91
91
  }
92
92
  const url = `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&limit=${normalizeLimit(limit, 10, 50)}&raw_json=1`;
93
- const data = await fetchJson(url);
93
+ const data = await fetchJson(url, { signal });
94
94
  return {
95
95
  platform: this.id,
96
96
  query: q,
@@ -31,9 +31,9 @@ class RssChannel extends SocialReachChannel {
31
31
  };
32
32
  }
33
33
 
34
- async read({ url, limit }) {
34
+ async read({ url, limit, signal }) {
35
35
  const parsed = assertHttpUrl(url);
36
- const xml = await fetchText(parsed.toString());
36
+ const xml = await fetchText(parsed.toString(), { publicOnly: true, signal });
37
37
  const $ = cheerio.load(xml, { xmlMode: true });
38
38
  const entries = $('item, entry').toArray().slice(0, normalizeLimit(limit, 20, 100));
39
39
  const title = $('channel > title, feed > title').first().text().trim() || null;
@@ -3,6 +3,7 @@
3
3
  const { getPlatformDefinition } = require('../platforms');
4
4
  const { SocialReachChannel } = require('./base');
5
5
  const { normalizeAndDetectPlatform } = require('../../social_video/url');
6
+ const { createAbortError } = require('../../../utils/abort');
6
7
 
7
8
  const PLATFORM_LABELS = Object.freeze({
8
9
  youtube: 'YouTube',
@@ -26,7 +27,7 @@ class SocialVideoReachChannel extends SocialReachChannel {
26
27
  }
27
28
  }
28
29
 
29
- async check() {
30
+ async check({ signal } = {}) {
30
31
  if (!this.socialVideoService || typeof this.socialVideoService.getHealthStatus !== 'function') {
31
32
  return {
32
33
  ...(await super.check()),
@@ -37,11 +38,14 @@ class SocialVideoReachChannel extends SocialReachChannel {
37
38
  };
38
39
  }
39
40
 
40
- const health = await this.socialVideoService.getHealthStatus().catch((error) => ({
41
- ready: false,
42
- dependencies: [],
43
- error: error.message || String(error),
44
- }));
41
+ const health = await this.socialVideoService.getHealthStatus({ signal }).catch((error) => {
42
+ if (signal?.aborted) throw createAbortError(signal);
43
+ return {
44
+ ready: false,
45
+ dependencies: [],
46
+ error: error.message || String(error),
47
+ };
48
+ });
45
49
  const missing = (health.dependencies || [])
46
50
  .filter((item) => !item.available)
47
51
  .map((item) => item.name)
@@ -59,7 +63,7 @@ class SocialVideoReachChannel extends SocialReachChannel {
59
63
  };
60
64
  }
61
65
 
62
- async read({ userId, url, include_frame: includeFrame, force_stt: forceStt, agentId }) {
66
+ async read({ userId, url, include_frame: includeFrame, force_stt: forceStt, agentId, signal }) {
63
67
  if (!this.socialVideoService || typeof this.socialVideoService.extractFromUrl !== 'function') {
64
68
  const error = new Error('Social video extraction is not connected right now.');
65
69
  error.status = 503;
@@ -70,6 +74,7 @@ class SocialVideoReachChannel extends SocialReachChannel {
70
74
  includeFrame: includeFrame !== false,
71
75
  forceStt: forceStt === true,
72
76
  agentId: agentId || null,
77
+ signal,
73
78
  });
74
79
  return {
75
80
  ...result,
@@ -3,6 +3,7 @@
3
3
  const { SocialReachChannel } = require('./base');
4
4
  const { getPlatformDefinition } = require('../platforms');
5
5
  const { assertHttpUrl, fetchJson, normalizeLimit } = require('../utils');
6
+ const { createAbortError } = require('../../../utils/abort');
6
7
 
7
8
  function shapeTopic(item = {}) {
8
9
  const node = item.node || {};
@@ -33,17 +34,26 @@ class V2exChannel extends SocialReachChannel {
33
34
  };
34
35
  }
35
36
 
36
- async read({ url, limit }) {
37
+ async read({ url, limit, signal }) {
37
38
  const parsed = assertHttpUrl(url);
38
39
  const match = parsed.pathname.match(/^\/t\/(\d+)/);
39
40
  if (!match) {
40
- return this.search({ query: parsed.searchParams.get('q') || 'hot', limit });
41
+ return this.search({ query: parsed.searchParams.get('q') || 'hot', limit, signal });
41
42
  }
42
43
  const topicId = match[1];
43
- const topicData = await fetchJson(`https://www.v2ex.com/api/topics/show.json?id=${encodeURIComponent(topicId)}`);
44
+ const topicData = await fetchJson(
45
+ `https://www.v2ex.com/api/topics/show.json?id=${encodeURIComponent(topicId)}`,
46
+ { signal },
47
+ );
44
48
  const topic = Array.isArray(topicData) ? topicData[0] || {} : topicData || {};
45
- const replies = await fetchJson(`https://www.v2ex.com/api/replies/show.json?topic_id=${encodeURIComponent(topicId)}&page=1`)
46
- .catch(() => []);
49
+ const replies = await fetchJson(
50
+ `https://www.v2ex.com/api/replies/show.json?topic_id=${encodeURIComponent(topicId)}&page=1`,
51
+ { signal },
52
+ )
53
+ .catch((error) => {
54
+ if (signal?.aborted) throw createAbortError(signal);
55
+ return [];
56
+ });
47
57
  return {
48
58
  platform: this.id,
49
59
  ...shapeTopic(topic),
@@ -58,11 +68,14 @@ class V2exChannel extends SocialReachChannel {
58
68
  };
59
69
  }
60
70
 
61
- async search({ query, limit }) {
71
+ async search({ query, limit, signal }) {
62
72
  const normalized = String(query || '').trim().toLowerCase();
63
73
  const capped = normalizeLimit(limit, 20, 100);
64
74
  if (normalized && normalized !== 'hot') {
65
- const data = await fetchJson(`https://www.v2ex.com/api/topics/show.json?node_name=${encodeURIComponent(normalized)}&page=1`);
75
+ const data = await fetchJson(
76
+ `https://www.v2ex.com/api/topics/show.json?node_name=${encodeURIComponent(normalized)}&page=1`,
77
+ { signal },
78
+ );
66
79
  return {
67
80
  platform: this.id,
68
81
  query: normalized,
@@ -70,7 +83,7 @@ class V2exChannel extends SocialReachChannel {
70
83
  source: 'v2ex_public_api',
71
84
  };
72
85
  }
73
- const data = await fetchJson('https://www.v2ex.com/api/topics/hot.json');
86
+ const data = await fetchJson('https://www.v2ex.com/api/topics/hot.json', { signal });
74
87
  return {
75
88
  platform: this.id,
76
89
  query: 'hot',
@@ -43,7 +43,7 @@ class XChannel extends SocialReachChannel {
43
43
  };
44
44
  }
45
45
 
46
- async read({ userId, url }) {
46
+ async read({ userId, url, signal }) {
47
47
  const id = extractStatusId(url);
48
48
  if (!id) {
49
49
  const error = new Error('A public X post URL is required.');
@@ -54,7 +54,7 @@ class XChannel extends SocialReachChannel {
54
54
  const source = cookieHeaderForPlatform(userId, this.id) ? 'x_syndication_cookies' : 'x_syndication_public';
55
55
  const data = await fetchJson(
56
56
  `https://cdn.syndication.twimg.com/tweet-result?id=${encodeURIComponent(id)}&lang=en`,
57
- { headers: this.#headers(userId) },
57
+ { headers: this.#headers(userId), signal },
58
58
  );
59
59
  return {
60
60
  platform: this.id,
@@ -37,7 +37,7 @@ class XueqiuChannel extends SocialReachChannel {
37
37
  };
38
38
  }
39
39
 
40
- async search({ userId, query, limit }) {
40
+ async search({ userId, query, limit, signal }) {
41
41
  const q = String(query || '').trim();
42
42
  if (!q) {
43
43
  const error = new Error('query is required.');
@@ -45,7 +45,7 @@ class XueqiuChannel extends SocialReachChannel {
45
45
  throw error;
46
46
  }
47
47
  const url = `https://xueqiu.com/stock/search.json?code=${encodeURIComponent(q)}&size=${normalizeLimit(limit, 10, 50)}`;
48
- const data = await fetchJson(url, { headers: this.#headers(userId) });
48
+ const data = await fetchJson(url, { headers: this.#headers(userId), signal });
49
49
  return {
50
50
  platform: this.id,
51
51
  query: q,
@@ -58,12 +58,12 @@ class XueqiuChannel extends SocialReachChannel {
58
58
  };
59
59
  }
60
60
 
61
- async read({ userId, symbol, limit }) {
61
+ async read({ userId, symbol, limit, signal }) {
62
62
  const raw = String(symbol || '').trim().toUpperCase();
63
63
  if (raw) {
64
64
  const data = await fetchJson(
65
65
  `https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=${encodeURIComponent(raw)}`,
66
- { headers: this.#headers(userId) },
66
+ { headers: this.#headers(userId), signal },
67
67
  );
68
68
  const quote = data.data?.items?.[0]?.quote || {};
69
69
  return {
@@ -76,7 +76,7 @@ class XueqiuChannel extends SocialReachChannel {
76
76
  }
77
77
  const data = await fetchJson(
78
78
  'https://xueqiu.com/v4/statuses/public_timeline_by_category.json?since_id=-1&max_id=-1&count=20&category=-1',
79
- { headers: this.#headers(userId) },
79
+ { headers: this.#headers(userId), signal },
80
80
  );
81
81
  return {
82
82
  platform: this.id,
@@ -5,6 +5,7 @@ const { createChannels } = require('./channels');
5
5
  const { deleteCookieBundle, getCookieSummary, writeCookieBundle } = require('./store');
6
6
  const { domainsForPlatform, getPlatformDefinition, normalizePlatformId } = require('./platforms');
7
7
  const { assertHttpUrl } = require('./utils');
8
+ const { createAbortError } = require('../../utils/abort');
8
9
 
9
10
  const COOKIE_IMPORT_PLATFORMS = new Set(['xueqiu', 'x']);
10
11
  const MAX_IMPORTED_COOKIES = 80;
@@ -59,16 +60,17 @@ class SocialReachService {
59
60
  return this.channels.find((channel) => channel.canHandleUrl(parsed.toString())) || null;
60
61
  }
61
62
 
62
- async getStatus(userId) {
63
+ async getStatus(userId, options = {}) {
63
64
  const statuses = [];
64
65
  for (const channel of this.channels) {
65
66
  try {
66
- const status = await channel.check({ userId });
67
+ const status = await channel.check({ userId, signal: options.signal });
67
68
  if (!status.cookie && channel.setupKind === 'cookies') {
68
69
  status.cookie = getCookieSummary(userId, channel.id);
69
70
  }
70
71
  statuses.push(status);
71
72
  } catch (error) {
73
+ if (options.signal?.aborted) throw createAbortError(options.signal);
72
74
  statuses.push({
73
75
  platform: channel.id,
74
76
  label: channel.label,
@@ -87,7 +89,7 @@ class SocialReachService {
87
89
  };
88
90
  }
89
91
 
90
- async read(userId, args = {}) {
92
+ async read(userId, args = {}, options = {}) {
91
93
  const platform = normalizePlatformId(args.platform || '');
92
94
  const channel = platform ? this.getChannel(platform) : this.detectChannelForUrl(args.url);
93
95
  if (!channel) {
@@ -97,10 +99,10 @@ class SocialReachService {
97
99
  error.status = 400;
98
100
  throw error;
99
101
  }
100
- return channel.read({ ...args, userId });
102
+ return channel.read({ ...args, userId, signal: options.signal });
101
103
  }
102
104
 
103
- async search(userId, args = {}) {
105
+ async search(userId, args = {}, options = {}) {
104
106
  const platform = normalizePlatformId(args.platform || '');
105
107
  const channel = this.getChannel(platform);
106
108
  if (!channel) {
@@ -108,7 +110,7 @@ class SocialReachService {
108
110
  error.status = 400;
109
111
  throw error;
110
112
  }
111
- return channel.search({ ...args, userId });
113
+ return channel.search({ ...args, userId, signal: options.signal });
112
114
  }
113
115
 
114
116
  async importCookiesFromExtension(userId, platform, options = {}) {
@@ -132,6 +134,7 @@ class SocialReachService {
132
134
  }, {
133
135
  tokenId: options.tokenId || null,
134
136
  timeoutMs: 15000,
137
+ signal: options.signal,
135
138
  });
136
139
  const cookies = sanitizeCookies(response?.cookies || [], domains);
137
140
  if (cookies.length === 0) {