359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
import { getAgentById, getAgentAccessControl } from "./agents";
|
||
|
||
/**
|
||
* Определение инструмента
|
||
*/
|
||
export interface ToolDefinition {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
category: "browser" | "shell" | "file" | "docker" | "http" | "system" | "custom" | "web" | "data" | "ai";
|
||
icon?: string;
|
||
parameters: Record<string, { type: string; description: string; required?: boolean }>;
|
||
dangerous: boolean;
|
||
/** Optional custom execute function for dynamically registered tools */
|
||
execute?: (params: Record<string, any>) => Promise<any>;
|
||
}
|
||
|
||
/**
|
||
* Реестр доступных инструментов
|
||
*/
|
||
export const TOOL_REGISTRY: ToolDefinition[] = [
|
||
{
|
||
id: "http_get",
|
||
name: "HTTP GET",
|
||
description: "Выполнить GET-запрос к URL",
|
||
category: "http",
|
||
icon: "Globe",
|
||
parameters: {
|
||
url: { type: "string", description: "URL для запроса", required: true },
|
||
headers: { type: "object", description: "Заголовки запроса" },
|
||
},
|
||
dangerous: false,
|
||
},
|
||
{
|
||
id: "http_post",
|
||
name: "HTTP POST",
|
||
description: "Выполнить POST-запрос к URL с телом",
|
||
category: "http",
|
||
icon: "Send",
|
||
parameters: {
|
||
url: { type: "string", description: "URL для запроса", required: true },
|
||
body: { type: "object", description: "Тело запроса" },
|
||
headers: { type: "object", description: "Заголовки запроса" },
|
||
},
|
||
dangerous: false,
|
||
},
|
||
{
|
||
id: "shell_exec",
|
||
name: "Shell Execute",
|
||
description: "Выполнить bash-команду в изолированной среде",
|
||
category: "shell",
|
||
icon: "Terminal",
|
||
parameters: {
|
||
command: { type: "string", description: "Команда для выполнения", required: true },
|
||
timeout: { type: "number", description: "Таймаут в секундах (по умолчанию 30)" },
|
||
},
|
||
dangerous: true,
|
||
},
|
||
{
|
||
id: "file_read",
|
||
name: "File Read",
|
||
description: "Прочитать содержимое файла",
|
||
category: "file",
|
||
icon: "FileText",
|
||
parameters: {
|
||
path: { type: "string", description: "Путь к файлу", required: true },
|
||
},
|
||
dangerous: false,
|
||
},
|
||
{
|
||
id: "file_write",
|
||
name: "File Write",
|
||
description: "Записать содержимое в файл",
|
||
category: "file",
|
||
icon: "FilePlus",
|
||
parameters: {
|
||
path: { type: "string", description: "Путь к файлу", required: true },
|
||
content: { type: "string", description: "Содержимое файла", required: true },
|
||
},
|
||
dangerous: true,
|
||
},
|
||
{
|
||
id: "docker_list",
|
||
name: "Docker List",
|
||
description: "Получить список контейнеров Docker",
|
||
category: "docker",
|
||
icon: "Box",
|
||
parameters: {
|
||
all: { type: "boolean", description: "Показать все контейнеры (включая остановленные)" },
|
||
},
|
||
dangerous: false,
|
||
},
|
||
{
|
||
id: "docker_exec",
|
||
name: "Docker Exec",
|
||
description: "Выполнить команду в контейнере Docker",
|
||
category: "docker",
|
||
icon: "Play",
|
||
parameters: {
|
||
container: { type: "string", description: "ID или имя контейнера", required: true },
|
||
command: { type: "string", description: "Команда для выполнения", required: true },
|
||
},
|
||
dangerous: true,
|
||
},
|
||
{
|
||
id: "docker_logs",
|
||
name: "Docker Logs",
|
||
description: "Получить логи контейнера",
|
||
category: "docker",
|
||
icon: "FileText",
|
||
parameters: {
|
||
container: { type: "string", description: "ID или имя контейнера", required: true },
|
||
tail: { type: "number", description: "Количество последних строк (по умолчанию 100)" },
|
||
},
|
||
dangerous: false,
|
||
},
|
||
{
|
||
id: "browser_navigate",
|
||
name: "Browser Navigate",
|
||
description: "Открыть URL в браузере и получить содержимое",
|
||
category: "browser",
|
||
icon: "Globe",
|
||
parameters: {
|
||
url: { type: "string", description: "URL для открытия", required: true },
|
||
},
|
||
dangerous: false,
|
||
},
|
||
{
|
||
id: "browser_screenshot",
|
||
name: "Browser Screenshot",
|
||
description: "Сделать скриншот текущей страницы",
|
||
category: "browser",
|
||
icon: "Camera",
|
||
parameters: {},
|
||
dangerous: false,
|
||
},
|
||
];
|
||
|
||
/**
|
||
* Получить все доступные инструменты
|
||
*/
|
||
export function getAllTools(): ToolDefinition[] {
|
||
return TOOL_REGISTRY;
|
||
}
|
||
|
||
/**
|
||
* Получить инструмент по ID
|
||
*/
|
||
export function getToolById(id: string): ToolDefinition | undefined {
|
||
return TOOL_REGISTRY.find((t) => t.id === id);
|
||
}
|
||
|
||
/**
|
||
* Выполнить инструмент от имени агента
|
||
*/
|
||
export async function executeTool(
|
||
agentId: number,
|
||
toolId: string,
|
||
params: Record<string, any>
|
||
): Promise<{ success: boolean; result?: any; error?: string; executionTimeMs: number }> {
|
||
const startTime = Date.now();
|
||
|
||
// Проверяем существование агента
|
||
const agent = await getAgentById(agentId);
|
||
if (!agent) {
|
||
return { success: false, error: "Agent not found", executionTimeMs: 0 };
|
||
}
|
||
|
||
// Проверяем доступность инструмента
|
||
const tool = getToolById(toolId);
|
||
if (!tool) {
|
||
return { success: false, error: `Unknown tool: ${toolId}`, executionTimeMs: 0 };
|
||
}
|
||
|
||
// Проверяем разрешения агента
|
||
const accessControls = await getAgentAccessControl(agentId);
|
||
const toolAccess = accessControls.find((ac) => ac.tool === toolId);
|
||
|
||
if (toolAccess && !toolAccess.isAllowed) {
|
||
return {
|
||
success: false,
|
||
error: `Tool '${toolId}' is blocked for this agent`,
|
||
executionTimeMs: Date.now() - startTime,
|
||
};
|
||
}
|
||
|
||
// Проверяем, есть ли инструмент в allowedTools агента
|
||
const allowedTools = (agent.allowedTools as string[]) || [];
|
||
if (allowedTools.length > 0 && !allowedTools.includes(toolId)) {
|
||
return {
|
||
success: false,
|
||
error: `Tool '${toolId}' is not in agent's allowed tools list`,
|
||
executionTimeMs: Date.now() - startTime,
|
||
};
|
||
}
|
||
|
||
try {
|
||
const result = await executeToolImpl(toolId, params, toolAccess);
|
||
return {
|
||
success: true,
|
||
result,
|
||
executionTimeMs: Date.now() - startTime,
|
||
};
|
||
} catch (err: any) {
|
||
return {
|
||
success: false,
|
||
error: err.message,
|
||
executionTimeMs: Date.now() - startTime,
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Реализация выполнения инструментов
|
||
*/
|
||
async function executeToolImpl(
|
||
toolId: string,
|
||
params: Record<string, any>,
|
||
_accessControl?: any
|
||
): Promise<any> {
|
||
switch (toolId) {
|
||
case "http_get": {
|
||
const response = await fetch(params.url, {
|
||
headers: params.headers || {},
|
||
signal: AbortSignal.timeout(30000),
|
||
});
|
||
const text = await response.text();
|
||
return {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
body: text.slice(0, 10000), // Limit response size
|
||
headers: Object.fromEntries(response.headers.entries()),
|
||
};
|
||
}
|
||
|
||
case "http_post": {
|
||
const response = await fetch(params.url, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
...(params.headers || {}),
|
||
},
|
||
body: JSON.stringify(params.body || {}),
|
||
signal: AbortSignal.timeout(30000),
|
||
});
|
||
const text = await response.text();
|
||
return {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
body: text.slice(0, 10000),
|
||
headers: Object.fromEntries(response.headers.entries()),
|
||
};
|
||
}
|
||
|
||
case "shell_exec": {
|
||
const { exec } = await import("child_process");
|
||
const { promisify } = await import("util");
|
||
const execAsync = promisify(exec);
|
||
const timeout = (params.timeout || 30) * 1000;
|
||
|
||
// Safety: block dangerous commands
|
||
const blockedPatterns = ["rm -rf /", "mkfs", "dd if=", ":(){ :|:& };:"];
|
||
for (const pattern of blockedPatterns) {
|
||
if (params.command.includes(pattern)) {
|
||
throw new Error(`Command blocked for safety: contains '${pattern}'`);
|
||
}
|
||
}
|
||
|
||
const { stdout, stderr } = await execAsync(params.command, { timeout });
|
||
return { stdout: stdout.slice(0, 10000), stderr: stderr.slice(0, 2000) };
|
||
}
|
||
|
||
case "file_read": {
|
||
const { readFile } = await import("fs/promises");
|
||
const content = await readFile(params.path, "utf-8");
|
||
return { content: content.slice(0, 50000), size: content.length };
|
||
}
|
||
|
||
case "file_write": {
|
||
const { writeFile, mkdir } = await import("fs/promises");
|
||
const { dirname } = await import("path");
|
||
await mkdir(dirname(params.path), { recursive: true });
|
||
await writeFile(params.path, params.content, "utf-8");
|
||
return { written: params.content.length, path: params.path };
|
||
}
|
||
|
||
case "docker_list": {
|
||
const { exec } = await import("child_process");
|
||
const { promisify } = await import("util");
|
||
const execAsync = promisify(exec);
|
||
const flag = params.all ? "-a" : "";
|
||
const { stdout } = await execAsync(`docker ps ${flag} --format json`);
|
||
const containers = stdout
|
||
.trim()
|
||
.split("\n")
|
||
.filter(Boolean)
|
||
.map((line) => {
|
||
try {
|
||
return JSON.parse(line);
|
||
} catch {
|
||
return line;
|
||
}
|
||
});
|
||
return { containers };
|
||
}
|
||
|
||
case "docker_exec": {
|
||
const { exec } = await import("child_process");
|
||
const { promisify } = await import("util");
|
||
const execAsync = promisify(exec);
|
||
const { stdout, stderr } = await execAsync(
|
||
`docker exec ${params.container} ${params.command}`,
|
||
{ timeout: 30000 }
|
||
);
|
||
return { stdout: stdout.slice(0, 10000), stderr: stderr.slice(0, 2000) };
|
||
}
|
||
|
||
case "docker_logs": {
|
||
const { exec } = await import("child_process");
|
||
const { promisify } = await import("util");
|
||
const execAsync = promisify(exec);
|
||
const tail = params.tail || 100;
|
||
const { stdout, stderr } = await execAsync(
|
||
`docker logs --tail ${tail} ${params.container}`,
|
||
{ timeout: 15000 }
|
||
);
|
||
return { logs: (stdout + stderr).slice(0, 20000) };
|
||
}
|
||
|
||
case "browser_navigate": {
|
||
// Simple HTTP fetch as browser substitute (no JS rendering)
|
||
const response = await fetch(params.url, {
|
||
headers: {
|
||
"User-Agent":
|
||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||
},
|
||
signal: AbortSignal.timeout(30000),
|
||
});
|
||
const html = await response.text();
|
||
// Strip HTML tags for readable output
|
||
const text = html
|
||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
||
.replace(/<[^>]+>/g, " ")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.slice(0, 10000);
|
||
return { url: params.url, status: response.status, text };
|
||
}
|
||
|
||
case "browser_screenshot": {
|
||
return { error: "Screenshot requires headless browser (not available in this environment)" };
|
||
}
|
||
|
||
default:
|
||
throw new Error(`Tool '${toolId}' not implemented`);
|
||
}
|
||
}
|