whale-code 6.4.0

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 (319) hide show
  1. package/README.md +95 -0
  2. package/bin/swag-agent.js +9 -0
  3. package/bin/swagmanager-mcp.js +321 -0
  4. package/dist/cli/app.d.ts +26 -0
  5. package/dist/cli/app.js +64 -0
  6. package/dist/cli/chat/AgentSelector.d.ts +14 -0
  7. package/dist/cli/chat/AgentSelector.js +14 -0
  8. package/dist/cli/chat/ChatApp.d.ts +9 -0
  9. package/dist/cli/chat/ChatApp.js +267 -0
  10. package/dist/cli/chat/ChatInput.d.ts +39 -0
  11. package/dist/cli/chat/ChatInput.js +509 -0
  12. package/dist/cli/chat/MarkdownText.d.ts +10 -0
  13. package/dist/cli/chat/MarkdownText.js +20 -0
  14. package/dist/cli/chat/MessageList.d.ts +37 -0
  15. package/dist/cli/chat/MessageList.js +80 -0
  16. package/dist/cli/chat/ModelSelector.d.ts +20 -0
  17. package/dist/cli/chat/ModelSelector.js +73 -0
  18. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  19. package/dist/cli/chat/RewindViewer.js +185 -0
  20. package/dist/cli/chat/StoreSelector.d.ts +14 -0
  21. package/dist/cli/chat/StoreSelector.js +24 -0
  22. package/dist/cli/chat/StreamingText.d.ts +12 -0
  23. package/dist/cli/chat/StreamingText.js +12 -0
  24. package/dist/cli/chat/SubagentPanel.d.ts +45 -0
  25. package/dist/cli/chat/SubagentPanel.js +110 -0
  26. package/dist/cli/chat/TeamPanel.d.ts +21 -0
  27. package/dist/cli/chat/TeamPanel.js +42 -0
  28. package/dist/cli/chat/ToolIndicator.d.ts +25 -0
  29. package/dist/cli/chat/ToolIndicator.js +436 -0
  30. package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
  31. package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
  32. package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
  33. package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
  34. package/dist/cli/commands/config-cmd.d.ts +10 -0
  35. package/dist/cli/commands/config-cmd.js +99 -0
  36. package/dist/cli/commands/doctor.d.ts +14 -0
  37. package/dist/cli/commands/doctor.js +172 -0
  38. package/dist/cli/commands/init.d.ts +16 -0
  39. package/dist/cli/commands/init.js +278 -0
  40. package/dist/cli/commands/mcp.d.ts +12 -0
  41. package/dist/cli/commands/mcp.js +162 -0
  42. package/dist/cli/login/LoginApp.d.ts +7 -0
  43. package/dist/cli/login/LoginApp.js +157 -0
  44. package/dist/cli/print-mode.d.ts +31 -0
  45. package/dist/cli/print-mode.js +202 -0
  46. package/dist/cli/serve-mode.d.ts +37 -0
  47. package/dist/cli/serve-mode.js +636 -0
  48. package/dist/cli/services/agent-definitions.d.ts +25 -0
  49. package/dist/cli/services/agent-definitions.js +91 -0
  50. package/dist/cli/services/agent-events.d.ts +178 -0
  51. package/dist/cli/services/agent-events.js +175 -0
  52. package/dist/cli/services/agent-loop.d.ts +90 -0
  53. package/dist/cli/services/agent-loop.js +762 -0
  54. package/dist/cli/services/agent-worker-base.d.ts +97 -0
  55. package/dist/cli/services/agent-worker-base.js +220 -0
  56. package/dist/cli/services/auth-service.d.ts +30 -0
  57. package/dist/cli/services/auth-service.js +160 -0
  58. package/dist/cli/services/background-processes.d.ts +126 -0
  59. package/dist/cli/services/background-processes.js +318 -0
  60. package/dist/cli/services/browser-auth.d.ts +24 -0
  61. package/dist/cli/services/browser-auth.js +180 -0
  62. package/dist/cli/services/claude-md-loader.d.ts +16 -0
  63. package/dist/cli/services/claude-md-loader.js +58 -0
  64. package/dist/cli/services/config-store.d.ts +47 -0
  65. package/dist/cli/services/config-store.js +79 -0
  66. package/dist/cli/services/debug-log.d.ts +10 -0
  67. package/dist/cli/services/debug-log.js +52 -0
  68. package/dist/cli/services/error-logger.d.ts +58 -0
  69. package/dist/cli/services/error-logger.js +269 -0
  70. package/dist/cli/services/file-history.d.ts +21 -0
  71. package/dist/cli/services/file-history.js +83 -0
  72. package/dist/cli/services/format-server-response.d.ts +16 -0
  73. package/dist/cli/services/format-server-response.js +440 -0
  74. package/dist/cli/services/git-context.d.ts +11 -0
  75. package/dist/cli/services/git-context.js +66 -0
  76. package/dist/cli/services/hooks.d.ts +85 -0
  77. package/dist/cli/services/hooks.js +258 -0
  78. package/dist/cli/services/interactive-tools.d.ts +125 -0
  79. package/dist/cli/services/interactive-tools.js +260 -0
  80. package/dist/cli/services/keybinding-manager.d.ts +52 -0
  81. package/dist/cli/services/keybinding-manager.js +115 -0
  82. package/dist/cli/services/local-tools.d.ts +22 -0
  83. package/dist/cli/services/local-tools.js +697 -0
  84. package/dist/cli/services/lsp-manager.d.ts +18 -0
  85. package/dist/cli/services/lsp-manager.js +717 -0
  86. package/dist/cli/services/mcp-client.d.ts +48 -0
  87. package/dist/cli/services/mcp-client.js +157 -0
  88. package/dist/cli/services/memory-manager.d.ts +16 -0
  89. package/dist/cli/services/memory-manager.js +57 -0
  90. package/dist/cli/services/model-manager.d.ts +18 -0
  91. package/dist/cli/services/model-manager.js +71 -0
  92. package/dist/cli/services/model-router.d.ts +26 -0
  93. package/dist/cli/services/model-router.js +149 -0
  94. package/dist/cli/services/permission-modes.d.ts +13 -0
  95. package/dist/cli/services/permission-modes.js +43 -0
  96. package/dist/cli/services/rewind.d.ts +84 -0
  97. package/dist/cli/services/rewind.js +194 -0
  98. package/dist/cli/services/ripgrep.d.ts +28 -0
  99. package/dist/cli/services/ripgrep.js +138 -0
  100. package/dist/cli/services/sandbox.d.ts +29 -0
  101. package/dist/cli/services/sandbox.js +97 -0
  102. package/dist/cli/services/server-tools.d.ts +61 -0
  103. package/dist/cli/services/server-tools.js +543 -0
  104. package/dist/cli/services/session-persistence.d.ts +23 -0
  105. package/dist/cli/services/session-persistence.js +99 -0
  106. package/dist/cli/services/subagent-worker.d.ts +19 -0
  107. package/dist/cli/services/subagent-worker.js +41 -0
  108. package/dist/cli/services/subagent.d.ts +47 -0
  109. package/dist/cli/services/subagent.js +647 -0
  110. package/dist/cli/services/system-prompt.d.ts +7 -0
  111. package/dist/cli/services/system-prompt.js +198 -0
  112. package/dist/cli/services/team-lead.d.ts +73 -0
  113. package/dist/cli/services/team-lead.js +512 -0
  114. package/dist/cli/services/team-state.d.ts +77 -0
  115. package/dist/cli/services/team-state.js +398 -0
  116. package/dist/cli/services/teammate.d.ts +31 -0
  117. package/dist/cli/services/teammate.js +689 -0
  118. package/dist/cli/services/telemetry.d.ts +61 -0
  119. package/dist/cli/services/telemetry.js +209 -0
  120. package/dist/cli/services/tools/agent-tools.d.ts +14 -0
  121. package/dist/cli/services/tools/agent-tools.js +347 -0
  122. package/dist/cli/services/tools/file-ops.d.ts +15 -0
  123. package/dist/cli/services/tools/file-ops.js +487 -0
  124. package/dist/cli/services/tools/search-tools.d.ts +8 -0
  125. package/dist/cli/services/tools/search-tools.js +186 -0
  126. package/dist/cli/services/tools/shell-exec.d.ts +10 -0
  127. package/dist/cli/services/tools/shell-exec.js +168 -0
  128. package/dist/cli/services/tools/task-manager.d.ts +28 -0
  129. package/dist/cli/services/tools/task-manager.js +209 -0
  130. package/dist/cli/services/tools/web-tools.d.ts +11 -0
  131. package/dist/cli/services/tools/web-tools.js +395 -0
  132. package/dist/cli/setup/SetupApp.d.ts +9 -0
  133. package/dist/cli/setup/SetupApp.js +191 -0
  134. package/dist/cli/shared/MatrixIntro.d.ts +4 -0
  135. package/dist/cli/shared/MatrixIntro.js +83 -0
  136. package/dist/cli/shared/Theme.d.ts +74 -0
  137. package/dist/cli/shared/Theme.js +127 -0
  138. package/dist/cli/shared/WhaleBanner.d.ts +10 -0
  139. package/dist/cli/shared/WhaleBanner.js +12 -0
  140. package/dist/cli/shared/markdown.d.ts +21 -0
  141. package/dist/cli/shared/markdown.js +756 -0
  142. package/dist/cli/status/StatusApp.d.ts +4 -0
  143. package/dist/cli/status/StatusApp.js +105 -0
  144. package/dist/cli/stores/StoreApp.d.ts +7 -0
  145. package/dist/cli/stores/StoreApp.js +81 -0
  146. package/dist/index.d.ts +15 -0
  147. package/dist/index.js +538 -0
  148. package/dist/local-agent/connection.d.ts +48 -0
  149. package/dist/local-agent/connection.js +332 -0
  150. package/dist/local-agent/discovery.d.ts +18 -0
  151. package/dist/local-agent/discovery.js +146 -0
  152. package/dist/local-agent/executor.d.ts +34 -0
  153. package/dist/local-agent/executor.js +241 -0
  154. package/dist/local-agent/index.d.ts +14 -0
  155. package/dist/local-agent/index.js +198 -0
  156. package/dist/node/adapters/base.d.ts +35 -0
  157. package/dist/node/adapters/base.js +10 -0
  158. package/dist/node/adapters/discord.d.ts +29 -0
  159. package/dist/node/adapters/discord.js +299 -0
  160. package/dist/node/adapters/email.d.ts +23 -0
  161. package/dist/node/adapters/email.js +218 -0
  162. package/dist/node/adapters/imessage.d.ts +17 -0
  163. package/dist/node/adapters/imessage.js +118 -0
  164. package/dist/node/adapters/slack.d.ts +26 -0
  165. package/dist/node/adapters/slack.js +259 -0
  166. package/dist/node/adapters/sms.d.ts +23 -0
  167. package/dist/node/adapters/sms.js +161 -0
  168. package/dist/node/adapters/telegram.d.ts +17 -0
  169. package/dist/node/adapters/telegram.js +101 -0
  170. package/dist/node/adapters/webchat.d.ts +27 -0
  171. package/dist/node/adapters/webchat.js +160 -0
  172. package/dist/node/adapters/whatsapp.d.ts +28 -0
  173. package/dist/node/adapters/whatsapp.js +230 -0
  174. package/dist/node/cli.d.ts +2 -0
  175. package/dist/node/cli.js +325 -0
  176. package/dist/node/config.d.ts +17 -0
  177. package/dist/node/config.js +31 -0
  178. package/dist/node/runtime.d.ts +50 -0
  179. package/dist/node/runtime.js +351 -0
  180. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
  181. package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
  182. package/dist/server/handlers/analytics.d.ts +17 -0
  183. package/dist/server/handlers/analytics.js +266 -0
  184. package/dist/server/handlers/api-keys.d.ts +6 -0
  185. package/dist/server/handlers/api-keys.js +221 -0
  186. package/dist/server/handlers/billing.d.ts +33 -0
  187. package/dist/server/handlers/billing.js +272 -0
  188. package/dist/server/handlers/browser.d.ts +10 -0
  189. package/dist/server/handlers/browser.js +517 -0
  190. package/dist/server/handlers/catalog.d.ts +99 -0
  191. package/dist/server/handlers/catalog.js +976 -0
  192. package/dist/server/handlers/comms.d.ts +254 -0
  193. package/dist/server/handlers/comms.js +588 -0
  194. package/dist/server/handlers/creations.d.ts +6 -0
  195. package/dist/server/handlers/creations.js +479 -0
  196. package/dist/server/handlers/crm.d.ts +89 -0
  197. package/dist/server/handlers/crm.js +538 -0
  198. package/dist/server/handlers/discovery.d.ts +6 -0
  199. package/dist/server/handlers/discovery.js +288 -0
  200. package/dist/server/handlers/embeddings.d.ts +92 -0
  201. package/dist/server/handlers/embeddings.js +197 -0
  202. package/dist/server/handlers/enrichment.d.ts +8 -0
  203. package/dist/server/handlers/enrichment.js +768 -0
  204. package/dist/server/handlers/image-gen.d.ts +6 -0
  205. package/dist/server/handlers/image-gen.js +409 -0
  206. package/dist/server/handlers/inventory.d.ts +319 -0
  207. package/dist/server/handlers/inventory.js +447 -0
  208. package/dist/server/handlers/kali.d.ts +10 -0
  209. package/dist/server/handlers/kali.js +210 -0
  210. package/dist/server/handlers/llm-providers.d.ts +6 -0
  211. package/dist/server/handlers/llm-providers.js +673 -0
  212. package/dist/server/handlers/local-agent.d.ts +6 -0
  213. package/dist/server/handlers/local-agent.js +118 -0
  214. package/dist/server/handlers/meta-ads.d.ts +111 -0
  215. package/dist/server/handlers/meta-ads.js +2279 -0
  216. package/dist/server/handlers/nodes.d.ts +33 -0
  217. package/dist/server/handlers/nodes.js +699 -0
  218. package/dist/server/handlers/operations.d.ts +138 -0
  219. package/dist/server/handlers/operations.js +131 -0
  220. package/dist/server/handlers/platform.d.ts +23 -0
  221. package/dist/server/handlers/platform.js +227 -0
  222. package/dist/server/handlers/supply-chain.d.ts +19 -0
  223. package/dist/server/handlers/supply-chain.js +327 -0
  224. package/dist/server/handlers/transcription.d.ts +17 -0
  225. package/dist/server/handlers/transcription.js +121 -0
  226. package/dist/server/handlers/video-gen.d.ts +6 -0
  227. package/dist/server/handlers/video-gen.js +466 -0
  228. package/dist/server/handlers/voice.d.ts +8 -0
  229. package/dist/server/handlers/voice.js +1146 -0
  230. package/dist/server/handlers/workflow-steps.d.ts +86 -0
  231. package/dist/server/handlers/workflow-steps.js +2349 -0
  232. package/dist/server/handlers/workflows.d.ts +7 -0
  233. package/dist/server/handlers/workflows.js +989 -0
  234. package/dist/server/index.d.ts +1 -0
  235. package/dist/server/index.js +2427 -0
  236. package/dist/server/lib/batch-client.d.ts +80 -0
  237. package/dist/server/lib/batch-client.js +467 -0
  238. package/dist/server/lib/code-worker-pool.d.ts +31 -0
  239. package/dist/server/lib/code-worker-pool.js +224 -0
  240. package/dist/server/lib/code-worker.d.ts +1 -0
  241. package/dist/server/lib/code-worker.js +188 -0
  242. package/dist/server/lib/compaction-service.d.ts +32 -0
  243. package/dist/server/lib/compaction-service.js +162 -0
  244. package/dist/server/lib/logger.d.ts +19 -0
  245. package/dist/server/lib/logger.js +46 -0
  246. package/dist/server/lib/otel.d.ts +38 -0
  247. package/dist/server/lib/otel.js +126 -0
  248. package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
  249. package/dist/server/lib/pg-rate-limiter.js +86 -0
  250. package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
  251. package/dist/server/lib/prompt-sanitizer.js +177 -0
  252. package/dist/server/lib/provider-capabilities.d.ts +85 -0
  253. package/dist/server/lib/provider-capabilities.js +190 -0
  254. package/dist/server/lib/provider-failover.d.ts +74 -0
  255. package/dist/server/lib/provider-failover.js +210 -0
  256. package/dist/server/lib/rate-limiter.d.ts +39 -0
  257. package/dist/server/lib/rate-limiter.js +147 -0
  258. package/dist/server/lib/server-agent-loop.d.ts +107 -0
  259. package/dist/server/lib/server-agent-loop.js +667 -0
  260. package/dist/server/lib/server-subagent.d.ts +78 -0
  261. package/dist/server/lib/server-subagent.js +203 -0
  262. package/dist/server/lib/session-checkpoint.d.ts +51 -0
  263. package/dist/server/lib/session-checkpoint.js +145 -0
  264. package/dist/server/lib/ssrf-guard.d.ts +13 -0
  265. package/dist/server/lib/ssrf-guard.js +240 -0
  266. package/dist/server/lib/supabase-client.d.ts +7 -0
  267. package/dist/server/lib/supabase-client.js +78 -0
  268. package/dist/server/lib/template-resolver.d.ts +31 -0
  269. package/dist/server/lib/template-resolver.js +215 -0
  270. package/dist/server/lib/utils.d.ts +16 -0
  271. package/dist/server/lib/utils.js +147 -0
  272. package/dist/server/local-agent-gateway.d.ts +82 -0
  273. package/dist/server/local-agent-gateway.js +426 -0
  274. package/dist/server/providers/anthropic.d.ts +20 -0
  275. package/dist/server/providers/anthropic.js +199 -0
  276. package/dist/server/providers/bedrock.d.ts +20 -0
  277. package/dist/server/providers/bedrock.js +194 -0
  278. package/dist/server/providers/gemini.d.ts +24 -0
  279. package/dist/server/providers/gemini.js +486 -0
  280. package/dist/server/providers/openai.d.ts +24 -0
  281. package/dist/server/providers/openai.js +522 -0
  282. package/dist/server/providers/registry.d.ts +32 -0
  283. package/dist/server/providers/registry.js +58 -0
  284. package/dist/server/providers/shared.d.ts +32 -0
  285. package/dist/server/providers/shared.js +124 -0
  286. package/dist/server/providers/types.d.ts +92 -0
  287. package/dist/server/providers/types.js +12 -0
  288. package/dist/server/proxy-handlers.d.ts +6 -0
  289. package/dist/server/proxy-handlers.js +89 -0
  290. package/dist/server/tool-router.d.ts +149 -0
  291. package/dist/server/tool-router.js +803 -0
  292. package/dist/server/validation.d.ts +24 -0
  293. package/dist/server/validation.js +301 -0
  294. package/dist/server/worker.d.ts +19 -0
  295. package/dist/server/worker.js +201 -0
  296. package/dist/setup.d.ts +8 -0
  297. package/dist/setup.js +181 -0
  298. package/dist/shared/agent-core.d.ts +157 -0
  299. package/dist/shared/agent-core.js +534 -0
  300. package/dist/shared/anthropic-types.d.ts +105 -0
  301. package/dist/shared/anthropic-types.js +7 -0
  302. package/dist/shared/api-client.d.ts +90 -0
  303. package/dist/shared/api-client.js +379 -0
  304. package/dist/shared/constants.d.ts +33 -0
  305. package/dist/shared/constants.js +80 -0
  306. package/dist/shared/sse-parser.d.ts +26 -0
  307. package/dist/shared/sse-parser.js +259 -0
  308. package/dist/shared/tool-dispatch.d.ts +52 -0
  309. package/dist/shared/tool-dispatch.js +191 -0
  310. package/dist/shared/types.d.ts +72 -0
  311. package/dist/shared/types.js +7 -0
  312. package/dist/updater.d.ts +25 -0
  313. package/dist/updater.js +140 -0
  314. package/dist/webchat/widget.d.ts +0 -0
  315. package/dist/webchat/widget.js +397 -0
  316. package/package.json +95 -0
  317. package/src/cli/services/builtin-skills/commit.md +19 -0
  318. package/src/cli/services/builtin-skills/review-pr.md +21 -0
  319. package/src/cli/services/builtin-skills/review.md +18 -0
