001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2025-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.ai.adk.tool.builtin;
019
020import static dev.enola.common.SuccessOrError.error;
021import static dev.enola.common.SuccessOrError.success;
022
023import com.google.adk.tools.Annotations;
024import com.google.adk.tools.BaseTool;
025import com.google.adk.tools.FunctionTool;
026
027import dev.enola.ai.adk.tool.Tools;
028import dev.enola.common.SuccessOrError;
029
030import java.io.BufferedReader;
031import java.io.IOException;
032import java.io.InputStreamReader;
033import java.util.Map;
034import java.util.concurrent.TimeUnit;
035
036public class ExecTool {
037
038    // TODO Make allowed commands configurable
039
040    // TODO Use stuff from package dev.enola.common.exec
041
042    public BaseTool createTool() {
043        return FunctionTool.create(this, "executeCommand");
044    }
045
046    @Annotations.Schema(
047            description =
048                    "Executes a shell command and captures its standard output and standard error.")
049    public Map<String, ?> executeCommand(
050            @Annotations.Schema(description = "The command to execute (e.g., 'ls -l').")
051                    String command) {
052        return Tools.toMap(executeCommandHelper(command));
053    }
054
055    private SuccessOrError<String> executeCommandHelper(String command) {
056        try {
057            ProcessBuilder pb = new ProcessBuilder("bash", "-c", command);
058            pb.redirectErrorStream(true); // Combine stdout and stderr
059            Process process = pb.start();
060
061            StringBuilder output = new StringBuilder();
062            try (BufferedReader reader =
063                    new BufferedReader(new InputStreamReader(process.getInputStream()))) {
064                String line;
065                while ((line = reader.readLine()) != null) {
066                    output.append(line).append("\n");
067                }
068            }
069
070            if (!process.waitFor(10, TimeUnit.SECONDS)) {
071                process.destroyForcibly();
072                return error("Command timed out after 10 seconds.");
073            }
074
075            int exitCode = process.exitValue();
076            return success(
077                    String.format(
078                            "Exit Code: %d%nOutput:%n%s", exitCode, output.toString().trim()));
079        } catch (IOException | InterruptedException e) {
080            return error("Failed to execute command: " + e.getMessage());
081        }
082    }
083}