diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolAdapter.java new file mode 100644 index 0000000..e7a64c1 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolAdapter.java @@ -0,0 +1,184 @@ +package com.easyagents.agent.runtime.tool.operate; + +import com.easyagents.agent.runtime.AgentRuntimeException; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest; +import com.easyagents.agent.runtime.tool.AgentToolCategory; +import com.easyagents.agent.runtime.tool.AgentToolSpec; +import com.easyagents.agent.runtime.tool.AgentToolVisibility; +import io.agentscope.core.tool.Toolkit; +import io.agentscope.core.tool.coding.ShellCommandTool; +import io.agentscope.core.tool.file.ReadFileTool; +import io.agentscope.core.tool.file.WriteFileTool; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.*; + +/** + * AgentScope 内置操作工具适配器。 + * + *

该适配器只负责将 Easy-Agents 的操作工具声明转换为 AgentScope Toolkit 中的原生工具。 + * Shell 工具的人工审批不使用 AgentScope {@code ShellCommandTool} 的同步 callback,而是通过 + * Easy-Agents 现有 {@code ToolHitlInterceptor} 统一处理,以保持 SSE 暂停、恢复和审计语义一致。 + */ +public class AgentOperateToolAdapter { + + public static final String VIEW_TEXT_FILE_TOOL = "view_text_file"; + public static final String LIST_DIRECTORY_TOOL = "list_directory"; + public static final String WRITE_TEXT_FILE_TOOL = "write_text_file"; + public static final String INSERT_TEXT_FILE_TOOL = "insert_text_file"; + public static final String EXECUTE_SHELL_COMMAND_TOOL = "execute_shell_command"; + + /** + * 将操作工具声明注册到 Toolkit,并返回供 HITL 拦截器使用的工具声明。 + * + * @param specs 操作工具声明 + * @param toolkit AgentScope Toolkit + * @return 已启用操作工具对应的 AgentToolSpec + */ + public List register(List specs, Toolkit toolkit) { + List toolSpecs = new ArrayList<>(); + if (specs == null || specs.isEmpty()) { + return toolSpecs; + } + for (AgentOperateToolSpec spec : specs) { + if (spec == null || !spec.isEnabled()) { + continue; + } + registerOne(spec, toolkit, toolSpecs); + } + return toolSpecs; + } + + /** + * 解析已启用操作工具会注册到 AgentScope 的工具名称。 + * + * @param specs 操作工具声明 + * @return 工具名称集合 + */ + public Set enabledToolNames(List specs) { + Set names = new LinkedHashSet<>(); + if (specs == null || specs.isEmpty()) { + return names; + } + for (AgentOperateToolSpec spec : specs) { + if (spec == null || !spec.isEnabled() || spec.getType() == null) { + continue; + } + switch (spec.getType()) { + case READ_FILE -> { + names.add(VIEW_TEXT_FILE_TOOL); + names.add(LIST_DIRECTORY_TOOL); + } + case WRITE_FILE -> { + names.add(WRITE_TEXT_FILE_TOOL); + names.add(INSERT_TEXT_FILE_TOOL); + } + case SHELL -> names.add(EXECUTE_SHELL_COMMAND_TOOL); + default -> { + } + } + } + return names; + } + + private void registerOne(AgentOperateToolSpec spec, Toolkit toolkit, List toolSpecs) { + AgentOperateToolType type = spec.getType(); + if (type == null) { + throw new AgentRuntimeException("Agent operate tool type is required."); + } + Path baseDir = validateBaseDir(spec); + switch (type) { + case READ_FILE -> { + assertNoToolConflict(toolkit, VIEW_TEXT_FILE_TOOL); + assertNoToolConflict(toolkit, LIST_DIRECTORY_TOOL); + toolkit.registerTool(new ReadFileTool(baseDir.toString())); + toolSpecs.add(toolSpec(spec, VIEW_TEXT_FILE_TOOL, "View text file content.", false)); + toolSpecs.add(toolSpec(spec, LIST_DIRECTORY_TOOL, "List files and directories.", false)); + } + case WRITE_FILE -> { + assertNoToolConflict(toolkit, WRITE_TEXT_FILE_TOOL); + assertNoToolConflict(toolkit, INSERT_TEXT_FILE_TOOL); + toolkit.registerTool(new WriteFileTool(baseDir.toString())); + toolSpecs.add(toolSpec(spec, WRITE_TEXT_FILE_TOOL, "Write or replace text file content.", true)); + toolSpecs.add(toolSpec(spec, INSERT_TEXT_FILE_TOOL, "Insert text into a file.", true)); + } + case SHELL -> { + assertNoToolConflict(toolkit, EXECUTE_SHELL_COMMAND_TOOL); + Charset charset = parseCharset(spec); + toolkit.registerAgentTool(new ShellCommandTool(baseDir.toString(), spec.getShellAllowedCommands(), null, + null, charset)); + toolSpecs.add(toolSpec(spec, EXECUTE_SHELL_COMMAND_TOOL, "Execute shell command.", true)); + } + default -> throw new AgentRuntimeException("Unsupported agent operate tool type: " + type); + } + } + + private Path validateBaseDir(AgentOperateToolSpec spec) { + String baseDir = spec.getBaseDir(); + if (baseDir == null || baseDir.isBlank()) { + throw new AgentRuntimeException("Agent operate tool baseDir is required."); + } + Path path = Path.of(baseDir).toAbsolutePath().normalize(); + if (!Path.of(baseDir).isAbsolute()) { + throw new AgentRuntimeException("Agent operate tool baseDir must be an absolute path: " + baseDir); + } + return path; + } + + private Charset parseCharset(AgentOperateToolSpec spec) { + String charsetName = spec.getShellCharset(); + if (charsetName == null || charsetName.isBlank()) { + return StandardCharsets.UTF_8; + } + try { + return Charset.forName(charsetName.trim()); + } catch (Exception error) { + throw new AgentRuntimeException("Invalid shell charset: " + charsetName, error); + } + } + + private AgentToolSpec toolSpec(AgentOperateToolSpec operateSpec, + String toolName, + String description, + boolean defaultApprovalRequired) { + boolean approvalRequired = operateSpec.getApprovalRequired() == null + ? defaultApprovalRequired : operateSpec.getApprovalRequired(); + AgentToolSpec toolSpec = new AgentToolSpec(); + toolSpec.setName(toolName); + toolSpec.setDescription(description); + toolSpec.setCategory(AgentToolCategory.CUSTOM); + toolSpec.setVisibility(AgentToolVisibility.VISIBLE); + toolSpec.setApprovalRequired(approvalRequired); + toolSpec.setApprovalRequest(approvalRequest(operateSpec, approvalRequired)); + toolSpec.setMetadata(metadata(operateSpec)); + return toolSpec; + } + + private AgentToolApprovalRequest approvalRequest(AgentOperateToolSpec operateSpec, boolean approvalRequired) { + AgentToolApprovalRequest request = operateSpec.getApprovalRequest(); + if (request != null) { + return request; + } + AgentToolApprovalRequest defaultRequest = new AgentToolApprovalRequest(); + if (approvalRequired) { + defaultRequest.setApprovalPrompt("请确认是否允许智能体执行该操作工具。"); + } + return defaultRequest; + } + + private Map metadata(AgentOperateToolSpec spec) { + Map metadata = new LinkedHashMap<>(); + metadata.put("operateTool", true); + metadata.put("operateToolType", spec.getType().name()); + metadata.put("baseDir", spec.getBaseDir()); + return metadata; + } + + private void assertNoToolConflict(Toolkit toolkit, String toolName) { + if (toolkit.getTool(toolName) != null) { + throw new AgentRuntimeException("Agent operate tool conflicts with existing tool: " + toolName); + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolSpec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolSpec.java new file mode 100644 index 0000000..78df6de --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolSpec.java @@ -0,0 +1,150 @@ +package com.easyagents.agent.runtime.tool.operate; + +import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Agent 操作类工具声明。 + * + *

操作类工具是 runtime 直接适配的 AgentScope 内置工具,用于读文件、写文件和执行 Shell。 + * 这些工具直接作用于后端 JVM 所在宿主环境,调用方必须按 agent、session 或 user 维度传入受控 + * 的绝对工作目录。 + */ +public class AgentOperateToolSpec { + + private AgentOperateToolType type; + private boolean enabled = true; + private String baseDir; + private Boolean approvalRequired; + private AgentToolApprovalRequest approvalRequest; + private Set shellAllowedCommands = new LinkedHashSet<>(); + private String shellCharset; + + /** + * 获取操作工具类型。 + * + * @return 操作工具类型 + */ + public AgentOperateToolType getType() { + return type; + } + + /** + * 设置操作工具类型。 + * + * @param type 操作工具类型 + */ + public void setType(AgentOperateToolType type) { + this.type = type; + } + + /** + * 返回是否启用该操作工具。 + * + * @return 启用时为 true + */ + public boolean isEnabled() { + return enabled; + } + + /** + * 设置是否启用该操作工具。 + * + * @param enabled 启用标记 + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * 获取操作工具工作目录。 + * + * @return 绝对工作目录 + */ + public String getBaseDir() { + return baseDir; + } + + /** + * 设置操作工具工作目录。 + * + * @param baseDir 绝对工作目录 + */ + public void setBaseDir(String baseDir) { + this.baseDir = baseDir; + } + + /** + * 获取审批开关覆盖值。 + * + * @return 审批覆盖值,null 表示使用工具类型默认值 + */ + public Boolean getApprovalRequired() { + return approvalRequired; + } + + /** + * 设置审批开关覆盖值。 + * + * @param approvalRequired 审批覆盖值 + */ + public void setApprovalRequired(Boolean approvalRequired) { + this.approvalRequired = approvalRequired; + } + + /** + * 获取审批请求配置。 + * + * @return 审批请求配置 + */ + public AgentToolApprovalRequest getApprovalRequest() { + return approvalRequest; + } + + /** + * 设置审批请求配置。 + * + * @param approvalRequest 审批请求配置 + */ + public void setApprovalRequest(AgentToolApprovalRequest approvalRequest) { + this.approvalRequest = approvalRequest; + } + + /** + * 获取 Shell 命令白名单。 + * + * @return Shell 命令白名单 + */ + public Set getShellAllowedCommands() { + return shellAllowedCommands; + } + + /** + * 设置 Shell 命令白名单。 + * + * @param shellAllowedCommands Shell 命令白名单 + */ + public void setShellAllowedCommands(Set shellAllowedCommands) { + this.shellAllowedCommands = shellAllowedCommands == null ? new LinkedHashSet<>() : new LinkedHashSet<>(shellAllowedCommands); + } + + /** + * 获取 Shell 输出字符集名称。 + * + * @return 字符集名称 + */ + public String getShellCharset() { + return shellCharset; + } + + /** + * 设置 Shell 输出字符集名称。 + * + * @param shellCharset 字符集名称 + */ + public void setShellCharset(String shellCharset) { + this.shellCharset = shellCharset; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolType.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolType.java new file mode 100644 index 0000000..e566420 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolType.java @@ -0,0 +1,22 @@ +package com.easyagents.agent.runtime.tool.operate; + +/** + * Agent 操作类工具类型。 + */ +public enum AgentOperateToolType { + + /** + * 读取文本文件与列出目录。 + */ + READ_FILE, + + /** + * 写入、覆盖或插入文本文件。 + */ + WRITE_FILE, + + /** + * 在服务进程所在宿主环境执行 Shell 命令。 + */ + SHELL +} diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolAdapterTest.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolAdapterTest.java new file mode 100644 index 0000000..0accff4 --- /dev/null +++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/operate/AgentOperateToolAdapterTest.java @@ -0,0 +1,101 @@ +package com.easyagents.agent.runtime.tool.operate; + +import com.easyagents.agent.runtime.AgentRuntimeException; +import com.easyagents.agent.runtime.tool.AgentToolSpec; +import io.agentscope.core.tool.Toolkit; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.Set; + +/** + * 测试 Agent 操作类工具适配器。 + */ +public class AgentOperateToolAdapterTest { + + private final AgentOperateToolAdapter adapter = new AgentOperateToolAdapter(); + + @Test + public void shouldRegisterReadFileToolsWithDefaultHitlDisabled() { + Toolkit toolkit = new Toolkit(); + AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE); + + List toolSpecs = adapter.register(List.of(spec), toolkit); + + Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.VIEW_TEXT_FILE_TOOL)); + Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.LIST_DIRECTORY_TOOL)); + Assert.assertEquals(2, toolSpecs.size()); + Assert.assertTrue(toolSpecs.stream().noneMatch(AgentToolSpec::isApprovalRequired)); + } + + @Test + public void shouldRegisterWriteFileToolsWithDefaultHitlEnabled() { + Toolkit toolkit = new Toolkit(); + AgentOperateToolSpec spec = spec(AgentOperateToolType.WRITE_FILE); + + List toolSpecs = adapter.register(List.of(spec), toolkit); + + Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.WRITE_TEXT_FILE_TOOL)); + Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.INSERT_TEXT_FILE_TOOL)); + Assert.assertEquals(2, toolSpecs.size()); + Assert.assertTrue(toolSpecs.stream().allMatch(AgentToolSpec::isApprovalRequired)); + } + + @Test + public void shouldRegisterShellToolWithEmptyWhitelist() { + Toolkit toolkit = new Toolkit(); + AgentOperateToolSpec spec = spec(AgentOperateToolType.SHELL); + spec.setShellAllowedCommands(Set.of()); + + List toolSpecs = adapter.register(List.of(spec), toolkit); + + Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL)); + Assert.assertEquals(1, toolSpecs.size()); + Assert.assertTrue(toolSpecs.get(0).isApprovalRequired()); + } + + @Test + public void shouldSkipDisabledOperateTool() { + Toolkit toolkit = new Toolkit(); + AgentOperateToolSpec spec = spec(AgentOperateToolType.SHELL); + spec.setEnabled(false); + + List toolSpecs = adapter.register(List.of(spec), toolkit); + + Assert.assertNull(toolkit.getTool(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL)); + Assert.assertTrue(toolSpecs.isEmpty()); + } + + @Test(expected = AgentRuntimeException.class) + public void shouldRejectMissingBaseDir() { + AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE); + spec.setBaseDir(null); + + adapter.register(List.of(spec), new Toolkit()); + } + + @Test(expected = AgentRuntimeException.class) + public void shouldRejectRelativeBaseDir() { + AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE); + spec.setBaseDir("relative/workspace"); + + adapter.register(List.of(spec), new Toolkit()); + } + + @Test(expected = AgentRuntimeException.class) + public void shouldRejectToolNameConflict() { + Toolkit toolkit = new Toolkit(); + AgentOperateToolSpec first = spec(AgentOperateToolType.READ_FILE); + AgentOperateToolSpec second = spec(AgentOperateToolType.READ_FILE); + + adapter.register(List.of(first, second), toolkit); + } + + private AgentOperateToolSpec spec(AgentOperateToolType type) { + AgentOperateToolSpec spec = new AgentOperateToolSpec(); + spec.setType(type); + spec.setBaseDir(System.getProperty("java.io.tmpdir")); + return spec; + } +}