3 Commits

Author SHA1 Message Date
melod1n 321d185592 bump versions 2026-05-21 17:05:33 +03:00
melod1n a3f19f0413 Fix startup schema migration deadlock 2026-05-19 17:58:09 +03:00
melod1n c613c636e1 Add local tool filtering 2026-05-19 08:33:18 +03:00
8 changed files with 111 additions and 33 deletions
+9
View File
@@ -46,6 +46,15 @@ USE_NAMES_IN_PROMPT=true
# Disable all built-in local tools and keep only MCP tools
DISABLE_LOCAL_TOOLS=false
# Filter built-in local tools by name.
# LOCAL_TOOL_ALLOWLIST lets through only the listed tools.
# LOCAL_TOOL_DENYLIST removes the listed tools.
# Examples:
# LOCAL_TOOL_ALLOWLIST=get_datetime,web_search
# LOCAL_TOOL_DENYLIST=shell_execute,python_interpreter
LOCAL_TOOL_ALLOWLIST=
LOCAL_TOOL_DENYLIST=
# Custom system prompt for AI (or put it into data/SYSTEM_PROMPT.md)
SYSTEM_PROMPT=
+7
View File
@@ -39,6 +39,13 @@ If you want to disable all built-in local tools and use only MCP tools, set:
DISABLE_LOCAL_TOOLS=true
```
If you want a partial filter instead, use tool names:
```bash
LOCAL_TOOL_ALLOWLIST=get_datetime,web_search
LOCAL_TOOL_DENYLIST=shell_execute,python_interpreter
```
For local Ollama document RAG, install an embedding model locally and set it in `.env`:
```bash
+5 -5
View File
@@ -14,8 +14,8 @@
"emoji-regex": "^10.6.0",
"fluent-ffmpeg": "^2.1.3",
"ollama": "^0.6.3",
"openai": "^6.37.0",
"pg": "^8.20.0",
"openai": "^6.38.0",
"pg": "^8.21.0",
"qrcode": "^1.5.4",
"sharp": "^0.34.5",
"systeminformation": "^5.31.6",
@@ -27,12 +27,12 @@
"@eslint/js": "^9.39.4",
"@types/bun": "^1.3.14",
"@types/fluent-ffmpeg": "^2.1.28",
"@types/node": "^25.8.0",
"@types/node": "^25.9.1",
"@types/pg": "^8.20.0",
"@types/qrcode": "^1.5.6",
"eslint": "^9.39.4",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"typescript-eslint": "^8.59.4",
},
},
},
@@ -179,7 +179,7 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@25.9.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ=="],
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
+4 -4
View File
@@ -30,7 +30,7 @@
"@eslint/js": "^9.39.4",
"@types/bun": "^1.3.14",
"@types/fluent-ffmpeg": "^2.1.28",
"@types/node": "^25.9.0",
"@types/node": "^25.9.1",
"@types/pg": "^8.20.0",
"@types/qrcode": "^1.5.6",
"eslint": "^9.39.4",
@@ -1246,9 +1246,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz",
"integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==",
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
+1 -1
View File
@@ -35,7 +35,7 @@
"@eslint/js": "^9.39.4",
"@types/bun": "^1.3.14",
"@types/fluent-ffmpeg": "^2.1.28",
"@types/node": "^25.9.0",
"@types/node": "^25.9.1",
"@types/pg": "^8.20.0",
"@types/qrcode": "^1.5.6",
"eslint": "^9.39.4",
+61 -22
View File
@@ -73,35 +73,62 @@ export const fileTools = [
deletePathTool,
] satisfies AiTool[];
function parseToolNameSet(raw: string | undefined): Set<string> | undefined {
if (!raw?.trim()) return undefined;
const names = raw
.split(",")
.map(item => item.trim().toLowerCase())
.filter(Boolean);
return names.length ? new Set(names) : undefined;
}
function isLocalToolEnabled(toolName: string): boolean {
if (Environment.DISABLE_LOCAL_TOOLS) return false;
const allowlist = parseToolNameSet(Environment.LOCAL_TOOL_ALLOWLIST);
if (allowlist && !allowlist.has(toolName.toLowerCase())) return false;
const denylist = parseToolNameSet(Environment.LOCAL_TOOL_DENYLIST);
if (denylist && denylist.has(toolName.toLowerCase())) return false;
return true;
}
function filterEnabledTools(tools: AiTool[]): AiTool[] {
return tools.filter(tool => isLocalToolEnabled(tool.function.name));
}
export const getTools = (forCreator?: boolean) => {
const tools: AiTool[] = Environment.DISABLE_LOCAL_TOOLS ? [] : [
...defaultTools,
];
const tools: AiTool[] = [];
if (Environment.DISABLE_LOCAL_TOOLS) {
tools.push(...getMcpTools());
return tools;
}
tools.push(...filterEnabledTools(defaultTools));
if (Environment.BRAVE_SEARCH_API_KEY) {
tools.push(webSearchTool);
tools.push(...filterEnabledTools([webSearchTool]));
}
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
tools.push(getWeatherTool);
tools.push(...filterEnabledTools([getWeatherTool]));
}
if (Environment.FILE_TOOLS_ROOT_DIR && Environment.ENABLE_FS_TOOLS) {
tools.push(...fileTools);
tools.push(...filterEnabledTools(fileTools));
}
if (forCreator) {
if (Environment.ENABLE_PYTHON_INTERPRETER) {
tools.push(pythonInterpreterTool);
tools.push(...filterEnabledTools([pythonInterpreterTool]));
}
if (Environment.ENABLE_UNSAFE_EVAL) {
tools.push(shellExecuteTool);
tools.push(...filterEnabledTools([shellExecuteTool]));
}
}
@@ -132,7 +159,7 @@ export const fileToolHandlers = {
};
export const getToolHandlers = () => {
let handlers: Record<string, ToolHandler> = {
const handlers: Record<string, ToolHandler> = {
...getMcpToolHandlers(),
};
@@ -140,21 +167,29 @@ export const getToolHandlers = () => {
return handlers;
}
handlers = {
...handlers,
get_datetime: getCurrentDateTime,
get_financial_market_data: getMarketRates,
if (isLocalToolEnabled("get_datetime")) handlers.get_datetime = getCurrentDateTime;
if (isLocalToolEnabled("get_financial_market_data")) handlers.get_financial_market_data = getMarketRates;
...fileToolHandlers,
if (isLocalToolEnabled("read_file")) handlers.read_file = readFile;
if (isLocalToolEnabled("list_directory")) handlers.list_directory = listDirectory;
if (isLocalToolEnabled("search_files")) handlers.search_files = searchFiles;
if (isLocalToolEnabled("create_file")) handlers.create_file = createFile;
if (isLocalToolEnabled("begin_file_write")) handlers.begin_file_write = beginFileWrite;
if (isLocalToolEnabled("write_file_chunk")) handlers.write_file_chunk = writeFileChunk;
if (isLocalToolEnabled("finish_file_write")) handlers.finish_file_write = finishFileWrite;
if (isLocalToolEnabled("cancel_file_write")) handlers.cancel_file_write = cancelFileWrite;
if (isLocalToolEnabled("send_file_as_attachment")) handlers.send_file_as_attachment = sendFileAsAttachment;
if (isLocalToolEnabled("create_directory")) handlers.create_directory = createDirectory;
if (isLocalToolEnabled("copy_path")) handlers.copy_path = copyPath;
if (isLocalToolEnabled("update_file")) handlers.update_file = updateFile;
if (isLocalToolEnabled("edit_file_patch")) handlers.edit_file_patch = editFilePatch;
if (isLocalToolEnabled("rename_path")) handlers.rename_path = renamePath;
if (isLocalToolEnabled("delete_path")) handlers.delete_path = deletePath;
python_interpreter: runPythonInterpreter,
shell_execute: shellExecute,
web_search: webSearch,
get_weather: getWeather,
};
if (isLocalToolEnabled("python_interpreter")) handlers.python_interpreter = runPythonInterpreter;
if (isLocalToolEnabled("shell_execute")) handlers.shell_execute = shellExecute;
if (isLocalToolEnabled("web_search")) handlers.web_search = webSearch;
if (isLocalToolEnabled("get_weather")) handlers.get_weather = getWeather;
return handlers;
};
@@ -167,6 +202,10 @@ export function getToolPrompts(toolNames: string[]): string[] {
const prompts: string[] = [];
for (const toolName of toolNames) {
if (!isLocalToolEnabled(toolName)) {
continue;
}
if (!prompts.includes(fileToolsToolPrompt) &&
fileTools.map(t => t.function.name).includes(toolName)) {
prompts.push(fileToolsToolPrompt);
+6
View File
@@ -215,6 +215,8 @@ const RuntimeEnvSchema = z.object({
ENABLE_PYTHON_INTERPRETER: optionalBooleanSchema,
DISABLE_LOCAL_TOOLS: optionalBooleanSchema,
LOCAL_TOOL_ALLOWLIST: optionalStringSchema,
LOCAL_TOOL_DENYLIST: optionalStringSchema,
MCP_SERVERS: optionalStringSchema,
OLLAMA_API_KEY: optionalStringSchema,
@@ -311,6 +313,8 @@ export class Environment {
static ENABLE_PYTHON_INTERPRETER: boolean = false;
static DISABLE_LOCAL_TOOLS: boolean = false;
static LOCAL_TOOL_ALLOWLIST?: string;
static LOCAL_TOOL_DENYLIST?: string;
static MCP_SERVERS?: string;
static OLLAMA_API_KEY?: string;
@@ -1847,6 +1851,8 @@ export class Environment {
Environment.ENABLE_PYTHON_INTERPRETER = env.ENABLE_PYTHON_INTERPRETER ?? false;
Environment.DISABLE_LOCAL_TOOLS = env.DISABLE_LOCAL_TOOLS ?? false;
Environment.LOCAL_TOOL_ALLOWLIST = env.LOCAL_TOOL_ALLOWLIST;
Environment.LOCAL_TOOL_DENYLIST = env.LOCAL_TOOL_DENYLIST;
Environment.MCP_SERVERS = env.MCP_SERVERS;
Environment.OLLAMA_API_KEY = env.OLLAMA_API_KEY;
+18 -1
View File
@@ -2055,7 +2055,24 @@ export class DatabaseManager {
}
private static async migrateLegacyNormalizedTables(): Promise<void> {
const messages = await DatabaseManager.getAllMessages();
// Do not call getAllMessages() here: it awaits DatabaseManager.ready, which
// is the promise currently waiting on ensureSchema(). That creates a
// self-deadlock during startup migrations.
const messages = await DatabaseManager.query<MessageDbRow>(`
SELECT
"id",
"chatId",
"replyToMessageId",
"fromId",
"text",
"quoteText",
"date",
"deletedByBotAt",
"attachments",
"pipelineAudit"
FROM "messages"
ORDER BY "chatId", "id"
`);
const attachments = messages.flatMap(message => DatabaseManager.attachmentRowsFromMessageRow(message));
const artifacts = messages.flatMap(message => DatabaseManager.artifactRowsFromMessageRow(message));
const requestAudits = messages.flatMap(message => DatabaseManager.requestAuditRowsFromMessageRow(message));