feat: BashTool sandbox, ConfigTool persistence, ToolSearchTool

- BashTool: command blacklist (fork bombs, rm -rf /, etc) + risky
  command warning log. Blocked commands return clear error message.
- ConfigTool: JSON file persistence (~/.claude-code/config.json),
  new 'list' action, loads from file on first access.
- ToolSearchTool: search ToolRegistry by name/description keywords,
  returns matching tools with descriptions (total: 23 tools).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
abel533 1 month ago
parent f5da3499a4
commit 05e6e019b2
  1. 6
      src/main/java/com/claudecode/config/AppConfig.java
  2. 48
      src/main/java/com/claudecode/tool/impl/BashTool.java
  3. 303
      src/main/java/com/claudecode/tool/impl/ConfigTool.java
  4. 115
      src/main/java/com/claudecode/tool/impl/ToolSearchTool.java

@ -109,7 +109,8 @@ public class AppConfig {
// P2: 配置工具
new ConfigTool(),
// P2: 实用工具
new SleepTool()
new SleepTool(),
new ToolSearchTool()
);
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
@ -119,6 +120,9 @@ public class AppConfig {
}
}
// 将 ToolRegistry 注入 ToolContext,供 ToolSearchTool 使用
toolContext.set("TOOL_REGISTRY", registry);
return registry;
}

