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.
- package/README.md +95 -0
- package/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +321 -0
- package/dist/cli/app.d.ts +26 -0
- package/dist/cli/app.js +64 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +9 -0
- package/dist/cli/chat/ChatApp.js +267 -0
- package/dist/cli/chat/ChatInput.d.ts +39 -0
- package/dist/cli/chat/ChatInput.js +509 -0
- package/dist/cli/chat/MarkdownText.d.ts +10 -0
- package/dist/cli/chat/MarkdownText.js +20 -0
- package/dist/cli/chat/MessageList.d.ts +37 -0
- package/dist/cli/chat/MessageList.js +80 -0
- package/dist/cli/chat/ModelSelector.d.ts +20 -0
- package/dist/cli/chat/ModelSelector.js +73 -0
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +12 -0
- package/dist/cli/chat/SubagentPanel.d.ts +45 -0
- package/dist/cli/chat/SubagentPanel.js +110 -0
- package/dist/cli/chat/TeamPanel.d.ts +21 -0
- package/dist/cli/chat/TeamPanel.js +42 -0
- package/dist/cli/chat/ToolIndicator.d.ts +25 -0
- package/dist/cli/chat/ToolIndicator.js +436 -0
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
- package/dist/cli/commands/config-cmd.d.ts +10 -0
- package/dist/cli/commands/config-cmd.js +99 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +172 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +278 -0
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +162 -0
- package/dist/cli/login/LoginApp.d.ts +7 -0
- package/dist/cli/login/LoginApp.js +157 -0
- package/dist/cli/print-mode.d.ts +31 -0
- package/dist/cli/print-mode.js +202 -0
- package/dist/cli/serve-mode.d.ts +37 -0
- package/dist/cli/serve-mode.js +636 -0
- package/dist/cli/services/agent-definitions.d.ts +25 -0
- package/dist/cli/services/agent-definitions.js +91 -0
- package/dist/cli/services/agent-events.d.ts +178 -0
- package/dist/cli/services/agent-events.js +175 -0
- package/dist/cli/services/agent-loop.d.ts +90 -0
- package/dist/cli/services/agent-loop.js +762 -0
- package/dist/cli/services/agent-worker-base.d.ts +97 -0
- package/dist/cli/services/agent-worker-base.js +220 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +160 -0
- package/dist/cli/services/background-processes.d.ts +126 -0
- package/dist/cli/services/background-processes.js +318 -0
- package/dist/cli/services/browser-auth.d.ts +24 -0
- package/dist/cli/services/browser-auth.js +180 -0
- package/dist/cli/services/claude-md-loader.d.ts +16 -0
- package/dist/cli/services/claude-md-loader.js +58 -0
- package/dist/cli/services/config-store.d.ts +47 -0
- package/dist/cli/services/config-store.js +79 -0
- package/dist/cli/services/debug-log.d.ts +10 -0
- package/dist/cli/services/debug-log.js +52 -0
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/file-history.d.ts +21 -0
- package/dist/cli/services/file-history.js +83 -0
- package/dist/cli/services/format-server-response.d.ts +16 -0
- package/dist/cli/services/format-server-response.js +440 -0
- package/dist/cli/services/git-context.d.ts +11 -0
- package/dist/cli/services/git-context.js +66 -0
- package/dist/cli/services/hooks.d.ts +85 -0
- package/dist/cli/services/hooks.js +258 -0
- package/dist/cli/services/interactive-tools.d.ts +125 -0
- package/dist/cli/services/interactive-tools.js +260 -0
- package/dist/cli/services/keybinding-manager.d.ts +52 -0
- package/dist/cli/services/keybinding-manager.js +115 -0
- package/dist/cli/services/local-tools.d.ts +22 -0
- package/dist/cli/services/local-tools.js +697 -0
- package/dist/cli/services/lsp-manager.d.ts +18 -0
- package/dist/cli/services/lsp-manager.js +717 -0
- package/dist/cli/services/mcp-client.d.ts +48 -0
- package/dist/cli/services/mcp-client.js +157 -0
- package/dist/cli/services/memory-manager.d.ts +16 -0
- package/dist/cli/services/memory-manager.js +57 -0
- package/dist/cli/services/model-manager.d.ts +18 -0
- package/dist/cli/services/model-manager.js +71 -0
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/permission-modes.d.ts +13 -0
- package/dist/cli/services/permission-modes.js +43 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/ripgrep.d.ts +28 -0
- package/dist/cli/services/ripgrep.js +138 -0
- package/dist/cli/services/sandbox.d.ts +29 -0
- package/dist/cli/services/sandbox.js +97 -0
- package/dist/cli/services/server-tools.d.ts +61 -0
- package/dist/cli/services/server-tools.js +543 -0
- package/dist/cli/services/session-persistence.d.ts +23 -0
- package/dist/cli/services/session-persistence.js +99 -0
- package/dist/cli/services/subagent-worker.d.ts +19 -0
- package/dist/cli/services/subagent-worker.js +41 -0
- package/dist/cli/services/subagent.d.ts +47 -0
- package/dist/cli/services/subagent.js +647 -0
- package/dist/cli/services/system-prompt.d.ts +7 -0
- package/dist/cli/services/system-prompt.js +198 -0
- package/dist/cli/services/team-lead.d.ts +73 -0
- package/dist/cli/services/team-lead.js +512 -0
- package/dist/cli/services/team-state.d.ts +77 -0
- package/dist/cli/services/team-state.js +398 -0
- package/dist/cli/services/teammate.d.ts +31 -0
- package/dist/cli/services/teammate.js +689 -0
- package/dist/cli/services/telemetry.d.ts +61 -0
- package/dist/cli/services/telemetry.js +209 -0
- package/dist/cli/services/tools/agent-tools.d.ts +14 -0
- package/dist/cli/services/tools/agent-tools.js +347 -0
- package/dist/cli/services/tools/file-ops.d.ts +15 -0
- package/dist/cli/services/tools/file-ops.js +487 -0
- package/dist/cli/services/tools/search-tools.d.ts +8 -0
- package/dist/cli/services/tools/search-tools.js +186 -0
- package/dist/cli/services/tools/shell-exec.d.ts +10 -0
- package/dist/cli/services/tools/shell-exec.js +168 -0
- package/dist/cli/services/tools/task-manager.d.ts +28 -0
- package/dist/cli/services/tools/task-manager.js +209 -0
- package/dist/cli/services/tools/web-tools.d.ts +11 -0
- package/dist/cli/services/tools/web-tools.js +395 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +74 -0
- package/dist/cli/shared/Theme.js +127 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +21 -0
- package/dist/cli/shared/markdown.js +756 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +105 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +538 -0
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/node/adapters/base.d.ts +35 -0
- package/dist/node/adapters/base.js +10 -0
- package/dist/node/adapters/discord.d.ts +29 -0
- package/dist/node/adapters/discord.js +299 -0
- package/dist/node/adapters/email.d.ts +23 -0
- package/dist/node/adapters/email.js +218 -0
- package/dist/node/adapters/imessage.d.ts +17 -0
- package/dist/node/adapters/imessage.js +118 -0
- package/dist/node/adapters/slack.d.ts +26 -0
- package/dist/node/adapters/slack.js +259 -0
- package/dist/node/adapters/sms.d.ts +23 -0
- package/dist/node/adapters/sms.js +161 -0
- package/dist/node/adapters/telegram.d.ts +17 -0
- package/dist/node/adapters/telegram.js +101 -0
- package/dist/node/adapters/webchat.d.ts +27 -0
- package/dist/node/adapters/webchat.js +160 -0
- package/dist/node/adapters/whatsapp.d.ts +28 -0
- package/dist/node/adapters/whatsapp.js +230 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +325 -0
- package/dist/node/config.d.ts +17 -0
- package/dist/node/config.js +31 -0
- package/dist/node/runtime.d.ts +50 -0
- package/dist/node/runtime.js +351 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
- package/dist/server/handlers/analytics.d.ts +17 -0
- package/dist/server/handlers/analytics.js +266 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/billing.d.ts +33 -0
- package/dist/server/handlers/billing.js +272 -0
- package/dist/server/handlers/browser.d.ts +10 -0
- package/dist/server/handlers/browser.js +517 -0
- package/dist/server/handlers/catalog.d.ts +99 -0
- package/dist/server/handlers/catalog.js +976 -0
- package/dist/server/handlers/comms.d.ts +254 -0
- package/dist/server/handlers/comms.js +588 -0
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/crm.d.ts +89 -0
- package/dist/server/handlers/crm.js +538 -0
- package/dist/server/handlers/discovery.d.ts +6 -0
- package/dist/server/handlers/discovery.js +288 -0
- package/dist/server/handlers/embeddings.d.ts +92 -0
- package/dist/server/handlers/embeddings.js +197 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/image-gen.d.ts +6 -0
- package/dist/server/handlers/image-gen.js +409 -0
- package/dist/server/handlers/inventory.d.ts +319 -0
- package/dist/server/handlers/inventory.js +447 -0
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.d.ts +6 -0
- package/dist/server/handlers/llm-providers.js +673 -0
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/nodes.d.ts +33 -0
- package/dist/server/handlers/nodes.js +699 -0
- package/dist/server/handlers/operations.d.ts +138 -0
- package/dist/server/handlers/operations.js +131 -0
- package/dist/server/handlers/platform.d.ts +23 -0
- package/dist/server/handlers/platform.js +227 -0
- package/dist/server/handlers/supply-chain.d.ts +19 -0
- package/dist/server/handlers/supply-chain.js +327 -0
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/video-gen.d.ts +6 -0
- package/dist/server/handlers/video-gen.js +466 -0
- package/dist/server/handlers/voice.d.ts +8 -0
- package/dist/server/handlers/voice.js +1146 -0
- package/dist/server/handlers/workflow-steps.d.ts +86 -0
- package/dist/server/handlers/workflow-steps.js +2349 -0
- package/dist/server/handlers/workflows.d.ts +7 -0
- package/dist/server/handlers/workflows.js +989 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2427 -0
- package/dist/server/lib/batch-client.d.ts +80 -0
- package/dist/server/lib/batch-client.js +467 -0
- package/dist/server/lib/code-worker-pool.d.ts +31 -0
- package/dist/server/lib/code-worker-pool.js +224 -0
- package/dist/server/lib/code-worker.d.ts +1 -0
- package/dist/server/lib/code-worker.js +188 -0
- package/dist/server/lib/compaction-service.d.ts +32 -0
- package/dist/server/lib/compaction-service.js +162 -0
- package/dist/server/lib/logger.d.ts +19 -0
- package/dist/server/lib/logger.js +46 -0
- package/dist/server/lib/otel.d.ts +38 -0
- package/dist/server/lib/otel.js +126 -0
- package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
- package/dist/server/lib/pg-rate-limiter.js +86 -0
- package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
- package/dist/server/lib/prompt-sanitizer.js +177 -0
- package/dist/server/lib/provider-capabilities.d.ts +85 -0
- package/dist/server/lib/provider-capabilities.js +190 -0
- package/dist/server/lib/provider-failover.d.ts +74 -0
- package/dist/server/lib/provider-failover.js +210 -0
- package/dist/server/lib/rate-limiter.d.ts +39 -0
- package/dist/server/lib/rate-limiter.js +147 -0
- package/dist/server/lib/server-agent-loop.d.ts +107 -0
- package/dist/server/lib/server-agent-loop.js +667 -0
- package/dist/server/lib/server-subagent.d.ts +78 -0
- package/dist/server/lib/server-subagent.js +203 -0
- package/dist/server/lib/session-checkpoint.d.ts +51 -0
- package/dist/server/lib/session-checkpoint.js +145 -0
- package/dist/server/lib/ssrf-guard.d.ts +13 -0
- package/dist/server/lib/ssrf-guard.js +240 -0
- package/dist/server/lib/supabase-client.d.ts +7 -0
- package/dist/server/lib/supabase-client.js +78 -0
- package/dist/server/lib/template-resolver.d.ts +31 -0
- package/dist/server/lib/template-resolver.js +215 -0
- package/dist/server/lib/utils.d.ts +16 -0
- package/dist/server/lib/utils.js +147 -0
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/providers/anthropic.d.ts +20 -0
- package/dist/server/providers/anthropic.js +199 -0
- package/dist/server/providers/bedrock.d.ts +20 -0
- package/dist/server/providers/bedrock.js +194 -0
- package/dist/server/providers/gemini.d.ts +24 -0
- package/dist/server/providers/gemini.js +486 -0
- package/dist/server/providers/openai.d.ts +24 -0
- package/dist/server/providers/openai.js +522 -0
- package/dist/server/providers/registry.d.ts +32 -0
- package/dist/server/providers/registry.js +58 -0
- package/dist/server/providers/shared.d.ts +32 -0
- package/dist/server/providers/shared.js +124 -0
- package/dist/server/providers/types.d.ts +92 -0
- package/dist/server/providers/types.js +12 -0
- package/dist/server/proxy-handlers.d.ts +6 -0
- package/dist/server/proxy-handlers.js +89 -0
- package/dist/server/tool-router.d.ts +149 -0
- package/dist/server/tool-router.js +803 -0
- package/dist/server/validation.d.ts +24 -0
- package/dist/server/validation.js +301 -0
- package/dist/server/worker.d.ts +19 -0
- package/dist/server/worker.js +201 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +181 -0
- package/dist/shared/agent-core.d.ts +157 -0
- package/dist/shared/agent-core.js +534 -0
- package/dist/shared/anthropic-types.d.ts +105 -0
- package/dist/shared/anthropic-types.js +7 -0
- package/dist/shared/api-client.d.ts +90 -0
- package/dist/shared/api-client.js +379 -0
- package/dist/shared/constants.d.ts +33 -0
- package/dist/shared/constants.js +80 -0
- package/dist/shared/sse-parser.d.ts +26 -0
- package/dist/shared/sse-parser.js +259 -0
- package/dist/shared/tool-dispatch.d.ts +52 -0
- package/dist/shared/tool-dispatch.js +191 -0
- package/dist/shared/types.d.ts +72 -0
- package/dist/shared/types.js +7 -0
- package/dist/updater.d.ts +25 -0
- package/dist/updater.js +140 -0
- package/dist/webchat/widget.d.ts +0 -0
- package/dist/webchat/widget.js +397 -0
- package/package.json +95 -0
- package/src/cli/services/builtin-skills/commit.md +19 -0
- package/src/cli/services/builtin-skills/review-pr.md +21 -0
- 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
|
+
}
|