Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions docs/concepts/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 48 additions & 2 deletions docs/concepts/tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 🔎 🌐

Expand All @@ -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!
82 changes: 82 additions & 0 deletions java/dev/enola/ai/adk/tool/ExecTool.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2025 The Enola <https://enola.dev> 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<String, String> executeCommand(
@Annotations.Schema(description = "The command to execute (e.g., 'ls -l').")
String command) {
return Tools.toMap(executeCommandHelper(command));
}

private SuccessOrError<String> 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)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The command execution timeout is hardcoded to 10 seconds. This might not be suitable for all commands; some may need more time, while for others this might be too long. It would be more flexible to make this timeout configurable, for example by passing it to the ExecTool constructor.

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());
}
}
}
45 changes: 3 additions & 42 deletions java/dev/enola/ai/adk/tool/FileSystemTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The final keyword was removed from the class definition. If this class is not designed for extension, it's a good practice to declare it as final. This prevents subclassing, makes the class's behavior more predictable, and can allow for certain compiler optimizations. If you intend for this class to be subclassed, please consider adding a comment explaining why.

Suggested change
public class FileSystemTools {
public final class FileSystemTools {


// TODO Enforce root(s); see "root" related TODO in McpLoader

public Map<String, BaseTool> createToolSet() {
return ImmutableMap.of(
"read_file", FunctionTool.create(this, "readFile"),
Expand All @@ -56,8 +55,7 @@ public Map<String, BaseTool> 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.")
Expand Down Expand Up @@ -121,14 +119,6 @@ public Map<String, String> grepFile(
return Tools.toMap(grepFileHelper(path, pattern, context));
}

@Schema(
description =
"Executes a shell command and captures its standard output and standard error.")
public Map<String, String> executeCommand(
@Schema(description = "The command to execute (e.g., 'ls -l').") String command) {
return Tools.toMap(executeCommandHelper(command));
}

// Private Helper Methods
private SuccessOrError<String> readFileHelper(String pathString) {
try {
Expand Down Expand Up @@ -275,35 +265,6 @@ private SuccessOrError<String> grepFileHelper(String pathString, String pattern,
}
}

private SuccessOrError<String> 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);
Expand Down
1 change: 1 addition & 0 deletions java/dev/enola/ai/adk/tool/Tools.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 0 additions & 6 deletions models/enola.dev/ai/mcp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 0 additions & 12 deletions test/agents/builtin-tool.agent.yaml

This file was deleted.

10 changes: 8 additions & 2 deletions test/agents/filesystem.agent.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions test/agents/linux-system-summary.agent.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading