diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 99cfff5..04c9ce5 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -106,11 +106,15 @@ const char CA_ENABLE_CHAT_TOOLS[] = "QodeAssist.caEnableChatTools"; const char CA_USE_TOOLS[] = "QodeAssist.caUseTools"; const char CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead"; const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite"; +const char CA_ALLOW_NETWORK_ACCESS[] = "QodeAssist.caAllowNetworkAccess"; const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject"; const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool"; const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectTool"; const char CA_ENABLE_TERMINAL_COMMAND_TOOL[] = "QodeAssist.caEnableTerminalCommandTool"; const char CA_ALLOWED_TERMINAL_COMMANDS[] = "QodeAssist.caAllowedTerminalCommands"; +const char CA_ALLOWED_TERMINAL_COMMANDS_LINUX[] = "QodeAssist.caAllowedTerminalCommandsLinux"; +const char CA_ALLOWED_TERMINAL_COMMANDS_MACOS[] = "QodeAssist.caAllowedTerminalCommandsMacOS"; +const char CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS[] = "QodeAssist.caAllowedTerminalCommandsWindows"; const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions"; const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId"; diff --git a/settings/ToolsSettings.cpp b/settings/ToolsSettings.cpp index 032c441..bb7dd13 100644 --- a/settings/ToolsSettings.cpp +++ b/settings/ToolsSettings.cpp @@ -54,6 +54,13 @@ ToolsSettings::ToolsSettings() Tr::tr("Allow tools to write and modify files on disk (WARNING: Use with caution!)")); allowFileSystemWrite.setDefaultValue(false); + allowNetworkAccess.setSettingsKey(Constants::CA_ALLOW_NETWORK_ACCESS); + allowNetworkAccess.setLabelText(Tr::tr("Allow Network Access for tools")); + allowNetworkAccess.setToolTip( + Tr::tr("Allow tools to make network requests (e.g., execute commands like git, curl, wget). " + "Required for ExecuteTerminalCommandTool with network-capable commands.")); + allowNetworkAccess.setDefaultValue(false); + allowAccessOutsideProject.setSettingsKey(Constants::CA_ALLOW_ACCESS_OUTSIDE_PROJECT); allowAccessOutsideProject.setLabelText(Tr::tr("Allow file access outside project")); allowAccessOutsideProject.setToolTip( @@ -90,13 +97,29 @@ ToolsSettings::ToolsSettings() "unexpected behavior.")); enableTerminalCommandTool.setDefaultValue(false); - allowedTerminalCommands.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS); - allowedTerminalCommands.setLabelText(Tr::tr("Allowed Terminal Commands")); - allowedTerminalCommands.setToolTip( - Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute. " - "Example: git, ls, cat, grep, cmake")); - allowedTerminalCommands.setDisplayStyle(Utils::StringAspect::LineEditDisplay); - allowedTerminalCommands.setDefaultValue("git, ls, cat, grep"); + allowedTerminalCommandsLinux.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_LINUX); + allowedTerminalCommandsLinux.setLabelText(Tr::tr("Allowed Commands (Linux)")); + allowedTerminalCommandsLinux.setToolTip( + Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Linux. " + "Example: git, ls, cat, grep, find, cmake")); + allowedTerminalCommandsLinux.setDisplayStyle(Utils::StringAspect::LineEditDisplay); + allowedTerminalCommandsLinux.setDefaultValue("git, ls, cat, grep, find"); + + allowedTerminalCommandsMacOS.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_MACOS); + allowedTerminalCommandsMacOS.setLabelText(Tr::tr("Allowed Commands (macOS)")); + allowedTerminalCommandsMacOS.setToolTip( + Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on macOS. " + "Example: git, ls, cat, grep, find, cmake")); + allowedTerminalCommandsMacOS.setDisplayStyle(Utils::StringAspect::LineEditDisplay); + allowedTerminalCommandsMacOS.setDefaultValue("git, ls, cat, grep, find"); + + allowedTerminalCommandsWindows.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS); + allowedTerminalCommandsWindows.setLabelText(Tr::tr("Allowed Commands (Windows)")); + allowedTerminalCommandsWindows.setToolTip( + Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Windows. " + "Example: git, dir, type, findstr, where, cmake")); + allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay); + allowedTerminalCommandsWindows.setDefaultValue("git, dir, type, findstr, where"); resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults"); @@ -107,6 +130,16 @@ ToolsSettings::ToolsSettings() setLayouter([this]() { using namespace Layouting; +#ifdef Q_OS_LINUX + auto ¤tOsCommands = allowedTerminalCommandsLinux; +#elif defined(Q_OS_MACOS) + auto ¤tOsCommands = allowedTerminalCommandsMacOS; +#elif defined(Q_OS_WIN) + auto ¤tOsCommands = allowedTerminalCommandsWindows; +#else + auto ¤tOsCommands = allowedTerminalCommandsLinux; // fallback +#endif + return Column{ Row{Stretch{1}, resetToDefaults}, Space{8}, @@ -115,6 +148,7 @@ ToolsSettings::ToolsSettings() Column{ allowFileSystemRead, allowFileSystemWrite, + allowNetworkAccess, allowAccessOutsideProject }}, Space{8}, @@ -124,7 +158,7 @@ ToolsSettings::ToolsSettings() enableEditFileTool, enableBuildProjectTool, enableTerminalCommandTool, - allowedTerminalCommands, + currentOsCommands, autoApplyFileEdits}}, Stretch{1}}; }); @@ -151,12 +185,15 @@ void ToolsSettings::resetSettingsToDefaults() if (reply == QMessageBox::Yes) { resetAspect(allowFileSystemRead); resetAspect(allowFileSystemWrite); + resetAspect(allowNetworkAccess); resetAspect(allowAccessOutsideProject); resetAspect(autoApplyFileEdits); resetAspect(enableEditFileTool); resetAspect(enableBuildProjectTool); resetAspect(enableTerminalCommandTool); - resetAspect(allowedTerminalCommands); + resetAspect(allowedTerminalCommandsLinux); + resetAspect(allowedTerminalCommandsMacOS); + resetAspect(allowedTerminalCommandsWindows); writeSettings(); } } diff --git a/settings/ToolsSettings.hpp b/settings/ToolsSettings.hpp index 6910822..6a3b907 100644 --- a/settings/ToolsSettings.hpp +++ b/settings/ToolsSettings.hpp @@ -34,13 +34,16 @@ public: Utils::BoolAspect allowFileSystemRead{this}; Utils::BoolAspect allowFileSystemWrite{this}; + Utils::BoolAspect allowNetworkAccess{this}; Utils::BoolAspect allowAccessOutsideProject{this}; // Experimental features Utils::BoolAspect enableEditFileTool{this}; Utils::BoolAspect enableBuildProjectTool{this}; Utils::BoolAspect enableTerminalCommandTool{this}; - Utils::StringAspect allowedTerminalCommands{this}; + Utils::StringAspect allowedTerminalCommandsLinux{this}; + Utils::StringAspect allowedTerminalCommandsMacOS{this}; + Utils::StringAspect allowedTerminalCommandsWindows{this}; Utils::BoolAspect autoApplyFileEdits{this}; private: diff --git a/tools/BuildProjectTool.cpp b/tools/BuildProjectTool.cpp index aec6164..bf6e43e 100644 --- a/tools/BuildProjectTool.cpp +++ b/tools/BuildProjectTool.cpp @@ -112,7 +112,8 @@ QJsonObject BuildProjectTool::getDefinition(LLMCore::ToolSchemaFormat format) co LLMCore::ToolPermissions BuildProjectTool::requiredPermissions() const { - return LLMCore::ToolPermission::None; + return LLMCore::ToolPermission::FileSystemRead + | LLMCore::ToolPermission::FileSystemWrite; } QFuture BuildProjectTool::executeAsync(const QJsonObject &input) diff --git a/tools/ExecuteTerminalCommandTool.cpp b/tools/ExecuteTerminalCommandTool.cpp index 8bf9215..0f01c59 100644 --- a/tools/ExecuteTerminalCommandTool.cpp +++ b/tools/ExecuteTerminalCommandTool.cpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace QodeAssist::Tools { @@ -50,16 +51,7 @@ QString ExecuteTerminalCommandTool::stringName() const QString ExecuteTerminalCommandTool::description() const { - const QStringList allowed = getAllowedCommands(); - const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", "); - - return QString( - "Execute a terminal command in the project directory. " - "Only commands from the allowed list can be executed. " - "Currently allowed commands: %1. " - "The command will be executed in the root directory of the active project. " - "Returns the command output (stdout and stderr) or an error message if the command fails.") - .arg(allowedList); + return getCommandDescription(); } QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat format) const @@ -67,19 +59,18 @@ QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat QJsonObject definition; definition["type"] = "object"; - const QStringList allowed = getAllowedCommands(); - const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", "); + const QString commandDesc = getCommandDescription(); QJsonObject properties; properties["command"] = QJsonObject{ {"type", "string"}, - {"description", - QString("The terminal command to execute. Must be one of the allowed commands: %1") - .arg(allowedList)}}; + {"description", commandDesc}}; properties["args"] = QJsonObject{ {"type", "string"}, - {"description", "Optional arguments for the command (default: empty)"}}; + {"description", + "Optional arguments for the command. Arguments with spaces should be properly quoted. " + "Example: '--file \"path with spaces.txt\" --verbose'"}}; definition["properties"] = properties; definition["required"] = QJsonArray{"command"}; @@ -100,7 +91,9 @@ QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat LLMCore::ToolPermissions ExecuteTerminalCommandTool::requiredPermissions() const { - return LLMCore::ToolPermission::None; + return LLMCore::ToolPermission::FileSystemRead + | LLMCore::ToolPermission::FileSystemWrite + | LLMCore::ToolPermission::NetworkAccess; } QFuture ExecuteTerminalCommandTool::executeAsync(const QJsonObject &input) @@ -113,6 +106,22 @@ QFuture ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp return QtFuture::makeReadyFuture(QString("Error: Command parameter is required.")); } + if (command.length() > MAX_COMMAND_LENGTH) { + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command too long (%1 chars)") + .arg(command.length())); + return QtFuture::makeReadyFuture( + QString("Error: Command exceeds maximum length of %1 characters.") + .arg(MAX_COMMAND_LENGTH)); + } + + if (args.length() > MAX_ARGS_LENGTH) { + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Arguments too long (%1 chars)") + .arg(args.length())); + return QtFuture::makeReadyFuture( + QString("Error: Arguments exceed maximum length of %1 characters.") + .arg(MAX_ARGS_LENGTH)); + } + if (!isCommandAllowed(command)) { LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' is not allowed") .arg(command)); @@ -124,6 +133,23 @@ QFuture ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp .arg(allowedList)); } + if (!isCommandSafe(command)) { + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' contains unsafe characters") + .arg(command)); + return QtFuture::makeReadyFuture( + QString("Error: Command '%1' contains potentially dangerous characters. " + "Only alphanumeric characters, hyphens, underscores, and dots are allowed.") + .arg(command)); + } + + if (!args.isEmpty() && !areArgumentsSafe(args)) { + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Arguments contain unsafe patterns: '%1'") + .arg(args)); + return QtFuture::makeReadyFuture( + QString("Error: Arguments contain potentially dangerous patterns (command chaining, " + "redirection, or pipe operators).")); + } + auto *project = ProjectExplorer::ProjectManager::startupProject(); QString workingDir; @@ -136,9 +162,20 @@ QFuture ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp workingDir = QDir::currentPath(); } - LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing command '%1' with args '%2'") + QDir dir(workingDir); + if (!dir.exists() || !dir.isReadable()) { + LOG_MESSAGE( + QString("ExecuteTerminalCommandTool: Working directory '%1' is not accessible") + .arg(workingDir)); + return QtFuture::makeReadyFuture( + QString("Error: Working directory '%1' does not exist or is not accessible.") + .arg(workingDir)); + } + + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing command '%1' with args '%2' in '%3'") .arg(command) - .arg(args)); + .arg(args.isEmpty() ? "(no args)" : args) + .arg(workingDir)); auto promise = QSharedPointer>::create(); QFuture future = promise->future(); @@ -147,40 +184,85 @@ QFuture ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp QProcess *process = new QProcess(); process->setWorkingDirectory(workingDir); process->setProcessChannelMode(QProcess::MergedChannels); + + process->setReadChannel(QProcess::StandardOutput); + + QTimer *timeoutTimer = new QTimer(); + timeoutTimer->setSingleShot(true); + timeoutTimer->setInterval(COMMAND_TIMEOUT_MS); + + auto outputSize = QSharedPointer::create(0); + + QObject::connect(timeoutTimer, &QTimer::timeout, [process, promise, command, args, timeoutTimer]() { + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1 %2' timed out after %3ms") + .arg(command) + .arg(args) + .arg(COMMAND_TIMEOUT_MS)); + + process->terminate(); + + QTimer::singleShot(1000, process, [process]() { + if (process->state() == QProcess::Running) { + LOG_MESSAGE("ExecuteTerminalCommandTool: Forcefully killing process after timeout"); + process->kill(); + } + }); + + promise->addResult(QString("Error: Command '%1 %2' timed out after %3 seconds. " + "The process has been terminated.") + .arg(command) + .arg(args.isEmpty() ? "" : args) + .arg(COMMAND_TIMEOUT_MS / 1000)); + promise->finish(); + process->deleteLater(); + timeoutTimer->deleteLater(); + }); QObject::connect( process, QOverload::of(&QProcess::finished), - [process, promise, command, args](int exitCode, QProcess::ExitStatus exitStatus) { - const QString output = QString::fromUtf8(process->readAll()); + [this, process, promise, command, args, timeoutTimer, outputSize]( + int exitCode, QProcess::ExitStatus exitStatus) { + timeoutTimer->stop(); + timeoutTimer->deleteLater(); + + const QByteArray rawOutput = process->readAll(); + *outputSize += rawOutput.size(); + const QString output = sanitizeOutput(QString::fromUtf8(rawOutput), *outputSize); + + const QString fullCommand = args.isEmpty() ? command : QString("%1 %2").arg(command).arg(args); if (exitStatus == QProcess::NormalExit) { if (exitCode == 0) { - LOG_MESSAGE( - QString("ExecuteTerminalCommandTool: Command completed successfully")); + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' completed " + "successfully (output size: %2 bytes)") + .arg(fullCommand) + .arg(*outputSize)); promise->addResult( - QString("Command '%1 %2' executed successfully.\n\nOutput:\n%3") - .arg(command) - .arg(args) + QString("Command '%1' executed successfully.\n\nOutput:\n%2") + .arg(fullCommand) .arg(output.isEmpty() ? "(no output)" : output)); } else { - LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command failed with exit " - "code %1") - .arg(exitCode)); + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' failed with " + "exit code %2 (output size: %3 bytes)") + .arg(fullCommand) + .arg(exitCode) + .arg(*outputSize)); promise->addResult( - QString("Command '%1 %2' failed with exit code %3.\n\nOutput:\n%4") - .arg(command) - .arg(args) + QString("Command '%1' failed with exit code %2.\n\nOutput:\n%3") + .arg(fullCommand) .arg(exitCode) .arg(output.isEmpty() ? "(no output)" : output)); } } else { - LOG_MESSAGE("ExecuteTerminalCommandTool: Command crashed or was terminated"); + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' crashed or was " + "terminated (output size: %2 bytes)") + .arg(fullCommand) + .arg(*outputSize)); const QString error = process->errorString(); promise->addResult( - QString("Command '%1 %2' crashed or was terminated.\n\nError: %3\n\nOutput:\n%4") - .arg(command) - .arg(args) + QString("Command '%1' crashed or was terminated.\n\nError: %2\n\nOutput:\n%3") + .arg(fullCommand) .arg(error) .arg(output.isEmpty() ? "(no output)" : output)); } @@ -189,25 +271,92 @@ QFuture ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp process->deleteLater(); }); - QObject::connect(process, &QProcess::errorOccurred, [process, promise, command, args]( + QObject::connect(process, &QProcess::errorOccurred, [process, promise, command, args, timeoutTimer]( QProcess::ProcessError error) { - LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process error occurred: %1").arg(error)); - const QString errorString = process->errorString(); - promise->addResult(QString("Error executing command '%1 %2': %3") - .arg(command) - .arg(args) - .arg(errorString)); + if (promise->future().isFinished()) { + return; + } + + timeoutTimer->stop(); + timeoutTimer->deleteLater(); + + const QString fullCommand = args.isEmpty() ? command : QString("%1 %2").arg(command).arg(args); + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process error occurred for '%1': %2 (%3)") + .arg(fullCommand) + .arg(error) + .arg(process->errorString())); + + QString errorMessage; + switch (error) { + case QProcess::FailedToStart: + errorMessage = QString("Failed to start command '%1'. The command may not exist or " + "you may not have permission to execute it.") + .arg(fullCommand); + break; + case QProcess::Crashed: + errorMessage = QString("Command '%1' crashed during execution.").arg(fullCommand); + break; + case QProcess::Timedout: + errorMessage = QString("Command '%1' timed out.").arg(fullCommand); + break; + case QProcess::WriteError: + errorMessage = QString("Write error occurred while executing '%1'.").arg(fullCommand); + break; + case QProcess::ReadError: + errorMessage = QString("Read error occurred while executing '%1'.").arg(fullCommand); + break; + default: + errorMessage = QString("Unknown error occurred while executing '%1': %2") + .arg(fullCommand) + .arg(process->errorString()); + break; + } + + promise->addResult(QString("Error: %1").arg(errorMessage)); promise->finish(); process->deleteLater(); }); - if (args.isEmpty()) { - process->start(command, QStringList()); - } else { - QStringList argList = args.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); - process->start(command, argList); + QString fullCommand = command; + if (!args.isEmpty()) { + fullCommand += " " + args; } + QStringList splitCommand = QProcess::splitCommand(fullCommand); + if (splitCommand.isEmpty()) { + LOG_MESSAGE("ExecuteTerminalCommandTool: Failed to parse command"); + promise->addResult(QString("Error: Failed to parse command '%1'").arg(fullCommand)); + promise->finish(); + process->deleteLater(); + timeoutTimer->deleteLater(); + return future; + } + + const QString program = splitCommand.takeFirst(); + process->start(program, splitCommand); + + if (!process->waitForStarted(PROCESS_START_TIMEOUT_MS)) { + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Failed to start command '%1' within %2ms") + .arg(fullCommand) + .arg(PROCESS_START_TIMEOUT_MS)); + const QString errorString = process->errorString(); + promise->addResult(QString("Error: Failed to start command '%1': %2\n\n" + "Possible reasons:\n" + "- Command not found in PATH\n" + "- Insufficient permissions\n" + "- Invalid command syntax") + .arg(fullCommand) + .arg(errorString)); + promise->finish(); + process->deleteLater(); + timeoutTimer->deleteLater(); + return future; + } + + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process started successfully (PID: %1)") + .arg(process->processId())); + + timeoutTimer->start(); return future; } @@ -217,24 +366,127 @@ bool ExecuteTerminalCommandTool::isCommandAllowed(const QString &command) const return allowed.contains(command, Qt::CaseInsensitive); } +bool ExecuteTerminalCommandTool::isCommandSafe(const QString &command) const +{ + static const QRegularExpression safePattern("^[a-zA-Z0-9._/-]+$"); + return safePattern.match(command).hasMatch(); +} + +bool ExecuteTerminalCommandTool::areArgumentsSafe(const QString &args) const +{ + if (args.isEmpty()) { + return true; + } + + static const QStringList dangerousPatterns = { + ";", // Command separator + "&&", // AND operator + "||", // OR operator + "|", // Pipe operator + ">", // Output redirection + ">>", // Append redirection + "<", // Input redirection + "`", // Command substitution + "$(", // Command substitution + "$()", // Command substitution + "\\n", // Newline (could start new command) + "\\r" // Carriage return + }; + + for (const QString &pattern : dangerousPatterns) { + if (args.contains(pattern)) { + LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Dangerous pattern '%1' found in args") + .arg(pattern)); + return false; + } + } + + return true; +} + +QString ExecuteTerminalCommandTool::sanitizeOutput(const QString &output, qint64 totalSize) const +{ + if (totalSize > MAX_OUTPUT_SIZE) { + const QString truncated = output.left(MAX_OUTPUT_SIZE / 2); + return QString("%1\n\n... [Output truncated: exceeded maximum size of %2 MB. " + "Total output size was %3 bytes] ...") + .arg(truncated) + .arg(MAX_OUTPUT_SIZE / (1024 * 1024)) + .arg(totalSize); + } + + return output; +} + QStringList ExecuteTerminalCommandTool::getAllowedCommands() const { - const QString commandsStr - = Settings::toolsSettings().allowedTerminalCommands().trimmed(); + static QString cachedCommandsStr; + static QStringList cachedCommands; + + QString commandsStr; + + // Get commands for current OS +#ifdef Q_OS_LINUX + commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed(); +#elif defined(Q_OS_MACOS) + commandsStr = Settings::toolsSettings().allowedTerminalCommandsMacOS().trimmed(); +#elif defined(Q_OS_WIN) + commandsStr = Settings::toolsSettings().allowedTerminalCommandsWindows().trimmed(); +#else + commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed(); // fallback +#endif + + // Return cached result if settings haven't changed + if (commandsStr == cachedCommandsStr && !cachedCommands.isEmpty()) { + return cachedCommands; + } + + // Update cache + cachedCommandsStr = commandsStr; + cachedCommands.clear(); if (commandsStr.isEmpty()) { return QStringList(); } const QStringList rawCommands = commandsStr.split(',', Qt::SkipEmptyParts); - QStringList commands; - commands.reserve(rawCommands.size()); + cachedCommands.reserve(rawCommands.size()); for (const QString &cmd : rawCommands) { - commands.append(cmd.trimmed()); + const QString trimmed = cmd.trimmed(); + if (!trimmed.isEmpty()) { + cachedCommands.append(trimmed); + } } - return commands; + return cachedCommands; +} + +QString ExecuteTerminalCommandTool::getCommandDescription() const +{ + const QStringList allowed = getAllowedCommands(); + const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", "); + +#ifdef Q_OS_LINUX + const QString osInfo = " Running on Linux."; +#elif defined(Q_OS_MACOS) + const QString osInfo = " Running on macOS."; +#elif defined(Q_OS_WIN) + const QString osInfo = " Running on Windows."; +#else + const QString osInfo = ""; +#endif + + return QString( + "Execute a terminal command in the project directory. " + "Only commands from the allowed list can be executed. " + "Currently allowed commands for this OS: %1. " + "The command will be executed in the root directory of the active project. " + "Commands have a %2 second timeout. " + "Returns the command output (stdout and stderr) or an error message if the command fails.%3") + .arg(allowedList) + .arg(COMMAND_TIMEOUT_MS / 1000) + .arg(osInfo); } } // namespace QodeAssist::Tools diff --git a/tools/ExecuteTerminalCommandTool.hpp b/tools/ExecuteTerminalCommandTool.hpp index 41d9e87..75fdc92 100644 --- a/tools/ExecuteTerminalCommandTool.hpp +++ b/tools/ExecuteTerminalCommandTool.hpp @@ -40,7 +40,18 @@ public: private: bool isCommandAllowed(const QString &command) const; + bool isCommandSafe(const QString &command) const; + bool areArgumentsSafe(const QString &args) const; QStringList getAllowedCommands() const; + QString getCommandDescription() const; + QString sanitizeOutput(const QString &output, qint64 maxSize) const; + + // Constants for production safety + static constexpr int COMMAND_TIMEOUT_MS = 30000; // 30 seconds + static constexpr qint64 MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10 MB + static constexpr int MAX_COMMAND_LENGTH = 1024; + static constexpr int MAX_ARGS_LENGTH = 4096; + static constexpr int PROCESS_START_TIMEOUT_MS = 3000; }; } // namespace QodeAssist::Tools diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp index 5a97d1f..4fdcee6 100644 --- a/tools/ToolsFactory.cpp +++ b/tools/ToolsFactory.cpp @@ -158,11 +158,11 @@ QJsonArray ToolsFactory::getToolsDefinitions( } } - // if (requiredPerms.testFlag(LLMCore::ToolPermission::NetworkAccess)) { - // if (!settings.allowNetworkAccess()) { - // hasPermission = false; - // } - // } + if (requiredPerms.testFlag(LLMCore::ToolPermission::NetworkAccess)) { + if (!settings.allowNetworkAccess()) { + hasPermission = false; + } + } if (hasPermission) { toolsArray.append(it.value()->getDefinition(format));