neoagent 3.2.1-beta.1 → 3.2.1-beta.11

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 (198) 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 +157 -24
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +7 -2
  8. package/flutter_app/lib/main_operations.dart +334 -370
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +342 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +97 -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 +12 -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 +81930 -80509
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/behavior.js +80 -0
  33. package/server/routes/browser.js +23 -15
  34. package/server/routes/desktop.js +18 -1
  35. package/server/routes/integrations.js +107 -1
  36. package/server/routes/memory.js +1 -0
  37. package/server/routes/settings.js +20 -5
  38. package/server/routes/social_reach.js +12 -3
  39. package/server/routes/social_video.js +4 -0
  40. package/server/services/agents/manager.js +1 -1
  41. package/server/services/ai/capabilityHealth.js +62 -96
  42. package/server/services/ai/compaction.js +7 -2
  43. package/server/services/ai/history.js +45 -6
  44. package/server/services/ai/integrated_tools/http_request.js +8 -0
  45. package/server/services/ai/loop/agent_engine_core.js +452 -176
  46. package/server/services/ai/loop/blank_recovery.js +5 -4
  47. package/server/services/ai/loop/callbacks.js +1 -0
  48. package/server/services/ai/loop/completion_judge.js +291 -8
  49. package/server/services/ai/loop/conversation_loop.js +515 -342
  50. package/server/services/ai/loop/messaging_delivery.js +176 -57
  51. package/server/services/ai/loop/model_call_guard.js +91 -0
  52. package/server/services/ai/loop/model_io.js +20 -45
  53. package/server/services/ai/loop/progress_classification.js +2 -0
  54. package/server/services/ai/loop/tool_dispatch.js +19 -8
  55. package/server/services/ai/loopPolicy.js +48 -21
  56. package/server/services/ai/messagingFallback.js +17 -17
  57. package/server/services/ai/model_discovery.js +227 -0
  58. package/server/services/ai/model_failure_cache.js +108 -0
  59. package/server/services/ai/model_identity.js +71 -0
  60. package/server/services/ai/models.js +68 -163
  61. package/server/services/ai/providerRetry.js +17 -59
  62. package/server/services/ai/provider_selector.js +166 -0
  63. package/server/services/ai/providers/anthropic.js +2 -2
  64. package/server/services/ai/providers/claudeCode.js +21 -33
  65. package/server/services/ai/providers/githubCopilot.js +41 -20
  66. package/server/services/ai/providers/google.js +135 -97
  67. package/server/services/ai/providers/grok.js +4 -3
  68. package/server/services/ai/providers/grokOauth.js +19 -27
  69. package/server/services/ai/providers/nvidia.js +10 -5
  70. package/server/services/ai/providers/ollama.js +111 -84
  71. package/server/services/ai/providers/ollama_stream.js +142 -0
  72. package/server/services/ai/providers/openai.js +39 -5
  73. package/server/services/ai/providers/openaiCodex.js +11 -4
  74. package/server/services/ai/providers/openrouter.js +29 -7
  75. package/server/services/ai/providers/provider_error.js +36 -0
  76. package/server/services/ai/settings.js +26 -2
  77. package/server/services/ai/systemPrompt.js +32 -123
  78. package/server/services/ai/taskAnalysis.js +104 -11
  79. package/server/services/ai/toolEvidence.js +256 -29
  80. package/server/services/ai/tools.js +248 -117
  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/behavior/config.js +251 -0
  87. package/server/services/behavior/defaults.js +68 -0
  88. package/server/services/behavior/delivery.js +176 -0
  89. package/server/services/behavior/index.js +43 -0
  90. package/server/services/behavior/model_client.js +35 -0
  91. package/server/services/behavior/modules/agent_identity.js +37 -0
  92. package/server/services/behavior/modules/channel_style.js +28 -0
  93. package/server/services/behavior/modules/index.js +29 -0
  94. package/server/services/behavior/modules/norms.js +101 -0
  95. package/server/services/behavior/modules/persona.js +48 -0
  96. package/server/services/behavior/modules/persona_prompt.js +33 -0
  97. package/server/services/behavior/modules/social_memory.js +86 -0
  98. package/server/services/behavior/modules/social_observability.js +94 -0
  99. package/server/services/behavior/modules/social_signals.js +41 -0
  100. package/server/services/behavior/modules/theory_of_mind.js +110 -0
  101. package/server/services/behavior/modules/turn_taking.js +237 -0
  102. package/server/services/behavior/pipeline.js +285 -0
  103. package/server/services/behavior/registry.js +78 -0
  104. package/server/services/behavior/signals.js +107 -0
  105. package/server/services/behavior/state.js +99 -0
  106. package/server/services/behavior/system_prompt.js +75 -0
  107. package/server/services/browser/controller.js +843 -385
  108. package/server/services/browser/extension/gateway.js +40 -16
  109. package/server/services/browser/extension/protocol.js +15 -1
  110. package/server/services/browser/extension/provider.js +71 -47
  111. package/server/services/browser/extension/registry.js +155 -34
  112. package/server/services/cli/executor.js +62 -9
  113. package/server/services/credentials/bitwarden_cli.js +322 -0
  114. package/server/services/credentials/broker.js +594 -0
  115. package/server/services/desktop/gateway.js +41 -4
  116. package/server/services/desktop/protocol.js +3 -0
  117. package/server/services/desktop/provider.js +39 -42
  118. package/server/services/desktop/registry.js +137 -52
  119. package/server/services/integrations/bitwarden/constants.js +14 -0
  120. package/server/services/integrations/bitwarden/provider.js +197 -0
  121. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  122. package/server/services/integrations/figma/provider.js +78 -12
  123. package/server/services/integrations/github/common.js +11 -6
  124. package/server/services/integrations/github/provider.js +52 -53
  125. package/server/services/integrations/google/provider.js +55 -19
  126. package/server/services/integrations/home_assistant/network.js +17 -20
  127. package/server/services/integrations/home_assistant/provider.js +7 -5
  128. package/server/services/integrations/home_assistant/tools.js +17 -5
  129. package/server/services/integrations/http.js +51 -0
  130. package/server/services/integrations/manager.js +159 -53
  131. package/server/services/integrations/microsoft/provider.js +80 -13
  132. package/server/services/integrations/neoarchive/provider.js +55 -29
  133. package/server/services/integrations/neorecall/client.js +17 -10
  134. package/server/services/integrations/neorecall/provider.js +20 -11
  135. package/server/services/integrations/notion/provider.js +16 -13
  136. package/server/services/integrations/oauth_provider.js +115 -51
  137. package/server/services/integrations/registry.js +2 -0
  138. package/server/services/integrations/slack/provider.js +98 -9
  139. package/server/services/integrations/spotify/provider.js +67 -71
  140. package/server/services/integrations/trello/provider.js +21 -7
  141. package/server/services/integrations/weather/provider.js +18 -12
  142. package/server/services/integrations/whatsapp/provider.js +76 -16
  143. package/server/services/manager.js +110 -1
  144. package/server/services/memory/embedding_index.js +20 -8
  145. package/server/services/memory/embeddings.js +151 -90
  146. package/server/services/memory/ingestion.js +50 -9
  147. package/server/services/memory/ingestion_documents.js +13 -3
  148. package/server/services/memory/manager.js +66 -52
  149. package/server/services/messaging/access_policy.js +10 -6
  150. package/server/services/messaging/automation.js +240 -34
  151. package/server/services/messaging/discord.js +11 -1
  152. package/server/services/messaging/formatting_guides.js +5 -4
  153. package/server/services/messaging/http_platforms.js +37 -16
  154. package/server/services/messaging/inbound_queue.js +143 -28
  155. package/server/services/messaging/inbound_store.js +257 -0
  156. package/server/services/messaging/manager.js +344 -51
  157. package/server/services/messaging/telegram.js +10 -1
  158. package/server/services/messaging/typing_keepalive.js +5 -2
  159. package/server/services/messaging/whatsapp.js +33 -14
  160. package/server/services/network/http.js +210 -0
  161. package/server/services/network/safe_request.js +307 -0
  162. package/server/services/runtime/backends/local-vm.js +227 -67
  163. package/server/services/runtime/docker-vm-manager.js +9 -0
  164. package/server/services/runtime/guest_bootstrap.js +30 -4
  165. package/server/services/runtime/guest_image.js +43 -12
  166. package/server/services/runtime/manager.js +77 -23
  167. package/server/services/runtime/validation.js +7 -6
  168. package/server/services/security/tool_categories.js +6 -0
  169. package/server/services/social_reach/channels/github.js +10 -4
  170. package/server/services/social_reach/channels/reddit.js +4 -4
  171. package/server/services/social_reach/channels/rss.js +2 -2
  172. package/server/services/social_reach/channels/social_video.js +13 -8
  173. package/server/services/social_reach/channels/v2ex.js +21 -8
  174. package/server/services/social_reach/channels/x.js +2 -2
  175. package/server/services/social_reach/channels/xueqiu.js +5 -5
  176. package/server/services/social_reach/service.js +9 -6
  177. package/server/services/social_reach/utils.js +65 -14
  178. package/server/services/social_video/captions.js +2 -2
  179. package/server/services/social_video/service.js +343 -68
  180. package/server/services/tasks/integration_runtime.js +18 -8
  181. package/server/services/tasks/runtime.js +39 -4
  182. package/server/services/voice/agentBridge.js +17 -4
  183. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  184. package/server/services/voice/liveSession.js +31 -0
  185. package/server/services/voice/message.js +1 -1
  186. package/server/services/voice/openaiSpeech.js +33 -8
  187. package/server/services/voice/providers.js +233 -151
  188. package/server/services/voice/runtime.js +2 -2
  189. package/server/services/voice/runtimeManager.js +118 -20
  190. package/server/services/voice/turnRunner.js +6 -0
  191. package/server/services/wearable/firmware_manifest.js +51 -13
  192. package/server/services/wearable/service.js +1 -0
  193. package/server/utils/abort.js +96 -0
  194. package/server/utils/cloud-security.js +110 -3
  195. package/server/utils/files.js +31 -0
  196. package/server/utils/image_payload.js +95 -0
  197. package/server/utils/logger.js +19 -0
  198. package/server/utils/retry.js +107 -0
