diff --git a/docs/concepts/mcp.md b/docs/concepts/mcp.md index c8f81867b..49965dd05 100644 --- a/docs/concepts/mcp.md +++ b/docs/concepts/mcp.md @@ -72,18 +72,6 @@ This needs `uvx` to be available; test if launching `uvx mcp-server-fetch` works CAUTION: This server can access local/internal IP addresses, which may represent a security risk. Exercise caution when using this MCP server to ensure this does not expose any sensitive data! -### Filesystem - -```yaml -{% include "../../test/agents/filesystem.agent.yaml" %} -``` - -```shell -enola ai -a test/agents/filesystem.agent.yaml --in="list the files in $PWD" -``` - -This [currently](https://github.com/enola-dev/enola/issues/1631) needs `npx` to be available; test if launching `npx @modelcontextprotocol/server-filesystem` works, first. - ### Git ```yaml diff --git a/docs/concepts/tool.md b/docs/concepts/tool.md index b17a732ec..b841ef7e6 100644 --- a/docs/concepts/tool.md +++ b/docs/concepts/tool.md @@ -36,9 +36,33 @@ $ enola ai --agents=test/agents/clock.agent.yaml --in "What's the time?" The current date & time in CET is Saturday, August 16, 2025, 11:42 PM. ``` -## Exec +## Exec ▶️ -**TODO** _Enola is planning to offer an `exec` tool. We intended to make this highly configurable._ +The `exec` tool can be used to run any command; for example, for something like this: + +```yaml +{% include "../../test/agents/linux-system-summary.agent.yaml" %} +``` + +When this Agent is run, it would print (something like) this: + +```shell +$ enola ai --agents=test/agents/linux-system-summary.agent.yaml --in="do it" + +Xeon CPU runs fast, +Twelve cores, power strong. +Memory 62GiB, +56GiB now in use. +One day, twenty-one mins. +System running well, +Tasks flow with ease. +``` + +!!! danger "Security Consideration" + + The exec tool is very powerful, as it can run any shell command if added to an agent. This may introduce a significant security risk, as a compromised or manipulated prompt or instruction could lead to arbitrary code execution on the machine running Enola. Note that Enola (currently) does not yet double-check tool execution with the user. Please carefully consider this when using this tool. + + **TODO** _We intended to make this highly configurable in the future._ ## Google 🔎 🌐 @@ -65,6 +89,28 @@ Here's a summary of what happened on August 16, 2025: This tool is currently only supported [on Gemini](../specs/aiuri/index.md#google-ai-). +## Files 📂 + +The following built-in tools let an Agent work with the filesystem: + +* `read_file`: Reads the entire content of a specified file. +* `write_file`: Writes content into a file. +* `edit_file`: Replaces a specific range of lines in a file and returns a git-style diff of the changes. +* `search_files`: Recursively searches for files and directories using a glob pattern. +* `list_directory`: Lists the files and directory contents of a given directory with, with details like size and modification date. +* `create_directory`: Creates a directory, including any necessary parent directories. +* `grep_file`: Searches for a text pattern within a file. + +```shell +enola ai -a test/agents/filesystem.agent.yaml --in="list the files in $PWD" +``` + +!!! danger "Security Consideration" + + The filesystem related tools are very powerful, because when enabled in an agent, they can currently access the full filesystem. This may introduce a significant security risk, as a compromised or manipulated prompt or instruction could unintentionally expose data from sensitive files on the machine running Enola. Note that Enola (currently) does not yet double-check tool execution with the user. Please carefully consider this when using this tool, and only add actually required tools to agents. + + **TODO** _We intended to be able to limit accessible directories (like MCP roots) in the future._ + ## MCP [MCP](mcp.md) allows Enola to access many thousands of other tools! diff --git a/java/dev/enola/ai/adk/tool/ExecTool.java b/java/dev/enola/ai/adk/tool/ExecTool.java new file mode 100644 index 000000000..8a2b2294b --- /dev/null +++ b/java/dev/enola/ai/adk/tool/ExecTool.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2025 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.ai.adk.tool; + +import static dev.enola.common.SuccessOrError.error; +import static dev.enola.common.SuccessOrError.success; + +import com.google.adk.tools.Annotations; +import com.google.adk.tools.BaseTool; +import com.google.adk.tools.FunctionTool; + +import dev.enola.common.SuccessOrError; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class ExecTool { + + // TODO Make allowed commands configurable + + // TODO Use stuff from package dev.enola.common.exec + + public BaseTool createTool() { + return FunctionTool.create(this, "executeCommand"); + } + + @Annotations.Schema( + description = + "Executes a shell command and captures its standard output and standard error.") + public Map executeCommand( + @Annotations.Schema(description = "The command to execute (e.g., 'ls -l').") + String command) { + return Tools.toMap(executeCommandHelper(command)); + } + + private SuccessOrError executeCommandHelper(String command) { + try { + ProcessBuilder pb = new ProcessBuilder("bash", "-c", command); + pb.redirectErrorStream(true); // Combine stdout and stderr + Process process = pb.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + if (!process.waitFor(10, TimeUnit.SECONDS)) { + process.destroyForcibly(); + return error("Command timed out after 10 seconds."); + } + + int exitCode = process.exitValue(); + return success( + String.format( + "Exit Code: %d%nOutput:%n%s", exitCode, output.toString().trim())); + } catch (IOException | InterruptedException e) { + return error("Failed to execute command: " + e.getMessage()); + } + } +} diff --git a/java/dev/enola/ai/adk/tool/FileSystemTools.java b/java/dev/enola/ai/adk/tool/FileSystemTools.java index 69f4bfb11..cb4ccefb5 100644 --- a/java/dev/enola/ai/adk/tool/FileSystemTools.java +++ b/java/dev/enola/ai/adk/tool/FileSystemTools.java @@ -27,9 +27,7 @@ import dev.enola.common.SuccessOrError; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.nio.file.*; import java.nio.file.Files; import java.nio.file.Path; @@ -41,13 +39,14 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; public class FileSystemTools { + // TODO Enforce root(s); see "root" related TODO in McpLoader + public Map createToolSet() { return ImmutableMap.of( "read_file", FunctionTool.create(this, "readFile"), @@ -56,8 +55,7 @@ public Map createToolSet() { "search_files", FunctionTool.create(this, "searchFiles"), "list_directory", FunctionTool.create(this, "listDirectory"), "create_directory", FunctionTool.create(this, "createDirectory"), - "grep_file", FunctionTool.create(this, "grepFile"), - "execute_command", FunctionTool.create(this, "executeCommand")); + "grep_file", FunctionTool.create(this, "grepFile")); } @Schema(description = "Reads the entire content of a specified file.") @@ -121,14 +119,6 @@ public Map grepFile( return Tools.toMap(grepFileHelper(path, pattern, context)); } - @Schema( - description = - "Executes a shell command and captures its standard output and standard error.") - public Map executeCommand( - @Schema(description = "The command to execute (e.g., 'ls -l').") String command) { - return Tools.toMap(executeCommandHelper(command)); - } - // Private Helper Methods private SuccessOrError readFileHelper(String pathString) { try { @@ -275,35 +265,6 @@ private SuccessOrError grepFileHelper(String pathString, String pattern, } } - private SuccessOrError executeCommandHelper(String command) { - try { - ProcessBuilder pb = new ProcessBuilder("bash", "-c", command); - pb.redirectErrorStream(true); // Combine stdout and stderr - Process process = pb.start(); - - StringBuilder output = new StringBuilder(); - try (BufferedReader reader = - new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - } - - if (!process.waitFor(10, TimeUnit.SECONDS)) { - process.destroy(); - return error("Command timed out after 10 seconds."); - } - - int exitCode = process.exitValue(); - return success( - String.format( - "Exit Code: %d%nOutput:%n%s", exitCode, output.toString().trim())); - } catch (IOException | InterruptedException e) { - return error("Failed to execute command: " + e.getMessage()); - } - } - private static int sortPaths(Path a, Path b) { boolean isDirectoryA = Files.isDirectory(a); boolean isDirectoryB = Files.isDirectory(b); diff --git a/java/dev/enola/ai/adk/tool/Tools.java b/java/dev/enola/ai/adk/tool/Tools.java index c5e0349f0..58b22cb71 100644 --- a/java/dev/enola/ai/adk/tool/Tools.java +++ b/java/dev/enola/ai/adk/tool/Tools.java @@ -47,6 +47,7 @@ public static ToolsetProvider builtin(InstantSource instantSource) { "clock", DateTimeTools.currentDateAndTimeAdkTool(new DateTimeTools(instantSource))); tools.put("search_google", new GoogleSearchTool()); tools.putAll(new FileSystemTools().createToolSet()); + tools.put("exec", new ExecTool().createTool()); return ToolsetProvider.immutableTools(tools); } diff --git a/java/dev/enola/ai/dotagent/AgentsLoaderIntegrationTest.java b/java/dev/enola/ai/dotagent/AgentsLoaderIntegrationTest.java index 0fc9b53a0..b9b002a93 100644 --- a/java/dev/enola/ai/dotagent/AgentsLoaderIntegrationTest.java +++ b/java/dev/enola/ai/dotagent/AgentsLoaderIntegrationTest.java @@ -81,7 +81,7 @@ public void clock() throws IOException { var tools = Tools.builtin(testInstantSource); if (secretManager.getOptional(GOOGLE_AI_API_KEY_SECRET_NAME).isEmpty()) return; - var loader = new AgentsLoader(rp, FLASH_LITE, new GoogleLlmProvider(secretManager), tools); + var loader = new AgentsLoader(rp, FLASH, new GoogleLlmProvider(secretManager), tools); var agent = load(loader, "clock.agent.yaml"); var agentTester = new AgentTester(agent); diff --git a/models/enola.dev/ai/mcp.yaml b/models/enola.dev/ai/mcp.yaml index 5baaf93cf..b41331325 100644 --- a/models/enola.dev/ai/mcp.yaml +++ b/models/enola.dev/ai/mcp.yaml @@ -18,12 +18,6 @@ $schema: https://enola.dev/ai/mcp/server/connections servers: - modelcontextprotocol/filesystem: - command: npx - args: [-y, "@modelcontextprotocol/server-filesystem"] - origin: https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem - roots: true - modelcontextprotocol/git: command: uvx args: [mcp-server-git] diff --git a/test/agents/builtin-tool.agent.yaml b/test/agents/builtin-tool.agent.yaml deleted file mode 100644 index 49c561258..000000000 --- a/test/agents/builtin-tool.agent.yaml +++ /dev/null @@ -1,12 +0,0 @@ -$schema: https://enola.dev/ai/agent -model: google://?model=gemini-2.5-flash-lite -tools: - - clock - - read_file - - write_file - - list_directory - - edit_file - - search_files - - create_directory - - grep_file - - execute_command diff --git a/test/agents/filesystem.agent.yaml b/test/agents/filesystem.agent.yaml index 52e5cea5e..86726a7eb 100644 --- a/test/agents/filesystem.agent.yaml +++ b/test/agents/filesystem.agent.yaml @@ -1,4 +1,10 @@ $schema: https://enola.dev/ai/agent -model: google://?model=gemini-2.5-flash-lite +model: google://?model=gemini-2.5-flash tools: - - modelcontextprotocol/filesystem + - read_file + - write_file + - list_directory + - edit_file + - search_files + - create_directory + - grep_file diff --git a/test/agents/linux-system-summary.agent.yaml b/test/agents/linux-system-summary.agent.yaml new file mode 100644 index 000000000..fe0024114 --- /dev/null +++ b/test/agents/linux-system-summary.agent.yaml @@ -0,0 +1,10 @@ +$schema: https://enola.dev/ai/agent +model: google://?model=gemini-2.5-flash +# TODO prompt +instruction: + Generate a system summary using various Linux commands and format the output as a poem. + For memory, do include both total as well as used, including buff/cache. + For uptime, run it without any options, because not all versions support. +# TODO Limit allowed commands to e.g. only "uname -a", "df -h /", "free -m", "uptime" (or whatever) +tools: + - exec