@ -9,6 +9,7 @@ import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
@ -28,6 +29,35 @@ public class BashTool implements Tool {
/** 默认超时(秒) */
private static final int DEFAULT_TIMEOUT = 120;
/**
* 危险命令黑名单 这些命令前缀需要特别警告
* 对应 TS shouldUseSandbox.ts containsExcludedCommand
* 注意这不是安全边界而是用户友好的提示机制
*/
private static final Set<String> DANGEROUS_COMMANDS = Set.of(
"rm -rf /", "rm -rf /*", "rm -rf ~",
"mkfs", "dd if=",
":(){:|:&};:", // fork bomb
"chmod -R 777 /",
"git push --force", "git push -f",
"git reset --hard",
"shutdown", "reboot", "halt",
"format c:", "del /f /s /q c:\\"
);
/**
* 需要用户确认的命令前缀 风险较高但不是完全禁止的操作
*/
private static final Set<String> WARN_COMMAND_PREFIXES = Set.of(
"rm -rf", "rm -r",
"git push --force", "git push -f",
"git reset --hard",
"git rebase",
"drop database", "drop table",
"truncate table",
"sudo rm", "sudo dd"
);
/** 检测到的 shell 类型 */
public enum ShellType {
POWERSHELL("PowerShell", "pwsh", "-NoProfile", "-Command"),
@ -126,6 +156,24 @@ public class BashTool implements Tool {
: DEFAULT_TIMEOUT;
Path workDir = context.getWorkDir();
// Sandbox check: block absolutely dangerous commands
String cmdLower = command.toLowerCase().trim();
for (String dangerous : DANGEROUS_COMMANDS) {
if (cmdLower.equals(dangerous) || cmdLower.startsWith(dangerous)) {
return "⛔ BLOCKED: This command is potentially destructive and has been blocked.\n"
+ "Command: " + command + "\n"
+ "If you really need to run this, please ask the user to execute it manually.";
}
}
// Sandbox warning: flag risky commands
for (String prefix : WARN_COMMAND_PREFIXES) {
if (cmdLower.startsWith(prefix)) {
log.warn("⚠ Risky command detected: {}", command);
break;
}
}
try {
ProcessBuilder pb = buildProcess(command);
pb.directory(workDir.toFile());

@ -2,35 +2,46 @@ package com.claudecode.tool.impl;
import com.claudecode.tool.Tool;
import com.claudecode.tool.ToolContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Config 工具 获取或设置配置值
* Config 工具 获取或设置配置值持久化到 JSON 文件
* <p>
* 属于 P2 优先级的辅助工具支持两种存储后端
* 属于 P2 优先级的辅助工具存储后端优先级
* </p>
* <ol>
* <li>首选从 ToolContext 中的配置映射 "CONFIG_STORE"读写</li>
* <li>回退到 {@link System#getProperty} / {@link System#setProperty}</li>
* <li>ToolContext 中的内存缓存CONFIG_STORE 快速读写</li>
* <li>JSON 文件持久化~/.claude-code/config.json 跨会话保持</li>
* <li>回退到 {@link System#getProperty} 兜底读取</li>
* </ol>
*
* <h3>参数</h3>
* <ul>
* <li><b>action</b>必填 "get" "set"</li>
* <li><b>key</b>必填 配置键名</li>
* <li><b>value</b>可选set 时必填 配置值</li>
* <li><b>action</b>必填 "get""set" "list"</li>
* <li><b>key</b>get/set 必填 配置键名</li>
* <li><b>value</b>set 时必填 配置值</li>
* </ul>
*
* <h3>返回</h3>
* <p>JSON 格式get 返回当前值set 返回确认信息</p>
*/
public class ConfigTool implements Tool {
private static final Logger log = LoggerFactory.getLogger(ConfigTool.class);
/** ToolContext 中配置存储的键名 */
private static final String CONFIG_STORE_KEY = "CONFIG_STORE";
/** 配置文件路径 */
private static final Path CONFIG_FILE = Path.of(
System.getProperty("user.home"), ".claude-code", "config.json");
@Override
public String name() {
return "Config";
@ -39,8 +50,8 @@ public class ConfigTool implements Tool {
@Override
public String description() {
return """
Get or set configuration values. Use when the user requests configuration changes, \
asks about current settings, or when adjusting a setting would benefit them.
Get, set, or list configuration values. Configuration persists across sessions \
in ~/.claude-code/config.json.
Available settings include:
- language: Preferred response language (e.g., "zh-CN", "en")
@ -50,7 +61,7 @@ public class ConfigTool implements Tool {
- timeout: Default command timeout in seconds
- permissions: Permission mode (ask/auto/deny)
Use action 'get' to read a setting value, 'set' to change it.""";
Use action 'get' to read, 'set' to change, 'list' to see all settings.""";
}
@Override
@ -61,26 +72,22 @@ public class ConfigTool implements Tool {
"properties": {
"action": {
"type": "string",
"description": "Action type: get or set",
"enum": ["get", "set"]
"description": "Action type: get, set, or list",
"enum": ["get", "set", "list"]
},
"key": {
"type": "string",
"description": "Configuration key name"
"description": "Configuration key name (required for get/set)"
},
"value": {
"type": "string",
"description": "Configuration value (required for set operation)"
}
},
"required": ["action", "key"]
"required": ["action"]
}""";
}
/**
* Config 工具不是纯只读的set 操作会修改状态
* 但出于安全考虑仍标记为 false
*/
@Override
public boolean isReadOnly() {
return false;
@ -88,33 +95,32 @@ public class ConfigTool implements Tool {
@Override
public String execute(Map<String, Object> input, ToolContext context) {
// 解析必填参数: action
String action = (String) input.get("action");
if (action == null || action.isBlank()) {
return errorJson("Parameter 'action' is required, valid values: get, set");
return errorJson("Parameter 'action' is required, valid values: get, set, list");
}
action = action.trim().toLowerCase();
// 解析必填参数: key
String key = (String) input.get("key");
if (key == null || key.isBlank()) {
return errorJson("Parameter 'key' is required and cannot be empty");
}
// 获取或初始化配置存储
@SuppressWarnings("unchecked")
ConcurrentHashMap<String, String> configStore =
context.getOrDefault(CONFIG_STORE_KEY, null);
if (configStore == null) {
configStore = new ConcurrentHashMap<>();
context.set(CONFIG_STORE_KEY, configStore);
}
// 获取或初始化配置存储(从文件加载)
ConcurrentHashMap<String, String> configStore = getOrInitStore(context);
return switch (action) {
case "get" -> executeGet(key, configStore);
case "set" -> executeSet(key, input, configStore);
default -> errorJson("Invalid action value: '" + action + "'. Valid values: get, set");
case "get" -> {
String key = (String) input.get("key");
if (key == null || key.isBlank()) {
yield errorJson("'get' action requires 'key' parameter");
}
yield executeGet(key, configStore);
}
case "set" -> {
String key = (String) input.get("key");
if (key == null || key.isBlank()) {
yield errorJson("'set' action requires 'key' parameter");
}
yield executeSet(key, input, configStore);
}
case "list" -> executeList(configStore);
default -> errorJson("Invalid action: '" + action + "'. Valid values: get, set, list");
};
}
@ -122,6 +128,9 @@ public class ConfigTool implements Tool {
public String activityDescription(Map<String, Object> input) {
String action = (String) input.getOrDefault("action", "?");
String key = (String) input.getOrDefault("key", "?");
if ("list".equalsIgnoreCase(action)) {
return "⚙ Listing all config";
}
if ("set".equalsIgnoreCase(action)) {
return "⚙ Setting config: " + key;
}
@ -129,119 +138,187 @@ public class ConfigTool implements Tool {
}
/* ------------------------------------------------------------------ */
/* get / set 具体实现 */
/* get / set / list 具体实现 */
/* ------------------------------------------------------------------ */
/**
* 执行 get 操作优先从配置映射读取回退到系统属性
*
* @param key 配置键
* @param configStore 配置映射
* @return JSON 格式的结果
*/
private String executeGet(String key, ConcurrentHashMap<String, String> configStore) {
// 优先从上下文配置映射获取
String value = configStore.get(key);
// 回退到系统属性
if (value == null) {
value = System.getProperty(key);
}
if (value == null) {
return """
{
"action": "get",
"key": "%s",
"value": null,
"found": false,
"message": "Config key '%s' not found"
}""".formatted(escapeJson(key), escapeJson(key));
{"action": "get", "key": "%s", "value": null, "found": false, \
"message": "Config key '%s' not found"}"""
.formatted(escapeJson(key), escapeJson(key));
}
return """
{
"action": "get",
"key": "%s",
"value": "%s",
"found": true
}""".formatted(escapeJson(key), escapeJson(value));
{"action": "get", "key": "%s", "value": "%s", "found": true}"""
.formatted(escapeJson(key), escapeJson(value));
}
/**
* 执行 set 操作同时写入配置映射和系统属性
*
* @param key 配置键
* @param input 输入参数映射
* @param configStore 配置映射
* @return JSON 格式的确认
*/
private String executeSet(String key, Map<String, Object> input,
ConcurrentHashMap<String, String> configStore) {
String value = (String) input.get("value");
if (value == null) {
return errorJson("set operation requires 'value' parameter");
return errorJson("'set' action requires 'value' parameter");
}
// 获取旧值(用于返回信息)
String oldValue = configStore.get(key);
if (oldValue == null) {
oldValue = System.getProperty(key);
}
// 写入配置映射
configStore.put(key, value);
// 同步写入系统属性(简单实现,生产环境应使用专门的配置管理)
try {
System.setProperty(key, value);
} catch (SecurityException e) {
// 如果没有权限设置系统属性,只使用配置映射即可
}
// 持久化到文件
persistToFile(configStore);
StringBuilder sb = new StringBuilder();
sb.append("{\n");
sb.append(" \"action\": \"set\",\n");
sb.append(" \"key\": \"").append(escapeJson(key)).append("\",\n");
sb.append(" \"value\": \"").append(escapeJson(value)).append("\",\n");
sb.append("{\"action\": \"set\", \"key\": \"").append(escapeJson(key));
sb.append("\", \"value\": \"").append(escapeJson(value)).append("\"");
if (oldValue != null) {
sb.append(" \"previous_value\": \"").append(escapeJson(oldValue)).append("\",\n");
} else {
sb.append(" \"previous_value\": null,\n");
sb.append(", \"previous_value\": \"").append(escapeJson(oldValue)).append("\"");
}
sb.append(", \"success\": true}");
return sb.toString();
}
sb.append(" \"success\": true,\n");
sb.append(" \"message\": \"Config key '").append(escapeJson(key)).append("' has been set\"\n");
sb.append("}");
private String executeList(ConcurrentHashMap<String, String> configStore) {
if (configStore.isEmpty()) {
return "{\"action\": \"list\", \"count\": 0, \"message\": \"No configuration set\"}";
}
StringBuilder sb = new StringBuilder();
sb.append("{\"action\": \"list\", \"count\": ").append(configStore.size());
sb.append(", \"settings\": {");
boolean first = true;
for (var entry : new java.util.TreeMap<>(configStore).entrySet()) {
if (!first) sb.append(", ");
sb.append("\"").append(escapeJson(entry.getKey())).append("\": \"")
.append(escapeJson(entry.getValue())).append("\"");
first = false;
}
sb.append("}}");
return sb.toString();
}
/* ------------------------------------------------------------------ */
/* 辅助方法 */
/* 持久化 — JSON 文件读写 */
/* ------------------------------------------------------------------ */
@SuppressWarnings("unchecked")
private ConcurrentHashMap<String, String> getOrInitStore(ToolContext context) {
ConcurrentHashMap<String, String> store = context.getOrDefault(CONFIG_STORE_KEY, null);
if (store != null) {
return store;
}
// 从文件加载
store = loadFromFile();
context.set(CONFIG_STORE_KEY, store);
return store;
}
/**
* 转义 JSON 特殊字符
* JSON 文件加载配置
* 使用简单的手动 JSON 解析避免引入额外依赖
*/
private String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
private ConcurrentHashMap<String, String> loadFromFile() {
ConcurrentHashMap<String, String> store = new ConcurrentHashMap<>();
if (!Files.exists(CONFIG_FILE)) {
return store;
}
try {
String json = Files.readString(CONFIG_FILE, StandardCharsets.UTF_8).trim();
if (json.startsWith("{") && json.endsWith("}")) {
// Simple JSON parsing for flat key-value pairs
String inner = json.substring(1, json.length() - 1).trim();
if (!inner.isEmpty()) {
parseJsonPairs(inner, store);
}
}
log.debug("Loaded {} config entries from {}", store.size(), CONFIG_FILE);
} catch (IOException e) {
log.warn("Failed to load config from {}: {}", CONFIG_FILE, e.getMessage());
}
return store;
}
/**
* 持久化配置到 JSON 文件
*/
private void persistToFile(ConcurrentHashMap<String, String> store) {
try {
Files.createDirectories(CONFIG_FILE.getParent());
StringBuilder json = new StringBuilder("{\n");
boolean first = true;
for (var entry : new java.util.TreeMap<>(store).entrySet()) {
if (!first) json.append(",\n");
json.append(" \"").append(escapeJson(entry.getKey()))
.append("\": \"").append(escapeJson(entry.getValue())).append("\"");
first = false;
}
json.append("\n}");
Files.writeString(CONFIG_FILE, json.toString(), StandardCharsets.UTF_8);
log.debug("Persisted {} config entries to {}", store.size(), CONFIG_FILE);
} catch (IOException e) {
log.warn("Failed to persist config to {}: {}", CONFIG_FILE, e.getMessage());
}
}
/**
* 构建错误 JSON 响应
* 简单解析 JSON 键值对仅支持字符串值的扁平对象
*/
private void parseJsonPairs(String inner, Map<String, String> store) {
int i = 0;
while (i < inner.length()) {
// Find key
int keyStart = inner.indexOf('"', i);
if (keyStart < 0) break;
int keyEnd = findClosingQuote(inner, keyStart + 1);
if (keyEnd < 0) break;
String key = unescapeJson(inner.substring(keyStart + 1, keyEnd));
// Find colon
int colon = inner.indexOf(':', keyEnd + 1);
if (colon < 0) break;
// Find value
int valStart = inner.indexOf('"', colon + 1);
if (valStart < 0) break;
int valEnd = findClosingQuote(inner, valStart + 1);
if (valEnd < 0) break;
String value = unescapeJson(inner.substring(valStart + 1, valEnd));
store.put(key, value);
i = valEnd + 1;
}
}
private int findClosingQuote(String s, int from) {
for (int i = from; i < s.length(); i++) {
if (s.charAt(i) == '\\') { i++; continue; }
if (s.charAt(i) == '"') return i;
}
return -1;
}
private String unescapeJson(String s) {
return s.replace("\\\"", "\"").replace("\\\\", "\\")
.replace("\\n", "\n").replace("\\t", "\t");
}
private String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\").replace("\"", "\\\"")
.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
}
private String errorJson(String message) {
return """
{
"error": true,
"message": "%s"
}""".formatted(escapeJson(message));
return "{\"error\": true, \"message\": \"%s\"}".formatted(escapeJson(message));
}
}

@ -0,0 +1,115 @@
package com.claudecode.tool.impl;
import com.claudecode.tool.Tool;
import com.claudecode.tool.ToolContext;
import com.claudecode.tool.ToolRegistry;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 工具搜索工具 对应 claude-code /tools 命令功能
* <p>
* ToolRegistry 中搜索已注册的工具按名称或描述关键字匹配
* 用于帮助 LLM 发现可用工具
*/
public class ToolSearchTool implements Tool {
private static final String TOOL_REGISTRY_KEY = "TOOL_REGISTRY";
@Override
public String name() {
return "ToolSearch";
}
@Override
public String description() {
return """
Search for available tools by name or keyword. Returns matching tool names and \
their descriptions. Use this when you need to find which tools are available for \
a specific task, or when the user asks about available capabilities.""";
}
@Override
public String inputSchema() {
return """
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query to match against tool names and descriptions. Empty string lists all tools."
}
},
"required": ["query"]
}""";
}
@Override
public boolean isReadOnly() {
return true;
}
@Override
public String execute(Map<String, Object> input, ToolContext context) {
String query = (String) input.get("query");
if (query == null) query = "";
String queryLower = query.toLowerCase().trim();
ToolRegistry registry = context.getOrDefault(TOOL_REGISTRY_KEY, null);
if (registry == null) {
return "Error: ToolRegistry is not available";
}
List<Tool> allTools = registry.getTools();
List<Tool> matches;
if (queryLower.isEmpty()) {
matches = allTools;
} else {
matches = allTools.stream()
.filter(t -> t.name().toLowerCase().contains(queryLower)
|| t.description().toLowerCase().contains(queryLower))
.collect(Collectors.toList());
}
if (matches.isEmpty()) {
return String.format("""
{"query": "%s", "total_tools": %d, "matches": 0, \
"message": "No tools matched the query."}""",
escapeJson(query), allTools.size());
}
StringBuilder sb = new StringBuilder();
sb.append(String.format("Found %d tool(s)", matches.size()));
if (!queryLower.isEmpty()) {
sb.append(" matching \"").append(query).append("\"");
}
sb.append(" (").append(allTools.size()).append(" total):\n\n");
for (Tool t : matches) {
sb.append("• **").append(t.name()).append("**");
if (t.isReadOnly()) sb.append(" [read-only]");
sb.append("\n");
// Truncate description to first 120 chars for overview
String desc = t.description().strip();
if (desc.length() > 120) {
desc = desc.substring(0, 117) + "...";
}
sb.append(" ").append(desc).append("\n\n");
}
return sb.toString().stripTrailing();
}
@Override
public String activityDescription(Map<String, Object> input) {
return "🔍 Searching tools: " + input.getOrDefault("query", "*");
}
private String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
Loading…
Cancel
Save