@@ -1,5 +1,28 @@
1
1
  'use strict';
2
2
 
3
+ const dns = require('node:dns');
4
+ const net = require('node:net');
5
+
6
+ const ALLOWED_SCHEMES = new Set(['http', 'https']);
7
+ const BLOCKED_ANDROID_INTENT_SCHEMES = new Set([
8
+ 'about',
9
+ 'chrome',
10
+ 'chrome-extension',
11
+ 'content',
12
+ 'data',
13
+ 'file',
14
+ 'javascript',
15
+ 'vbscript',
16
+ ]);
17
+
18
+ function urlAbortError(signal) {
19
+ if (signal?.reason instanceof Error) return signal.reason;
20
+ const error = new Error('URL validation was aborted.');
21
+ error.name = 'AbortError';
22
+ error.code = 'ABORT_ERR';
23
+ return error;
24
+ }
25
+
3
26
  // URL schemes that must never be navigated to in the cloud browser or Android.
4
27
  const BLOCKED_SCHEMES = new Set([
5
28
  'javascript',
@@ -56,8 +79,26 @@ function isPrivateHost(hostname) {
56
79
 
57
80
  // IPv6 loopback, link-local and unique-local
58
81
  if (h === '::1' || h === '::') return true;
59
- if (h.startsWith('fe80:')) return true;
82
+ if (/^fe[89ab][0-9a-f]:/.test(h)) return true; // fe80::/10 link-local
83
+ if (/^fe[c-f][0-9a-f]:/.test(h)) return true; // fec0::/10 deprecated site-local
60
84
  if (/^f[cd][0-9a-f]*:/.test(h)) return true; // fc00::/7 unique-local
85
+ if (h.startsWith('ff')) return true; // IPv6 multicast
86
+ if (h === '100::' || h.startsWith('100::')) return true; // discard-only prefix
87
+ if (h === '2001:db8::' || h.startsWith('2001:db8:')) return true; // documentation
88
+
89
+ if (net.isIPv4(h)) {
90
+ const [a, b, c] = h.split('.').map(Number);
91
+ if (a === 0 || a === 10 || a === 127 || a >= 224) return true;
92
+ if (a === 100 && b >= 64 && b <= 127) return true;
93
+ if (a === 169 && b === 254) return true;
94
+ if (a === 172 && b >= 16 && b <= 31) return true;
95
+ if (a === 192 && b === 168) return true;
96
+ if (a === 192 && b === 0 && c === 0) return true;
97
+ if (a === 192 && b === 0 && c === 2) return true;
98
+ if (a === 198 && (b === 18 || b === 19)) return true;
99
+ if (a === 198 && b === 51 && c === 100) return true;
100
+ if (a === 203 && b === 0 && c === 113) return true;
101
+ }
61
102
 
62
103
  for (const pattern of PRIVATE_IPV4) {
63
104
  if (pattern.test(h)) return true;
@@ -90,7 +131,7 @@ function validateCloudUrl(urlString) {
90
131
  }
91
132
 
92
133
  const scheme = parsed.protocol.replace(/:$/, '').toLowerCase();
93
- if (BLOCKED_SCHEMES.has(scheme)) return { allowed: false };
134
+ if (BLOCKED_SCHEMES.has(scheme) || !ALLOWED_SCHEMES.has(scheme)) return { allowed: false };
94
135
 
95
136
  const hostname = parsed.hostname;
96
137
  if (isPrivateHost(hostname)) return { allowed: false };
@@ -99,4 +140,70 @@ function validateCloudUrl(urlString) {
99
140
  return { allowed: true };
100
141
  }
101
142
 
102
- module.exports = { validateCloudUrl, isPrivateHost };
143
+ async function validateCloudUrlWithDns(urlString, options = {}) {
144
+ if (options.signal?.aborted) {
145
+ throw urlAbortError(options.signal);
146
+ }
147
+ const syntactic = validateCloudUrl(urlString);
148
+ if (!syntactic.allowed) return syntactic;
149
+
150
+ const hostname = new URL(urlString).hostname.replace(/^\[|\]$/g, '');
151
+ if (net.isIP(hostname)) return { allowed: !isPrivateHost(hostname) };
152
+
153
+ const lookup = options.lookup || dns.promises.lookup;
154
+ const timeoutMs = Math.max(100, Math.min(Number(options.timeoutMs) || 5000, 30_000));
155
+ let timer = null;
156
+ let onAbort = null;
157
+ try {
158
+ const pending = [
159
+ lookup(hostname, { all: true, verbatim: true }),
160
+ new Promise((_, reject) => {
161
+ timer = setTimeout(() => reject(new Error('DNS lookup timed out.')), timeoutMs);
162
+ }),
163
+ ];
164
+ if (options.signal) {
165
+ pending.push(new Promise((_, reject) => {
166
+ onAbort = () => {
167
+ reject(urlAbortError(options.signal));
168
+ };
169
+ options.signal.addEventListener('abort', onAbort, { once: true });
170
+ }));
171
+ }
172
+ const addresses = await Promise.race(pending);
173
+ const resolved = Array.isArray(addresses) ? addresses : [addresses];
174
+ if (resolved.length === 0) return { allowed: false };
175
+ if (resolved.some((entry) => isPrivateHost(entry?.address || entry))) {
176
+ return { allowed: false };
177
+ }
178
+ return { allowed: true };
179
+ } catch (error) {
180
+ if (options.signal?.aborted) throw error;
181
+ return { allowed: false };
182
+ } finally {
183
+ if (timer) clearTimeout(timer);
184
+ if (onAbort) options.signal?.removeEventListener('abort', onAbort);
185
+ }
186
+ }
187
+
188
+ async function validateAndroidIntentUrl(urlString, options = {}) {
189
+ if (!urlString || typeof urlString !== 'string') return { allowed: false };
190
+ let parsed;
191
+ try {
192
+ parsed = new URL(urlString);
193
+ } catch {
194
+ return { allowed: false };
195
+ }
196
+ const scheme = parsed.protocol.replace(/:$/, '').toLowerCase();
197
+ if (BLOCKED_ANDROID_INTENT_SCHEMES.has(scheme)) return { allowed: false };
198
+ if (ALLOWED_SCHEMES.has(scheme)) {
199
+ return validateCloudUrlWithDns(urlString, options);
200
+ }
201
+ return { allowed: /^[a-z][a-z0-9+.-]*$/.test(scheme) };
202
+ }
203
+
204
+ module.exports = {
205
+ validateAndroidIntentUrl,
206
+ validateCloudUrl,
207
+ validateCloudUrlWithDns,
208
+ isPrivateHost,
209
+ };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { randomUUID } = require('node:crypto');
6
+ const { createAbortError, throwIfAborted } = require('./abort');
7
+
8
+ async function writeBufferAtomic(destination, data, options = {}) {
9
+ const target = String(destination || '').trim();
10
+ if (!target) throw new Error('Atomic file destination is required.');
11
+ const resolved = path.resolve(target);
12
+ const temporary = `${resolved}.${process.pid}.${randomUUID()}.tmp`;
13
+ throwIfAborted(options.signal, 'File write was aborted.');
14
+ try {
15
+ await fs.promises.writeFile(temporary, data, {
16
+ flag: 'wx',
17
+ mode: options.mode ?? 0o600,
18
+ signal: options.signal,
19
+ });
20
+ throwIfAborted(options.signal, 'File write was aborted.');
21
+ await fs.promises.rename(temporary, resolved);
22
+ return resolved;
23
+ } catch (error) {
24
+ if (options.signal?.aborted) throw createAbortError(options.signal, 'File write was aborted.');
25
+ throw error;
26
+ } finally {
27
+ await fs.promises.rm(temporary, { force: true }).catch(() => {});
28
+ }
29
+ }
30
+
31
+ module.exports = { writeBufferAtomic };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const MAX_SCREENSHOT_BYTES = 20 * 1024 * 1024;
4
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
5
+
6
+ function imageError(message, code = 'INVALID_IMAGE_PAYLOAD') {
7
+ const error = new Error(message);
8
+ error.code = code;
9
+ return error;
10
+ }
11
+
12
+ function detectImage(buffer) {
13
+ if (
14
+ buffer.length >= 24
15
+ && buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
16
+ && buffer.subarray(12, 16).toString('ascii') === 'IHDR'
17
+ ) {
18
+ const width = buffer.readUInt32BE(16);
19
+ const height = buffer.readUInt32BE(20);
20
+ if (!width || !height || width > 32768 || height > 32768 || width * height > 150_000_000) {
21
+ throw imageError('Screenshot PNG has invalid or unsafe dimensions.');
22
+ }
23
+ return { contentType: 'image/png', extension: 'png', width, height };
24
+ }
25
+ if (
26
+ buffer.length >= 4
27
+ && buffer[0] === 0xff
28
+ && buffer[1] === 0xd8
29
+ && buffer[buffer.length - 2] === 0xff
30
+ && buffer[buffer.length - 1] === 0xd9
31
+ ) {
32
+ return { contentType: 'image/jpeg', extension: 'jpg', width: null, height: null };
33
+ }
34
+ throw imageError('Screenshot payload is not a valid PNG or JPEG image.');
35
+ }
36
+
37
+ function validateImageBuffer(value, options = {}) {
38
+ const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value || []);
39
+ const maxBytes = Math.max(1024, Number(options.maxBytes) || MAX_SCREENSHOT_BYTES);
40
+ if (!buffer.length) throw imageError('Screenshot payload is empty.');
41
+ if (buffer.length > maxBytes) {
42
+ throw imageError(
43
+ `Screenshot payload exceeds the ${maxBytes}-byte limit.`,
44
+ 'IMAGE_PAYLOAD_TOO_LARGE',
45
+ );
46
+ }
47
+ const detected = detectImage(buffer);
48
+ const allowedTypes = Array.isArray(options.allowedTypes) && options.allowedTypes.length
49
+ ? new Set(options.allowedTypes.map((type) => String(type).toLowerCase()))
50
+ : null;
51
+ if (allowedTypes && !allowedTypes.has(detected.contentType)) {
52
+ throw imageError(`Screenshot type ${detected.contentType} is not allowed.`);
53
+ }
54
+ return { buffer, ...detected };
55
+ }
56
+
57
+ function decodeBase64Image(value, options = {}) {
58
+ const text = String(value || '');
59
+ if (!text) throw imageError('Screenshot payload is empty.');
60
+ let declaredType = null;
61
+ let encoded = text;
62
+ if (text.startsWith('data:')) {
63
+ const match = text.match(/^data:image\/(png|jpeg|jpg);base64,([A-Za-z0-9+/]*={0,2})$/i);
64
+ if (!match) throw imageError('Screenshot data URL is malformed or uses an unsupported type.');
65
+ declaredType = match[1].toLowerCase() === 'png' ? 'image/png' : 'image/jpeg';
66
+ encoded = match[2];
67
+ }
68
+ if (!encoded || encoded.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) {
69
+ throw imageError('Screenshot base64 payload is malformed.');
70
+ }
71
+ const maxBytes = Math.max(1024, Number(options.maxBytes) || MAX_SCREENSHOT_BYTES);
72
+ const padding = encoded.endsWith('==') ? 2 : (encoded.endsWith('=') ? 1 : 0);
73
+ const estimatedBytes = Math.floor(encoded.length * 3 / 4) - padding;
74
+ if (estimatedBytes > maxBytes) {
75
+ throw imageError(
76
+ `Screenshot payload exceeds the ${maxBytes}-byte limit.`,
77
+ 'IMAGE_PAYLOAD_TOO_LARGE',
78
+ );
79
+ }
80
+ const buffer = Buffer.from(encoded, 'base64');
81
+ if (buffer.toString('base64').replace(/=+$/, '') !== encoded.replace(/=+$/, '')) {
82
+ throw imageError('Screenshot base64 payload is malformed.');
83
+ }
84
+ const validated = validateImageBuffer(buffer, { ...options, maxBytes });
85
+ if (declaredType && declaredType !== validated.contentType) {
86
+ throw imageError('Screenshot data URL type does not match its image bytes.');
87
+ }
88
+ return validated;
89
+ }
90
+
91
+ module.exports = {
92
+ MAX_SCREENSHOT_BYTES,
93
+ decodeBase64Image,
94
+ validateImageBuffer,
95
+ };
@@ -99,6 +99,24 @@ function logRequestSummary(level, req, message, extra = null) {
99
99
  console[level](redactSecrets(`${prefix} ${message}`), summary);
100
100
  }
101
101
 
102
+ function createServiceLogger(serviceName) {
103
+ const prefix = `[${String(serviceName || 'Service').trim() || 'Service'}]`;
104
+ return {
105
+ debug(...args) {
106
+ console.debug(prefix, ...args);
107
+ },
108
+ info(...args) {
109
+ console.info(prefix, ...args);
110
+ },
111
+ warn(...args) {
112
+ console.warn(prefix, ...args);
113
+ },
114
+ error(...args) {
115
+ console.error(prefix, ...args);
116
+ },
117
+ };
118
+ }
119
+
102
120
  function getLogFile() {
103
121
  try {
104
122
  const { DATA_DIR } = require('../../runtime/paths');
@@ -180,6 +198,7 @@ function setupConsoleInterceptor(io) {
180
198
  }
181
199
 
182
200
  module.exports = {
201
+ createServiceLogger,
183
202
  formatLogArgs,
184
203
  logRequestSummary,
185
204
  setupConsoleInterceptor
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const { createAbortError, isAbortError } = require('./abort');
4
+
5
+ const RETRYABLE_HTTP_STATUS = new Set([
6
+ 408, 409, 425, 429, 500, 502, 503, 504, 520, 521, 522, 524, 529,
7
+ ]);
8
+ const RETRYABLE_NETWORK_CODES = new Set([
9
+ 'ECONNRESET',
10
+ 'ECONNREFUSED',
11
+ 'ETIMEDOUT',
12
+ 'EPIPE',
13
+ 'EAI_AGAIN',
14
+ 'ENOTFOUND',
15
+ 'ENETUNREACH',
16
+ 'EHOSTUNREACH',
17
+ 'EAGAIN',
18
+ 'UND_ERR_CONNECT_TIMEOUT',
19
+ 'UND_ERR_SOCKET',
20
+ 'UND_ERR_HEADERS_TIMEOUT',
21
+ 'INTEGRATION_HTTP_TIMEOUT',
22
+ ]);
23
+
24
+ function getHttpStatus(error) {
25
+ if (!error || typeof error !== 'object') return null;
26
+ const candidates = [
27
+ error.status,
28
+ error.statusCode,
29
+ error.response?.status,
30
+ error.cause?.status,
31
+ ];
32
+ for (const value of candidates) {
33
+ const status = Number(value);
34
+ if (Number.isFinite(status) && status >= 100 && status < 600) return status;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function getErrorCode(error) {
40
+ if (!error || typeof error !== 'object') return null;
41
+ return error.code || error.errno || error.cause?.code || null;
42
+ }
43
+
44
+ function readHeader(headers, name) {
45
+ if (!headers) return undefined;
46
+ if (typeof headers.get === 'function') return headers.get(name);
47
+ return headers[name] ?? headers[name.toLowerCase()];
48
+ }
49
+
50
+ function retryAfterMilliseconds(headers, now = Date.now()) {
51
+ const milliseconds = readHeader(headers, 'retry-after-ms');
52
+ if (milliseconds !== undefined && milliseconds !== null && String(milliseconds).trim()) {
53
+ const parsed = Number(milliseconds);
54
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
55
+ }
56
+
57
+ const retryAfter = readHeader(headers, 'retry-after');
58
+ if (retryAfter === undefined || retryAfter === null || !String(retryAfter).trim()) {
59
+ return null;
60
+ }
61
+ const seconds = Number(retryAfter);
62
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
63
+ const date = Date.parse(String(retryAfter));
64
+ return Number.isFinite(date) ? Math.max(0, date - now) : null;
65
+ }
66
+
67
+ function computeBackoffMs(attempt, baseDelayMs, maxDelayMs) {
68
+ const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
69
+ return Math.round(exp / 2 + Math.random() * (exp / 2));
70
+ }
71
+
72
+ function isTransientIoError(error) {
73
+ if (!error || isAbortError(error)) return false;
74
+ const status = getHttpStatus(error);
75
+ if (status !== null) return RETRYABLE_HTTP_STATUS.has(status);
76
+ const code = getErrorCode(error);
77
+ return Boolean(code && RETRYABLE_NETWORK_CODES.has(String(code)));
78
+ }
79
+
80
+ function abortableDelay(milliseconds, signal = null) {
81
+ if (signal?.aborted) return Promise.reject(createAbortError(signal));
82
+ return new Promise((resolve, reject) => {
83
+ let timer = null;
84
+ const onAbort = () => {
85
+ if (timer) clearTimeout(timer);
86
+ signal?.removeEventListener('abort', onAbort);
87
+ reject(createAbortError(signal));
88
+ };
89
+ timer = setTimeout(() => {
90
+ signal?.removeEventListener('abort', onAbort);
91
+ resolve();
92
+ }, Math.max(0, Number(milliseconds) || 0));
93
+ signal?.addEventListener('abort', onAbort, { once: true });
94
+ });
95
+ }
96
+
97
+ module.exports = {
98
+ RETRYABLE_HTTP_STATUS,
99
+ RETRYABLE_NETWORK_CODES,
100
+ abortableDelay,
101
+ computeBackoffMs,
102
+ getErrorCode,
103
+ getHttpStatus,
104
+ isTransientIoError,
105
+ readHeader,
106
+ retryAfterMilliseconds,
107
+ };