feat: 多层上下文压缩 + 多级权限管理系统

权限管理:
- PermissionTypes: 权限行为/模式/规则/决策/选择类型系统
- PermissionSettings: 用户级/项目级/会话级权限持久化(settings.json)
- PermissionRuleEngine: 多层规则引擎(deny→allow→readOnly→mode→dangerous→ASK)
- DangerousPatterns: 危险shell命令检测(rm -rf, eval, exec等)
- ReplSession: Y/A/N/D四选项权限确认UI
- ConfigCommand: permission-mode/list/reset子命令
- AgentLoop: 集成规则引擎,PermissionChoice回调

上下文压缩:
- TokenTracker: 上下文窗口监控(93%自动压缩/82%警告/98%阻塞)
- MicroCompact: 微压缩,裁剪旧tool_result(无API调用)
- SessionMemoryCompact: Session Memory压缩,AI摘要旧消息保留近期段
- FullCompact: 全量压缩+PTL重试(按API Round逐步丢弃)
- AutoCompactManager: 压缩编排器(micro→session→full),含熔断机制
- CompactCommand: 改造为委托FullCompact
- StatusLine: token使用百分比+颜色告警(绿→黄→红→闪烁)
- AppConfig: 注册PermissionSettings/RuleEngine/AutoCompactManager Bean

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
liuzh
2026-04-02 23:05:37 +08:00
co-authored by Copilot
parent c989d3b451
commit 730551cb3f
16 changed files with 1759 additions and 127 deletions
@@ -0,0 +1,105 @@
package com.claudecode.permission;
import java.util.List;
import java.util.Set;
/**
* 危险命令模式检测 —— 识别可能有害的 shell 命令。
* <p>
* 即使在 BYPASS 模式下也会对这些命令发出警告。
*/
public final class DangerousPatterns {
private DangerousPatterns() {}
/** 危险 shell 命令前缀(不区分大小写匹配) */
private static final List<String> DANGEROUS_BASH_PREFIXES = List.of(
"rm -rf /",
"rm -rf ~",
"rm -rf .",
"rm -r /",
"rmdir /s",
"del /f /s /q",
"format ",
"mkfs.",
"dd if=",
"> /dev/sda",
"chmod -R 777 /",
"chown -R",
":(){:|:&};:" // fork bomb
);
/** 危险代码执行模式 */
private static final List<String> CODE_EXECUTION_PATTERNS = List.of(
"eval ",
"exec ",
"python -c",
"python3 -c",
"node -e",
"ruby -e",
"perl -e",
"| sh",
"| bash",
"| zsh",
"| powershell",
"| pwsh",
"curl | sh",
"wget | sh",
"Invoke-Expression",
"iex ",
"Start-Process",
"Add-Type"
);
/** 在规则匹配中应自动拒绝的工具级通配符 */
private static final Set<String> DANGEROUS_TOOL_WILDCARDS = Set.of(
"Bash", // 不应允许所有 bash 命令
"Bash(*)",
"PowerShell",
"PowerShell(*)"
);
/**
* 检测命令是否包含危险模式
*
* @param command shell 命令文本
* @return 如果危险返回原因描述,否则返回 null
*/
public static String detectDangerous(String command) {
if (command == null || command.isBlank()) return null;
String lower = command.toLowerCase().trim();
for (String prefix : DANGEROUS_BASH_PREFIXES) {
if (lower.startsWith(prefix.toLowerCase()) || lower.contains(prefix.toLowerCase())) {
return "Dangerous command detected: " + prefix.trim();
}
}
for (String pattern : CODE_EXECUTION_PATTERNS) {
if (lower.contains(pattern.toLowerCase())) {
return "Code execution pattern detected: " + pattern.trim();
}
}
return null;
}
/**
* 检测是否为危险的工具级通配符规则
* <p>
* 用于防止用户添加过于宽泛的 "always allow" 规则。
*/
public static boolean isDangerousWildcard(String ruleStr) {
return DANGEROUS_TOOL_WILDCARDS.contains(ruleStr);
}
/**
* 获取危险原因的简短描述
*/
public static String getDangerLevel(String command) {
String reason = detectDangerous(command);
if (reason == null) return "LOW";
if (reason.contains("Dangerous command")) return "HIGH";
return "MEDIUM";
}
}
@@ -0,0 +1,173 @@
package com.claudecode.permission;
import com.claudecode.permission.PermissionTypes.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 权限规则引擎 —— 根据规则、模式和工具属性做出权限决策。
* <p>
* 决策流程:
* <ol>
* <li>检查全局模式(BYPASS → 全部允许,DONT_ASK → 拒绝需确认的)</li>
* <li>检查 alwaysDeny 规则 → 匹配则 DENY</li>
* <li>检查 alwaysAllow 规则 → 匹配则 ALLOW</li>
* <li>只读工具 → ALLOW</li>
* <li>ACCEPT_EDITS 模式下文件操作 → ALLOW</li>
* <li>检查危险命令 → 强制 ASK</li>
* <li>默认 → ASK</li>
* </ol>
*/
public class PermissionRuleEngine {
private static final Set<String> FILE_EDIT_TOOLS = Set.of("Write", "Edit", "NotebookEdit");
private static final Set<String> READ_ONLY_TOOLS = Set.of(
"Read", "Glob", "Grep", "ListFiles", "WebFetch", "WebSearch",
"TodoRead", "TaskGet", "TaskList", "AskUserQuestion"
);
private final PermissionSettings settings;
public PermissionRuleEngine(PermissionSettings settings) {
this.settings = settings;
}
/**
* 评估工具调用的权限
*
* @param toolName 工具名称
* @param input 工具参数
* @param isReadOnly 工具是否为只读
* @return 权限决策
*/
public PermissionDecision evaluate(String toolName, Map<String, Object> input, boolean isReadOnly) {
PermissionMode mode = settings.getCurrentMode();
// BYPASS 模式:全部允许
if (mode == PermissionMode.BYPASS) {
return PermissionDecision.allow("Bypass mode enabled");
}
// 获取命令内容(用于 Bash/PowerShell 的命令匹配)
String command = extractCommand(toolName, input);
// 检查所有持久化规则
List<PermissionRule> rules = settings.getAllRules();
// 1. 检查 alwaysDeny 规则
for (var rule : rules) {
if (rule.behavior() == PermissionBehavior.DENY && matchesRule(rule, toolName, command)) {
return PermissionDecision.deny("Denied by rule: " + PermissionSettings.formatRule(rule));
}
}
// 2. 检查 alwaysAllow 规则
for (var rule : rules) {
if (rule.behavior() == PermissionBehavior.ALLOW && matchesRule(rule, toolName, command)) {
return PermissionDecision.allow("Allowed by rule: " + PermissionSettings.formatRule(rule));
}
}
// 3. 只读工具直接放行
if (isReadOnly || READ_ONLY_TOOLS.contains(toolName)) {
return PermissionDecision.allow("Read-only tool");
}
// 4. ACCEPT_EDITS 模式:文件操作工具自动允许
if (mode == PermissionMode.ACCEPT_EDITS && FILE_EDIT_TOOLS.contains(toolName)) {
return PermissionDecision.allow("File edits auto-allowed in accept-edits mode");
}
// 5. DONT_ASK 模式:自动拒绝
if (mode == PermissionMode.DONT_ASK) {
return PermissionDecision.deny("Auto-denied in dont-ask mode");
}
// 6. 检查危险命令(强制 ASK,附带警告)
if (command != null) {
String danger = DangerousPatterns.detectDangerous(command);
if (danger != null) {
String prefix = extractCommandPrefix(command);
return new PermissionDecision(
PermissionBehavior.ASK,
"⚠ DANGEROUS: " + danger,
toolName, prefix, List.of()
);
}
}
// 7. 默认:需要用户确认
String prefix = extractCommandPrefix(command);
return PermissionDecision.ask(toolName, prefix);
}
/**
* 根据用户选择应用权限变更
*/
public void applyChoice(PermissionChoice choice, String toolName, String command) {
String prefix = extractCommandPrefix(command);
switch (choice) {
case ALWAYS_ALLOW -> {
var rule = prefix != null
? PermissionRule.forCommand(toolName, prefix, PermissionBehavior.ALLOW)
: PermissionRule.forTool(toolName, PermissionBehavior.ALLOW);
// 检查是否为危险通配符
String ruleStr = PermissionSettings.formatRule(rule);
if (!DangerousPatterns.isDangerousWildcard(ruleStr)) {
settings.addUserRule(rule);
}
}
case ALWAYS_DENY -> {
var rule = prefix != null
? PermissionRule.forCommand(toolName, prefix, PermissionBehavior.DENY)
: PermissionRule.forTool(toolName, PermissionBehavior.DENY);
settings.addUserRule(rule);
}
case ALLOW_ONCE, DENY_ONCE -> {
// 单次操作,不持久化
}
}
}
// ── 内部匹配方法 ──
/** 检查规则是否匹配当前工具和命令 */
boolean matchesRule(PermissionRule rule, String toolName, String command) {
// 工具名不匹配直接跳过
if (!rule.toolName().equalsIgnoreCase(toolName)) return false;
String content = rule.ruleContent();
// 通配符 * 匹配所有命令
if ("*".equals(content)) return true;
// 前缀匹配模式:npm:* 匹配以 "npm" 开头的命令
if (content.endsWith(":*") && command != null) {
String prefix = content.substring(0, content.length() - 2);
return command.toLowerCase().startsWith(prefix.toLowerCase());
}
// 精确匹配
return content.equalsIgnoreCase(command);
}
/** 从工具参数中提取命令文本 */
private String extractCommand(String toolName, Map<String, Object> input) {
if (input == null) return null;
return switch (toolName) {
case "Bash" -> (String) input.get("command");
case "Write" -> (String) input.get("file_path");
case "Edit" -> (String) input.get("file_path");
default -> null;
};
}
/** 提取命令前缀(第一个空格前的部分) */
private String extractCommandPrefix(String command) {
if (command == null || command.isBlank()) return null;
String trimmed = command.trim();
int space = trimmed.indexOf(' ');
return space > 0 ? trimmed.substring(0, space) : trimmed;
}
}
@@ -0,0 +1,200 @@
package com.claudecode.permission;
import com.claudecode.permission.PermissionTypes.PermissionBehavior;
import com.claudecode.permission.PermissionTypes.PermissionRule;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* 权限设置持久化 —— 管理用户级和项目级权限规则文件。
* <p>
* 存储结构:
* <ul>
* <li>用户级: ~/.claude-code-java/settings.json</li>
* <li>项目级: .claude-code-java/settings.json</li>
* </ul>
* 加载优先级: 项目级 > 用户级 > 会话级
*/
public class PermissionSettings {
private static final String SETTINGS_DIR = ".claude-code-java";
private static final String SETTINGS_FILE = "settings.json";
private static final ObjectMapper MAPPER = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
/** 内存中的合并规则(从所有来源加载后合并) */
private final List<PermissionRule> sessionRules = new ArrayList<>();
private PermissionTypes.PermissionMode currentMode = PermissionTypes.PermissionMode.DEFAULT;
private final Path userSettingsPath;
private final Path projectSettingsPath;
private SettingsData userData = new SettingsData();
private SettingsData projectData = new SettingsData();
public PermissionSettings() {
this(Path.of(System.getProperty("user.home")),
Path.of(System.getProperty("user.dir")));
}
public PermissionSettings(Path userHome, Path projectDir) {
this.userSettingsPath = userHome.resolve(SETTINGS_DIR).resolve(SETTINGS_FILE);
this.projectSettingsPath = projectDir.resolve(SETTINGS_DIR).resolve(SETTINGS_FILE);
}
/** 从磁盘加载所有设置 */
public void load() {
userData = loadFromFile(userSettingsPath);
projectData = loadFromFile(projectSettingsPath);
// 项目级模式优先
if (projectData.permissions.mode != null) {
currentMode = projectData.permissions.mode;
} else if (userData.permissions.mode != null) {
currentMode = userData.permissions.mode;
}
}
/** 获取所有合并后的规则(项目级 > 用户级 > 会话级) */
public List<PermissionRule> getAllRules() {
var rules = new ArrayList<PermissionRule>();
// 项目级优先
rules.addAll(toRules(projectData.permissions.alwaysAllow, PermissionBehavior.ALLOW));
rules.addAll(toRules(projectData.permissions.alwaysDeny, PermissionBehavior.DENY));
// 用户级
rules.addAll(toRules(userData.permissions.alwaysAllow, PermissionBehavior.ALLOW));
rules.addAll(toRules(userData.permissions.alwaysDeny, PermissionBehavior.DENY));
// 会话级
rules.addAll(sessionRules);
return rules;
}
/** 添加规则并保存到用户级设置 */
public void addUserRule(PermissionRule rule) {
if (rule.behavior() == PermissionBehavior.ALLOW) {
userData.permissions.alwaysAllow.add(formatRule(rule));
} else if (rule.behavior() == PermissionBehavior.DENY) {
userData.permissions.alwaysDeny.add(formatRule(rule));
}
saveToFile(userSettingsPath, userData);
}
/** 添加规则到会话级(不持久化) */
public void addSessionRule(PermissionRule rule) {
sessionRules.add(rule);
}
/** 移除用户级规则 */
public void removeUserRule(String ruleStr) {
userData.permissions.alwaysAllow.remove(ruleStr);
userData.permissions.alwaysDeny.remove(ruleStr);
saveToFile(userSettingsPath, userData);
}
/** 清除所有规则 */
public void clearAll() {
userData.permissions.alwaysAllow.clear();
userData.permissions.alwaysDeny.clear();
projectData.permissions.alwaysAllow.clear();
projectData.permissions.alwaysDeny.clear();
sessionRules.clear();
saveToFile(userSettingsPath, userData);
}
public PermissionTypes.PermissionMode getCurrentMode() {
return currentMode;
}
public void setCurrentMode(PermissionTypes.PermissionMode mode) {
this.currentMode = mode;
userData.permissions.mode = mode;
saveToFile(userSettingsPath, userData);
}
/** 获取所有已保存规则的可读列表 */
public List<String> listRules() {
var result = new ArrayList<String>();
for (var r : userData.permissions.alwaysAllow) {
result.add("[user] ALLOW " + r);
}
for (var r : userData.permissions.alwaysDeny) {
result.add("[user] DENY " + r);
}
for (var r : projectData.permissions.alwaysAllow) {
result.add("[proj] ALLOW " + r);
}
for (var r : projectData.permissions.alwaysDeny) {
result.add("[proj] DENY " + r);
}
for (var r : sessionRules) {
result.add("[sess] " + r.behavior() + " " + formatRule(r));
}
return result;
}
// ── 内部方法 ──
private SettingsData loadFromFile(Path path) {
if (!Files.exists(path)) return new SettingsData();
try {
return MAPPER.readValue(path.toFile(), SettingsData.class);
} catch (IOException e) {
return new SettingsData();
}
}
private void saveToFile(Path path, SettingsData data) {
try {
Files.createDirectories(path.getParent());
MAPPER.writeValue(path.toFile(), data);
} catch (IOException e) {
// 静默失败,不影响主流程
}
}
private List<PermissionRule> toRules(List<String> ruleStrings, PermissionBehavior behavior) {
return ruleStrings.stream()
.map(s -> parseRule(s, behavior))
.toList();
}
/** 解析规则字符串,格式: "ToolName(pattern)" 或 "ToolName" */
static PermissionRule parseRule(String ruleStr, PermissionBehavior behavior) {
int parenStart = ruleStr.indexOf('(');
if (parenStart > 0 && ruleStr.endsWith(")")) {
String toolName = ruleStr.substring(0, parenStart);
String content = ruleStr.substring(parenStart + 1, ruleStr.length() - 1);
return new PermissionRule(toolName, content, behavior);
}
return PermissionRule.forTool(ruleStr, behavior);
}
/** 格式化规则为字符串 */
static String formatRule(PermissionRule rule) {
if ("*".equals(rule.ruleContent())) {
return rule.toolName();
}
return rule.toolName() + "(" + rule.ruleContent() + ")";
}
// ── JSON 数据结构 ──
@JsonIgnoreProperties(ignoreUnknown = true)
static class SettingsData {
public PermissionsBlock permissions = new PermissionsBlock();
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class PermissionsBlock {
public PermissionTypes.PermissionMode mode;
public List<String> alwaysAllow = new ArrayList<>();
public List<String> alwaysDeny = new ArrayList<>();
public List<String> additionalDirectories = new ArrayList<>();
}
}
@@ -0,0 +1,110 @@
package com.claudecode.permission;
import java.util.List;
/**
* 权限管理类型定义 —— 对应 claude-code 中的 permissions.ts。
*/
public final class PermissionTypes {
private PermissionTypes() {}
/** 权限行为 */
public enum PermissionBehavior {
ALLOW, // 允许执行
DENY, // 拒绝执行
ASK // 需要用户确认
}
/** 权限模式 */
public enum PermissionMode {
/** 默认模式:非只读工具需要用户确认 */
DEFAULT,
/** 自动允许文件编辑,shell 命令仍需确认 */
ACCEPT_EDITS,
/** 跳过所有权限检查(不安全) */
BYPASS,
/** 自动拒绝而非询问用户(无头模式) */
DONT_ASK
}
/**
* 权限规则 —— 定义工具和命令模式的权限行为。
* <p>
* 示例:
* <ul>
* <li>{@code PermissionRule("Bash", "npm:*", ALLOW)} — 允许所有 npm 命令</li>
* <li>{@code PermissionRule("Bash", "rm -rf:*", DENY)} — 拒绝 rm -rf</li>
* <li>{@code PermissionRule("Write", "*", ALLOW)} — 允许所有文件写入</li>
* </ul>
*
* @param toolName 工具名称(如 Bash, Write, Edit
* @param ruleContent 规则内容,支持通配符 *(如 "npm:*", "git:*", "*"
* @param behavior 权限行为
*/
public record PermissionRule(
String toolName,
String ruleContent,
PermissionBehavior behavior
) {
/** 匹配整个工具(无命令模式限制) */
public static PermissionRule forTool(String toolName, PermissionBehavior behavior) {
return new PermissionRule(toolName, "*", behavior);
}
/** 匹配工具的特定命令前缀 */
public static PermissionRule forCommand(String toolName, String prefix, PermissionBehavior behavior) {
return new PermissionRule(toolName, prefix + ":*", behavior);
}
}
/** 权限决策结果 */
public record PermissionDecision(
PermissionBehavior behavior,
String reason,
String toolName,
String commandPrefix,
List<PermissionRule> suggestedRules
) {
public static PermissionDecision allow(String reason) {
return new PermissionDecision(PermissionBehavior.ALLOW, reason, null, null, List.of());
}
public static PermissionDecision deny(String reason) {
return new PermissionDecision(PermissionBehavior.DENY, reason, null, null, List.of());
}
public static PermissionDecision ask(String toolName, String commandPrefix) {
// 生成建议规则供用户选择 "always allow"
var suggested = List.of(
PermissionRule.forCommand(toolName, commandPrefix, PermissionBehavior.ALLOW)
);
return new PermissionDecision(PermissionBehavior.ASK, "Requires user confirmation",
toolName, commandPrefix, suggested);
}
public boolean isAllowed() {
return behavior == PermissionBehavior.ALLOW;
}
public boolean isDenied() {
return behavior == PermissionBehavior.DENY;
}
public boolean needsAsk() {
return behavior == PermissionBehavior.ASK;
}
}
/** 权限确认选项(用户在 UI 中的选择) */
public enum PermissionChoice {
/** 允许本次执行 */
ALLOW_ONCE,
/** 始终允许此模式 */
ALWAYS_ALLOW,
/** 拒绝本次执行 */
DENY_ONCE,
/** 始终拒绝此模式 */
ALWAYS_DENY
}
}