@@ -0,0 +1,803 @@
1
+ /**
2
+ * Tool Router — unified tool registry, dispatch, and execution.
3
+ *
4
+ * SINGLE SOURCE OF TRUTH for all AI systems (MCP server, HTTP agent, workflows, CLI).
5
+ *
6
+ * Architecture:
7
+ * - ai_tool_registry (DB): tool definitions (name, description, schema) — queried by ALL clients
8
+ * - TOOL_HANDLERS (code): name → handler function mapping — the only place handler routing lives
9
+ * - user_tools (DB): per-store custom tools — additive, never filtered from built-ins
10
+ * - getToolsForAgent(): consistent tool set — all registry tools always available
11
+ * - executeTool(): unified dispatch — handler lookup + timeout + audit logging
12
+ *
13
+ * To add a new tool:
14
+ * 1. Add handler in src/server/handlers/
15
+ * 2. Import and register in TOOL_HANDLERS below
16
+ * 3. Add row to ai_tool_registry table (defines schema for all clients)
17
+ * That's it — MCP, WhaleChat, workflows, CLI all pick it up automatically.
18
+ */
19
+ import { sanitizeError } from "../shared/agent-core.js";
20
+ import { validateToolArgs } from "./validation.js";
21
+ import { sanitizeToolDescription } from "./lib/prompt-sanitizer.js";
22
+ import { validateUrl } from "./lib/ssrf-guard.js";
23
+ import { handleInventory, handleInventoryQuery, handleInventoryAudit } from "./handlers/inventory.js";
24
+ import { handlePurchaseOrders, handleTransfers } from "./handlers/supply-chain.js";
25
+ import { handleProducts, handleCollections } from "./handlers/catalog.js";
26
+ import { handleCustomers, handleOrders } from "./handlers/crm.js";
27
+ import { handleAnalytics } from "./handlers/analytics.js";
28
+ import { handleLocations, handleSuppliers, handleAlerts, handleAuditTrail, handleStore } from "./handlers/operations.js";
29
+ import { handleEmail, handleDocuments } from "./handlers/comms.js";
30
+ import { handleWebSearch, handleTelemetry } from "./handlers/platform.js";
31
+ import { handleBrowser } from "./handlers/browser.js";
32
+ import { handleDiscovery } from "./handlers/discovery.js";
33
+ import { handleVoice } from "./handlers/voice.js";
34
+ import { handleWorkflows } from "./handlers/workflows.js";
35
+ import { handleEmbeddings } from "./handlers/embeddings.js";
36
+ import { handleLLM } from "./handlers/llm-providers.js";
37
+ import { handleImageGen } from "./handlers/image-gen.js";
38
+ import { handleVideoGen } from "./handlers/video-gen.js";
39
+ import { handleAPIKeys } from "./handlers/api-keys.js";
40
+ import { handleCreations } from "./handlers/creations.js";
41
+ import { handleMetaAds } from "./handlers/meta-ads.js";
42
+ import { handleKali } from "./handlers/kali.js";
43
+ import { handleLocalAgent } from "./handlers/local-agent.js";
44
+ import { handleEnrichment } from "./handlers/enrichment.js";
45
+ import { summarizeResult, withTimeout } from "./lib/utils.js";
46
+ // ============================================================================
47
+ // AUDIT LOG BATCHING — buffer inserts and flush periodically
48
+ // ============================================================================
49
+ const auditLogBuffer = [];
50
+ const AUDIT_FLUSH_INTERVAL = 500; // ms
51
+ const AUDIT_FLUSH_MAX = 100; // max records before force flush
52
+ let auditFlushTimer = null;
53
+ export async function flushAuditLogs(supabase) {
54
+ if (auditLogBuffer.length === 0)
55
+ return;
56
+ const batch = auditLogBuffer.splice(0, auditLogBuffer.length);
57
+ try {
58
+ const { error } = await supabase.from("audit_logs").insert(batch);
59
+ if (error)
60
+ console.error("[audit-batch] flush error:", error.message, "lost", batch.length, "records");
61
+ }
62
+ catch (err) {
63
+ console.error("[audit-batch] flush exception:", err.message, "lost", batch.length, "records");
64
+ }
65
+ }
66
+ function queueAuditLog(supabase, row) {
67
+ auditLogBuffer.push(row);
68
+ if (auditLogBuffer.length >= AUDIT_FLUSH_MAX) {
69
+ flushAuditLogs(supabase);
70
+ }
71
+ else if (!auditFlushTimer) {
72
+ auditFlushTimer = setTimeout(() => {
73
+ auditFlushTimer = null;
74
+ flushAuditLogs(supabase);
75
+ }, AUDIT_FLUSH_INTERVAL);
76
+ }
77
+ }
78
+ // ============================================================================
79
+ // IN-MEMORY EXECUTION METRICS
80
+ // ============================================================================
81
+ const toolMetrics = new Map();
82
+ export function getToolMetrics() {
83
+ const result = {};
84
+ for (const [name, m] of toolMetrics) {
85
+ result[name] = { invocations: m.invocations, errors: m.errors, avgMs: m.invocations > 0 ? Math.round(m.totalMs / m.invocations) : 0, lastMs: m.lastMs };
86
+ }
87
+ return result;
88
+ }
89
+ function recordToolMetric(toolName, durationMs, success) {
90
+ let m = toolMetrics.get(toolName);
91
+ if (!m) {
92
+ m = { invocations: 0, errors: 0, totalMs: 0, lastMs: 0 };
93
+ toolMetrics.set(toolName, m);
94
+ }
95
+ m.invocations++;
96
+ m.totalMs += durationMs;
97
+ m.lastMs = durationMs;
98
+ if (!success)
99
+ m.errors++;
100
+ }
101
+ let cachedResult = null;
102
+ let cacheTime = 0;
103
+ /** Pre-computed API-ready tool defs (name + truncated description + input_schema).
104
+ * Computed once at load time, reused every turn without re-serialization. */
105
+ let cachedApiToolDefs = null;
106
+ /** Truncate a description to maxLen characters, ending at a word boundary with ellipsis. */
107
+ function truncateDescription(desc, maxLen = 150) {
108
+ if (desc.length <= maxLen)
109
+ return desc;
110
+ const cut = desc.lastIndexOf(" ", maxLen - 3);
111
+ return desc.substring(0, cut > 0 ? cut : maxLen - 3) + "...";
112
+ }
113
+ /**
114
+ * Load tools from ai_tool_registry, split into core (auto_load=true) and extended.
115
+ * The `discover_tools` meta-tool is always injected into core.
116
+ * Descriptions are truncated to 150 chars at load time.
117
+ */
118
+ export async function loadTools(supabase, forceRefresh = false) {
119
+ if (!forceRefresh && cachedResult && Date.now() - cacheTime < 60_000)
120
+ return cachedResult;
121
+ const { data, error } = await supabase
122
+ .from("ai_tool_registry")
123
+ .select("name, description, definition, auto_load")
124
+ .eq("is_active", true)
125
+ .neq("tool_mode", "code");
126
+ if (error || !data) {
127
+ return cachedResult || { core: [], extended: [], all: [] };
128
+ }
129
+ const allTools = data.map((t) => ({
130
+ name: t.name,
131
+ description: sanitizeToolDescription(truncateDescription(t.description || t.definition?.description || t.name)),
132
+ input_schema: t.definition?.input_schema || { type: "object", properties: {} },
133
+ }));
134
+ const core = [];
135
+ const extended = [];
136
+ // Check if any row has auto_load set — if the column doesn't exist yet
137
+ // (migration not run), all values will be null/undefined. In that case,
138
+ // treat ALL tools as core to preserve pre-migration behavior.
139
+ const migrationApplied = data.some((t) => t.auto_load === true || t.auto_load === false);
140
+ if (!migrationApplied) {
141
+ // Pre-migration: all tools are core (original behavior)
142
+ core.push(...allTools);
143
+ }
144
+ else {
145
+ for (let i = 0; i < data.length; i++) {
146
+ if (data[i].auto_load) {
147
+ core.push(allTools[i]);
148
+ }
149
+ else {
150
+ extended.push(allTools[i]);
151
+ }
152
+ }
153
+ }
154
+ // Always inject discover_tools into core set
155
+ if (!core.some(t => t.name === "discover_tools")) {
156
+ core.push(DISCOVER_TOOLS_DEF);
157
+ }
158
+ // Pre-compute API-ready tool defs keyed by name — avoids per-turn .map()
159
+ cachedApiToolDefs = new Map();
160
+ for (const t of [...core, ...extended]) {
161
+ cachedApiToolDefs.set(t.name, {
162
+ name: t.name,
163
+ description: t.description,
164
+ input_schema: t.input_schema,
165
+ });
166
+ }
167
+ cachedResult = { core, extended, all: allTools };
168
+ cacheTime = Date.now();
169
+ return cachedResult;
170
+ }
171
+ /**
172
+ * Get pre-computed API-ready tool definitions. Returns frozen objects keyed by name
173
+ * so the agent loop can look up defs without re-mapping every turn.
174
+ * Falls back to building from a ToolDef[] if cache not yet populated.
175
+ */
176
+ export function getCachedToolDefs(tools) {
177
+ if (cachedApiToolDefs) {
178
+ // Fast path: look up each active tool in the pre-computed map
179
+ const result = [];
180
+ for (const t of tools) {
181
+ const cached = cachedApiToolDefs.get(t.name);
182
+ if (cached) {
183
+ result.push(cached);
184
+ }
185
+ else {
186
+ // Dynamic tool (e.g. delegate_task, user tools, discovered tools) — build on the fly
187
+ result.push({
188
+ name: t.name,
189
+ description: truncateDescription(t.description),
190
+ input_schema: t.input_schema,
191
+ });
192
+ }
193
+ }
194
+ return result;
195
+ }
196
+ // Fallback: cache not populated yet — build from scratch with truncation
197
+ return tools.map((t) => ({
198
+ name: t.name,
199
+ description: truncateDescription(t.description),
200
+ input_schema: t.input_schema,
201
+ }));
202
+ }
203
+ /** discover_tools meta-tool — lets the model load extended tool schemas on demand */
204
+ const DISCOVER_TOOLS_DEF = {
205
+ name: "discover_tools",
206
+ description: "Load full schemas for extended tools not in your core set. Call this before using a tool that is listed under 'Extended Tools' in your system prompt. You can load tools by name or by category.",
207
+ input_schema: {
208
+ type: "object",
209
+ properties: {
210
+ names: {
211
+ type: "array",
212
+ items: { type: "string" },
213
+ description: "Specific tool names to load (e.g. ['kali', 'browser', 'voice'])",
214
+ },
215
+ category: {
216
+ type: "string",
217
+ description: "Load all tools in this category (e.g. 'security', 'media', 'operations')",
218
+ },
219
+ refresh: {
220
+ type: "boolean",
221
+ description: "Force reload tool definitions from DB (bust cache). Use when tools may have been updated mid-conversation.",
222
+ },
223
+ },
224
+ },
225
+ };
226
+ // ============================================================================
227
+ // USER TOOLS (per-store custom tools from user_tools table)
228
+ // ============================================================================
229
+ const userToolCache = new Map();
230
+ const USER_TOOL_CACHE_MAX = 100;
231
+ function evictUserToolCache() {
232
+ if (userToolCache.size <= USER_TOOL_CACHE_MAX)
233
+ return;
234
+ const entries = [...userToolCache.entries()].sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
235
+ const excess = userToolCache.size - USER_TOOL_CACHE_MAX;
236
+ for (let i = 0; i < excess; i++) {
237
+ userToolCache.delete(entries[i][0]);
238
+ }
239
+ }
240
+ export async function loadUserTools(supabase, storeId) {
241
+ const cached = userToolCache.get(storeId);
242
+ if (cached && Date.now() - cached.time < 60_000) {
243
+ cached.lastAccessed = Date.now();
244
+ return { rows: cached.tools, defs: cached.defs };
245
+ }
246
+ const { data, error } = await supabase
247
+ .from("user_tools")
248
+ .select("id, name, display_name, description, input_schema, execution_type, is_read_only, requires_approval, http_config, rpc_function, sql_template, allowed_tables, max_execution_time_ms")
249
+ .eq("store_id", storeId)
250
+ .eq("is_active", true);
251
+ if (error || !data?.length)
252
+ return { rows: [], defs: [] };
253
+ const rows = data;
254
+ const defs = rows.map((t) => ({
255
+ name: `user_tool__${t.name}`,
256
+ description: sanitizeToolDescription(`[Custom Tool] ${t.display_name}: ${t.description}${t.requires_approval ? " (requires approval)" : ""}`),
257
+ input_schema: t.input_schema || { type: "object", properties: {} },
258
+ }));
259
+ userToolCache.set(storeId, { tools: rows, defs, time: Date.now(), lastAccessed: Date.now() });
260
+ evictUserToolCache();
261
+ return { rows, defs };
262
+ }
263
+ export function getUserToolByPrefixedName(rows, prefixedName) {
264
+ const toolName = prefixedName.replace(/^user_tool__/, "");
265
+ return rows.find((t) => t.name === toolName);
266
+ }
267
+ export function getToolsForAgent(agent, coreTools, userToolDefs = []) {
268
+ // Core registry tools are always available (matches MCP server behavior).
269
+ // enabled_tools only restricts custom user tools.
270
+ if (agent.enabled_tools?.length > 0) {
271
+ const filteredUserTools = userToolDefs.filter((t) => agent.enabled_tools.includes(t.name));
272
+ return [...coreTools, ...filteredUserTools];
273
+ }
274
+ return [...coreTools, ...userToolDefs];
275
+ }
276
+ const DEFAULT_TIMEOUT = 30_000;
277
+ /**
278
+ * Every built-in tool MUST be registered here. This map is the single source
279
+ * of truth for handler routing, timeouts, and store requirements.
280
+ * Adding a tool to ai_tool_registry without an entry here → "Unknown tool" error.
281
+ */
282
+ export const TOOL_HANDLERS = {
283
+ // --- Business Data ---
284
+ inventory: { handler: (sb, args, sid) => {
285
+ const a = args.action || "";
286
+ if (a.startsWith("audit_"))
287
+ return handleInventoryAudit(sb, { ...args, action: a.slice(6) }, sid);
288
+ if (["summary", "velocity", "by_location", "in_stock", "out_of_stock", "by_category"].includes(a))
289
+ return handleInventoryQuery(sb, args, sid);
290
+ return handleInventory(sb, args, sid);
291
+ }, timeout: DEFAULT_TIMEOUT, requiresStore: true },
292
+ purchase_orders: { handler: handlePurchaseOrders, timeout: DEFAULT_TIMEOUT, requiresStore: true },
293
+ transfers: { handler: handleTransfers, timeout: DEFAULT_TIMEOUT, requiresStore: true },
294
+ products: { handler: handleProducts, timeout: DEFAULT_TIMEOUT, requiresStore: true },
295
+ collections: { handler: handleCollections, timeout: DEFAULT_TIMEOUT, requiresStore: true },
296
+ customers: { handler: handleCustomers, timeout: DEFAULT_TIMEOUT, requiresStore: true },
297
+ orders: { handler: handleOrders, timeout: DEFAULT_TIMEOUT, requiresStore: true },
298
+ analytics: { handler: handleAnalytics, timeout: DEFAULT_TIMEOUT, requiresStore: true },
299
+ locations: { handler: handleLocations, timeout: DEFAULT_TIMEOUT, requiresStore: true },
300
+ suppliers: { handler: handleSuppliers, timeout: DEFAULT_TIMEOUT, requiresStore: true },
301
+ store: { handler: handleStore, timeout: DEFAULT_TIMEOUT, requiresStore: true },
302
+ // --- Communication ---
303
+ email: { handler: handleEmail, timeout: DEFAULT_TIMEOUT, requiresStore: true },
304
+ documents: { handler: handleDocuments, timeout: DEFAULT_TIMEOUT, requiresStore: true },
305
+ // --- Operations ---
306
+ alerts: { handler: handleAlerts, timeout: DEFAULT_TIMEOUT, requiresStore: true },
307
+ audit_trail: { handler: handleAuditTrail, timeout: DEFAULT_TIMEOUT, requiresStore: true },
308
+ workflows: { handler: handleWorkflows, timeout: DEFAULT_TIMEOUT, requiresStore: true },
309
+ // --- AI & Generation ---
310
+ voice: { handler: handleVoice, timeout: 120_000, requiresStore: true },
311
+ image_gen: { handler: handleImageGen, timeout: 60_000, requiresStore: true },
312
+ video_gen: { handler: handleVideoGen, timeout: 600_000, requiresStore: true },
313
+ llm: { handler: handleLLM, timeout: 120_000, requiresStore: true },
314
+ embeddings: { handler: handleEmbeddings, timeout: 60_000, requiresStore: true },
315
+ creations: { handler: handleCreations, timeout: 60_000, requiresStore: true },
316
+ // --- Platform & Infrastructure (no store required) ---
317
+ web_search: { handler: handleWebSearch, timeout: DEFAULT_TIMEOUT, requiresStore: false },
318
+ telemetry: { handler: handleTelemetry, timeout: DEFAULT_TIMEOUT, requiresStore: true },
319
+ browser: { handler: handleBrowser, timeout: 120_000, requiresStore: true },
320
+ discovery: { handler: handleDiscovery, timeout: DEFAULT_TIMEOUT, requiresStore: false },
321
+ api_keys: { handler: handleAPIKeys, timeout: DEFAULT_TIMEOUT, requiresStore: true },
322
+ // --- Advertising ---
323
+ meta_ads: { handler: handleMetaAds, timeout: 300_000, requiresStore: true },
324
+ // --- Security & Local ---
325
+ kali: { handler: handleKali, timeout: 600_000, requiresStore: true, supportsProgress: true },
326
+ local_agent: { handler: handleLocalAgent, timeout: 600_000, requiresStore: false },
327
+ // --- Customer Data Protection ---
328
+ enrichment: { handler: handleEnrichment, timeout: 60_000, requiresStore: true },
329
+ // --- Meta: Tool Discovery (lazy loading) ---
330
+ discover_tools: { handler: handleDiscoverTools, timeout: 5000, requiresStore: false },
331
+ };
332
+ Object.freeze(TOOL_HANDLERS);
333
+ /** Get all registered built-in tool names */
334
+ export function getRegisteredToolNames() {
335
+ return Object.keys(TOOL_HANDLERS);
336
+ }
337
+ // ============================================================================
338
+ // DISCOVER_TOOLS HANDLER — returns full schemas for extended tools on demand
339
+ // ============================================================================
340
+ /** Full extended tool defs (with schemas) — used by handleDiscoverTools to return schemas on demand */
341
+ let _extendedToolsCache = [];
342
+ /** Lightweight extended tool index — name + first-sentence description only (for system prompt) */
343
+ let _extendedToolsIndex = [];
344
+ /** Called by index.ts after loadTools() to populate the discover_tools cache.
345
+ * Stores full schemas for on-demand loading, plus a lightweight index for the system prompt. */
346
+ export function setExtendedToolsCache(tools) {
347
+ _extendedToolsCache = tools;
348
+ // Pre-compute lightweight index: name + first sentence only (no schemas)
349
+ _extendedToolsIndex = tools.map(t => ({
350
+ name: t.name,
351
+ description: t.description.split(".")[0],
352
+ }));
353
+ }
354
+ /** Get full extended tools with schemas (for discover_tools handler) */
355
+ export function getExtendedToolsCache() {
356
+ return _extendedToolsCache;
357
+ }
358
+ /** Get lightweight extended tools index — name + short description only (for system prompt).
359
+ * Avoids serializing full schemas into the prompt. */
360
+ export function getExtendedToolsIndex() {
361
+ return _extendedToolsIndex;
362
+ }
363
+ /**
364
+ * Get full tool schemas for specific tool names from the extended tools cache.
365
+ * Used by the agent loop to inject discovered tool schemas into the active tool set.
366
+ * Returns only tools that exist in the cache; unknown names are silently skipped.
367
+ */
368
+ export function getFullToolSchemas(toolNames) {
369
+ if (!toolNames.length || !_extendedToolsCache.length)
370
+ return [];
371
+ const nameSet = new Set(toolNames);
372
+ return _extendedToolsCache.filter(t => nameSet.has(t.name));
373
+ }
374
+ // Tool category heuristics — maps tool names to categories for category-based discovery
375
+ const TOOL_CATEGORIES = {
376
+ // Business Data
377
+ inventory: "business", purchase_orders: "business", transfers: "business",
378
+ products: "business", collections: "business", customers: "business",
379
+ orders: "business", analytics: "business", locations: "business",
380
+ suppliers: "business", store: "business",
381
+ // Communication
382
+ email: "communication", documents: "communication",
383
+ // Operations
384
+ alerts: "operations", audit_trail: "operations", workflows: "operations",
385
+ telemetry: "operations",
386
+ // Media & AI
387
+ voice: "media", image_gen: "media", video_gen: "media",
388
+ llm: "ai", embeddings: "ai", creations: "media",
389
+ // Platform
390
+ web_search: "platform", browser: "platform", discovery: "platform",
391
+ api_keys: "platform",
392
+ // Security
393
+ kali: "security", local_agent: "platform",
394
+ // Advertising
395
+ meta_ads: "advertising",
396
+ // Data
397
+ enrichment: "data",
398
+ };
399
+ async function handleDiscoverTools(sb, args) {
400
+ const names = args.names;
401
+ const category = args.category;
402
+ const refresh = args.refresh;
403
+ // Force reload from DB if refresh requested — busts the 60s cache
404
+ if (refresh) {
405
+ try {
406
+ const freshResult = await loadTools(sb, true);
407
+ setExtendedToolsCache(freshResult.extended);
408
+ }
409
+ catch {
410
+ // If refresh fails, continue with stale cache — better than erroring
411
+ }
412
+ }
413
+ if (!names?.length && !category) {
414
+ return { success: false, error: "Provide 'names' (array of tool names) or 'category' to discover tools." };
415
+ }
416
+ let matching = [];
417
+ if (names?.length) {
418
+ const nameSet = new Set(names);
419
+ matching = _extendedToolsCache.filter(t => nameSet.has(t.name));
420
+ }
421
+ if (category) {
422
+ const cat = category.toLowerCase();
423
+ const categoryTools = _extendedToolsCache.filter(t => TOOL_CATEGORIES[t.name] === cat);
424
+ // Merge without duplicates
425
+ const existingNames = new Set(matching.map(t => t.name));
426
+ for (const t of categoryTools) {
427
+ if (!existingNames.has(t.name))
428
+ matching.push(t);
429
+ }
430
+ }
431
+ if (matching.length === 0) {
432
+ const available = _extendedToolsCache.map(t => t.name).join(", ");
433
+ return { success: false, error: `No matching extended tools found. Available: ${available}` };
434
+ }
435
+ return {
436
+ success: true,
437
+ data: {
438
+ tools: matching.map(t => ({ name: t.name, description: t.description, input_schema: t.input_schema })),
439
+ count: matching.length,
440
+ refreshed: !!refresh,
441
+ },
442
+ };
443
+ }
444
+ // ============================================================================
445
+ // USER TOOL EXECUTOR — handles RPC, HTTP, SQL execution types
446
+ // ============================================================================
447
+ async function executeUserTool(supabase, userTool, args, storeId, agentId, conversationId) {
448
+ const timeout = userTool.max_execution_time_ms || 5000;
449
+ if (userTool.execution_type === "http") {
450
+ return executeHTTPUserTool(supabase, userTool, args, storeId, timeout);
451
+ }
452
+ try {
453
+ const { data, error } = await supabase.rpc("execute_user_tool", {
454
+ p_tool_id: userTool.id,
455
+ p_store_id: storeId,
456
+ p_args: args,
457
+ p_agent_id: agentId || null,
458
+ p_conversation_id: conversationId || null,
459
+ });
460
+ if (error)
461
+ return { success: false, error: error.message };
462
+ const result = data;
463
+ if (result.pending_approval) {
464
+ return { success: false, error: `Tool requires approval. Execution ID: ${result.execution_id}. ${result.message}` };
465
+ }
466
+ if (result.success && result.data && result.data.execute_sql) {
467
+ return executeSQLUserTool(supabase, result.data, args, storeId);
468
+ }
469
+ return result.success
470
+ ? { success: true, data: result.data }
471
+ : { success: false, error: result.error || "Unknown error" };
472
+ }
473
+ catch (err) {
474
+ return { success: false, error: sanitizeError(err) };
475
+ }
476
+ }
477
+ async function executeHTTPUserTool(supabase, userTool, args, storeId, timeout) {
478
+ const config = userTool.http_config;
479
+ if (!config || !config.url) {
480
+ return { success: false, error: "HTTP tool has no URL configured" };
481
+ }
482
+ const httpCfg = config;
483
+ let url = httpCfg.url;
484
+ let headers = { ...(httpCfg.headers || {}) };
485
+ // Collect secret names referenced in the URL, headers, and body template
486
+ const configStr = JSON.stringify({ url: httpCfg.url, headers: httpCfg.headers, body: httpCfg.body_template });
487
+ const secretRefs = [...configStr.matchAll(/\{\{secret:(\w+)\}\}/g)].map(m => m[1]);
488
+ // Decrypt each referenced secret via RPC (consistent with all other handlers)
489
+ const secretMap = new Map();
490
+ await Promise.all([...new Set(secretRefs)].map(async (name) => {
491
+ try {
492
+ const { data } = await supabase.rpc("decrypt_secret", { p_name: name, p_store_id: storeId });
493
+ if (data)
494
+ secretMap.set(name, data);
495
+ }
496
+ catch { /* secret not found — will remain as {{secret:NAME}} placeholder */ }
497
+ }));
498
+ const resolveSecrets = (text) => {
499
+ return text.replace(/\{\{secret:(\w+)\}\}/g, (_, name) => secretMap.get(name) || `{{secret:${name}}}`);
500
+ };
501
+ const resolveArgs = (text) => {
502
+ return text.replace(/\{\{(\w+)\}\}/g, (_, name) => {
503
+ if (name === "secret")
504
+ return `{{${name}}}`;
505
+ const val = args[name];
506
+ return val !== undefined ? String(val) : `{{${name}}}`;
507
+ });
508
+ };
509
+ const resolve = (text) => resolveSecrets(resolveArgs(text));
510
+ url = resolve(url);
511
+ for (const [key, val] of Object.entries(headers)) {
512
+ headers[key] = resolve(val);
513
+ }
514
+ let body;
515
+ const method = (httpCfg.method || "GET").toUpperCase();
516
+ if (method !== "GET" && method !== "HEAD") {
517
+ if (httpCfg.body_template) {
518
+ const resolvedBody = {};
519
+ for (const [key, val] of Object.entries(httpCfg.body_template)) {
520
+ if (typeof val === "string") {
521
+ resolvedBody[key] = resolve(val);
522
+ }
523
+ else {
524
+ resolvedBody[key] = val;
525
+ }
526
+ }
527
+ for (const [key, val] of Object.entries(args)) {
528
+ if (!(key in resolvedBody)) {
529
+ resolvedBody[key] = val;
530
+ }
531
+ }
532
+ body = JSON.stringify(resolvedBody);
533
+ }
534
+ else {
535
+ body = JSON.stringify(args);
536
+ }
537
+ if (!headers["Content-Type"] && !headers["content-type"]) {
538
+ headers["Content-Type"] = "application/json";
539
+ }
540
+ }
541
+ // P0 FIX: Use shared SSRF guard with DNS resolve-then-check (replaces inline regex)
542
+ const ssrfError = await validateUrl(url);
543
+ if (ssrfError) {
544
+ return { success: false, error: `Blocked: ${ssrfError}` };
545
+ }
546
+ try {
547
+ const controller = new AbortController();
548
+ const timer = setTimeout(() => controller.abort(), timeout);
549
+ const resp = await fetch(url, { method, headers, body, signal: controller.signal });
550
+ clearTimeout(timer);
551
+ // Clear secrets from memory as soon as the request is sent
552
+ secretMap.clear();
553
+ const contentType = resp.headers.get("content-type") || "";
554
+ let data;
555
+ if (contentType.includes("json")) {
556
+ data = await resp.json();
557
+ }
558
+ else {
559
+ data = await resp.text();
560
+ }
561
+ if (!resp.ok) {
562
+ return { success: false, error: `HTTP ${resp.status}: ${typeof data === "string" ? data.substring(0, 500) : JSON.stringify(data).substring(0, 500)}` };
563
+ }
564
+ return { success: true, data };
565
+ }
566
+ catch (err) {
567
+ // Clear secrets from memory even on error
568
+ secretMap.clear();
569
+ if (err.name === "AbortError") {
570
+ return { success: false, error: `HTTP request timed out after ${timeout}ms` };
571
+ }
572
+ return { success: false, error: sanitizeError(err) };
573
+ }
574
+ }
575
+ async function executeSQLUserTool(supabase, sqlConfig, args, storeId) {
576
+ if (!sqlConfig.is_read_only) {
577
+ return { success: false, error: "Write SQL tools are not supported in server execution" };
578
+ }
579
+ if (!sqlConfig.template) {
580
+ return { success: false, error: "No SQL template configured" };
581
+ }
582
+ try {
583
+ const { data, error } = await supabase.rpc("execute_safe_sql", {
584
+ p_sql: sqlConfig.template,
585
+ p_params: args,
586
+ p_store_id: storeId,
587
+ p_allowed_tables: sqlConfig.allowed_tables || [],
588
+ });
589
+ if (error)
590
+ return { success: false, error: error.message };
591
+ if (data?.success) {
592
+ return { success: true, data: data.data };
593
+ }
594
+ return { success: false, error: data?.error || "SQL execution failed" };
595
+ }
596
+ catch (err) {
597
+ return { success: false, error: sanitizeError(err) };
598
+ }
599
+ }
600
+ // ============================================================================
601
+ // SUPPLY CHAIN — action aliasing (LLMs omit po_/transfer_ prefix)
602
+ // ============================================================================
603
+ async function executeSupplyChain(supabase, args, storeId, traceId, userId, userEmail, source, conversationId, userToolRows, agentId) {
604
+ let scAction = args.action || "";
605
+ const PO_ALIASES = {
606
+ create: "po_create", list: "po_list", get: "po_get",
607
+ add_items: "po_add_items", approve: "po_approve", mark_ordered: "po_mark_ordered",
608
+ receive: "po_receive", cancel: "po_cancel",
609
+ };
610
+ if (PO_ALIASES[scAction])
611
+ scAction = PO_ALIASES[scAction];
612
+ if (scAction === "find_suppliers") {
613
+ return executeTool(supabase, "suppliers", { ...args, action: "find_suppliers" }, storeId, traceId, userId, userEmail, source, conversationId, userToolRows, agentId);
614
+ }
615
+ if (scAction.startsWith("po_")) {
616
+ return executeTool(supabase, "purchase_orders", { ...args, action: scAction.slice(3) }, storeId, traceId, userId, userEmail, source, conversationId, userToolRows, agentId);
617
+ }
618
+ if (scAction.startsWith("transfer_")) {
619
+ return executeTool(supabase, "transfers", { ...args, action: scAction.slice(9) }, storeId, traceId, userId, userEmail, source, conversationId, userToolRows, agentId);
620
+ }
621
+ return { success: false, error: `Unknown supply_chain action: ${scAction}. Use po_create, po_list, po_get, po_approve, po_receive, po_cancel, transfer_create, transfer_list, transfer_approve, transfer_ship, transfer_receive, transfer_cancel, find_suppliers.` };
622
+ }
623
+ // ============================================================================
624
+ // TOOL EXECUTOR — dispatches via unified handler registry
625
+ // ============================================================================
626
+ export async function executeTool(supabase, toolName, args, storeId, traceId, userId, userEmail, source, conversationId, userToolRows, agentId, onToolProgress,
627
+ /** Skip per-tool audit when called within a conversation — persistAgentTurn handles it */
628
+ skipAudit) {
629
+ const startTime = Date.now();
630
+ const action = args.action;
631
+ let result;
632
+ // Handle supply_chain aliases (LLMs often omit po_/transfer_ prefix)
633
+ if (toolName === "supply_chain") {
634
+ return executeSupplyChain(supabase, args, storeId, traceId, userId, userEmail, source, conversationId, userToolRows, agentId);
635
+ }
636
+ // Look up handler from unified registry
637
+ const entry = TOOL_HANDLERS[toolName];
638
+ // Check store requirement from registry (not hardcoded)
639
+ const requiresStore = entry ? entry.requiresStore : true;
640
+ if (requiresStore && (!storeId || !/^[0-9a-fA-F]{8}-/.test(storeId))) {
641
+ return { success: false, error: `store_id is required for ${toolName}. Ensure a store is selected.` };
642
+ }
643
+ // Permission enforcement — check agent flags before tool execution
644
+ if (agentId) {
645
+ const { data: agentFlags } = await supabase.from("ai_agent_config")
646
+ .select("can_query, can_modify")
647
+ .eq("id", agentId).single();
648
+ if (agentFlags) {
649
+ // Read-only agent check — block ALL tool calls if can_query is false
650
+ if (!agentFlags.can_query) {
651
+ const readActions = ["list", "get", "search", "query", "summary", "stats", "count", "by_location", "in_stock", "out_of_stock", "velocity"];
652
+ const action = args.action;
653
+ if (action && readActions.includes(action)) {
654
+ return { success: false, error: `Agent does not have query permission (can_query=false). Contact an admin.` };
655
+ }
656
+ }
657
+ // Mutation check — already in system prompt but enforce at execution level
658
+ if (!agentFlags.can_modify) {
659
+ const writeActions = ["create", "update", "delete", "upsert", "send", "approve", "cancel", "add", "remove", "set", "adjust", "transfer"];
660
+ const action = args.action;
661
+ if (action && writeActions.some(wa => action.startsWith(wa))) {
662
+ return { success: false, error: `Agent has read-only access (can_modify=false). Cannot perform "${action}".` };
663
+ }
664
+ }
665
+ }
666
+ }
667
+ // User tool approval check — requires_approval enforced at execution level
668
+ if (toolName.startsWith("user_tool__") && userToolRows?.length) {
669
+ const userTool = getUserToolByPrefixedName(userToolRows, toolName);
670
+ if (userTool?.requires_approval) {
671
+ return { success: false, error: `Tool "${userTool.display_name}" requires approval before execution. Use the workflow approval system to request permission.` };
672
+ }
673
+ }
674
+ // Validate mutation args before dispatch — read-only actions pass through
675
+ const validation = validateToolArgs(toolName, args);
676
+ if (!validation.valid) {
677
+ return { success: false, error: validation.error };
678
+ }
679
+ args = validation.data;
680
+ const execStartMs = Date.now();
681
+ try {
682
+ let toolPromise;
683
+ if (entry) {
684
+ // Dispatch via unified handler registry — pass progress callback to handlers that support it
685
+ const progressCb = entry.supportsProgress && onToolProgress
686
+ ? (progress) => onToolProgress(toolName, progress)
687
+ : undefined;
688
+ toolPromise = entry.handler(supabase, args, storeId, progressCb);
689
+ }
690
+ else if (toolName.startsWith("user_tool__") && userToolRows && storeId) {
691
+ // Custom user tools
692
+ const userTool = getUserToolByPrefixedName(userToolRows, toolName);
693
+ if (userTool) {
694
+ toolPromise = executeUserTool(supabase, userTool, args, storeId, agentId, conversationId);
695
+ }
696
+ else {
697
+ toolPromise = Promise.resolve({ success: false, error: `Unknown user tool: ${toolName}` });
698
+ }
699
+ }
700
+ else {
701
+ toolPromise = Promise.resolve({ success: false, error: `Unknown tool: ${toolName}. Use the discover_tools action to see available tools.` });
702
+ }
703
+ const timeoutMs = entry?.timeout || DEFAULT_TIMEOUT;
704
+ // FIX 7: Timeout cascade validation — warn if step timeout < tool timeout
705
+ const stepTimeout = args._step_timeout_seconds;
706
+ if (stepTimeout && stepTimeout * 1000 < timeoutMs) {
707
+ console.warn(`[timeout-cascade] Step timeout (${stepTimeout}s) < tool timeout (${timeoutMs / 1000}s) for ${toolName}. Tool may outlive step.`);
708
+ }
709
+ result = await withTimeout(toolPromise, timeoutMs, toolName);
710
+ }
711
+ catch (err) {
712
+ result = { success: false, error: sanitizeError(err) };
713
+ }
714
+ // Record execution metrics
715
+ recordToolMetric(toolName, Date.now() - execStartMs, result.success);
716
+ // Audit log — enriched with OTEL fields for distributed tracing
717
+ // Skipped when tool is called within a conversation (SSE chat, channel agent, workflow)
718
+ // because persistAgentTurn already logs the full conversation with tool details
719
+ if (skipAudit)
720
+ return result;
721
+ try {
722
+ const endTime = Date.now();
723
+ // Compute payload sizes for observability
724
+ const inputJson = JSON.stringify(args);
725
+ const outputJson = result.data ? JSON.stringify(result.data) : "";
726
+ const inputBytes = inputJson.length;
727
+ const outputBytes = outputJson.length;
728
+ const details = {
729
+ source: source || "fly_container",
730
+ args,
731
+ // Keys that SwiftUI telemetry panel reads for rich display
732
+ tool_input: args,
733
+ input_bytes: inputBytes,
734
+ output_bytes: outputBytes,
735
+ };
736
+ if (result.success && result.data) {
737
+ details.result_summary = summarizeResult(toolName, action, result.data);
738
+ // Store full result for rich telemetry display (capped at 50KB to prevent JSONB bloat)
739
+ if (outputBytes <= 50_000) {
740
+ details.tool_result = result.data;
741
+ }
742
+ else {
743
+ details.tool_result = {
744
+ _truncated: true,
745
+ _type: Array.isArray(result.data) ? "array" : "object",
746
+ _size: outputBytes,
747
+ _count: Array.isArray(result.data) ? result.data.length : Object.keys(result.data).length,
748
+ };
749
+ }
750
+ }
751
+ if (result.error) {
752
+ details.tool_error = result.error;
753
+ }
754
+ const bytes = new Uint8Array(8);
755
+ crypto.getRandomValues(bytes);
756
+ const spanId = Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
757
+ const auditRow = {
758
+ action: `tool.${toolName}${action ? `.${action}` : ""}`,
759
+ severity: result.success ? "info" : "error",
760
+ store_id: storeId || null,
761
+ resource_type: "mcp_tool",
762
+ resource_id: toolName,
763
+ request_id: traceId || null,
764
+ conversation_id: conversationId || null,
765
+ source: source || "fly_container",
766
+ details,
767
+ error_message: result.error || null,
768
+ duration_ms: endTime - startTime,
769
+ user_id: userId || null,
770
+ user_email: userEmail || null,
771
+ // OTEL fields
772
+ trace_id: traceId || null,
773
+ span_id: spanId,
774
+ span_kind: "INTERNAL",
775
+ service_name: "agent-server",
776
+ status_code: result.success ? "OK" : "ERROR",
777
+ start_time: new Date(startTime).toISOString(),
778
+ end_time: new Date(endTime).toISOString(),
779
+ };
780
+ queueAuditLog(supabase, auditRow);
781
+ }
782
+ catch (err) {
783
+ console.error("[audit] exception:", err);
784
+ }
785
+ return result;
786
+ }
787
+ // ============================================================================
788
+ // AGENT LOADER
789
+ // ============================================================================
790
+ export async function loadAgentConfig(supabase, agentId, storeId) {
791
+ let query = supabase
792
+ .from("ai_agent_config")
793
+ .select("*")
794
+ .eq("id", agentId);
795
+ // P0 FIX: Filter by store_id to prevent cross-tenant agent access
796
+ if (storeId) {
797
+ query = query.eq("store_id", storeId);
798
+ }
799
+ const { data, error } = await query.single();
800
+ if (error || !data)
801
+ return null;
802
+ return data;
803
+ }