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
15 changes: 12 additions & 3 deletions ccc-app/src/main/java/com/ccj/app/AppConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,24 @@ public ChatClient chatClient(AppSettings settings, Credentials credentials,
if (result instanceof com.ccj.core.tool.PermissionResult.Deny d) {
return d.message();
}
// Allow 和 Ask 都放行(Ask 在 REPL 层处理,此处简化为允许)
return null;
// 修复 Copilot 审查:Ask 不应被当作 allow 放行,否则绕过用户确认流程
if (result instanceof com.ccj.core.tool.PermissionResult.Ask a) {
return "Permission required: " + a.message();
}
return null; // Allow
} catch (Exception e) {
log.warn("Permission check failed for {}: {}", tool.name(), e.getMessage());
log.warn("Permission check failed for {}: {}", tool.name(), e.getMessage(), e);
return null; // 出错时允许(fail-open)
}
};
// 修复 Copilot 审查:REPL 模式下隐藏原始工具,只暴露 ReplTool 给模型
boolean replMode = com.ccj.tools.builtin.ReplPrimitiveTools.isReplModeEnabled();
java.util.Set<String> hiddenTools = replMode
? com.ccj.tools.builtin.ReplPrimitiveTools.REPL_ONLY_TOOLS
: java.util.Set.of();
java.util.List<org.springframework.ai.tool.ToolCallback> toolCallbacks = toolRegistry.all().stream()
.filter(Tool::isEnabled)
.filter(t -> !hiddenTools.contains(t.name()))
.map(t -> (org.springframework.ai.tool.ToolCallback) new com.ccj.tools.adapter.ToolCallbackAdapter(t, ctxTemplate, permChecker))
.toList();
log.info("Registered {} tool callbacks on ChatClient", toolCallbacks.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import org.springframework.ai.chat.prompt.Prompt;

import java.util.Optional;
import java.util.function.Consumer;

/**
* Agent 调用器。替换原 ClaudeCodeJavaApplication 中的 Function&lt;String,String&gt; 裸 lambda。
Expand Down
5 changes: 5 additions & 0 deletions ccc-core/src/main/java/com/ccj/core/tool/ToolUseContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public record ToolUseContext(
session = session == null ? Session.inMemory() : session;
}

/** 默认上下文(所有字段使用默认值)。 */
public static ToolUseContext defaultContext() {
return new ToolUseContext(null, null, null, null, null, null);
}

/** 中止能力抽象(对应 abortController)。 */
@FunctionalInterface
public interface Cancellable {
Expand Down
18 changes: 10 additions & 8 deletions ccc-cron/src/main/java/com/ccj/cron/CronScheduler.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,18 @@ private void check() {
log.info("Cron task {} fired: {}", task.id(), task.prompt().substring(0, Math.min(50, task.prompt().length())));

// Phase 2.2: 按 agentId 路由
if (onFireTask != null) {
onFireTask.accept(task);
} else {
onFire.accept(task.prompt());
}
// 同时通知 lead 队列(teammate 任务也通知 lead,便于感知)
// 修复 Copilot 审查:原实现对非 teammate 任务调用了两次 onFire
if (task.isTeammateTask()) {
// teammate 任务:onFireTask 负责路由到 teammate 队列
// 此处不再调 onFire(避免重复入队)
// teammate 任务:通过 onFireTask 路由到 teammate 队列
if (onFireTask != null) {
onFireTask.accept(task);
}
// 不再调 onFire(避免 lead 队列重复入队)
} else {
// lead 任务:通知 lead 队列
if (onFireTask != null) {
onFireTask.accept(task); // onFireTask 可做额外处理(如日志)
}
onFire.accept(task.prompt());
}

Expand Down
4 changes: 3 additions & 1 deletion ccc-cron/src/main/java/com/ccj/cron/CronTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ public CronCreateTool(CronTaskStore store, CronScheduler scheduler) {

@Override
public Map<String, Object> inputSchema() {
// 修复 Copilot 审查:agentId 在 call() 中被读取但未在 inputSchema 声明
return Map.of(
"type", "object",
"properties", Map.of(
"cron", Map.of("type", "string", "description", "5-field cron expression, e.g. '*/5 * * * *'"),
"prompt", Map.of("type", "string", "description", "Prompt to run when triggered"),
"recurring", Map.of("type", "boolean", "description", "true=recurring, false=one-shot", "default", true),
"durable", Map.of("type", "boolean", "description", "true=persist across restarts", "default", false)
"durable", Map.of("type", "boolean", "description", "true=persist across restarts", "default", false),
"agentId", Map.of("type", "string", "description", "Teammate agent ID to route the task to (optional, null for lead tasks)")
),
"required", List.of("cron", "prompt")
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,11 @@ private void watchLoop() {

/** 处理文件事件。 */
private void handleFileEvent(Path filePath, String eventKind) {
// 检查是否匹配任一监听路径
// 修复 Copilot 审查:原实现仅按文件名匹配,忽略目录,可能导致跨目录误触发。
// 现在按完整路径匹配(watchedPaths 存储的是完整解析路径)。
Path normalized = filePath.toAbsolutePath().normalize();
boolean matched = watchedPaths.stream()
.anyMatch(wp -> wp.getFileName() != null
&& wp.getFileName().toString().equals(
filePath.getFileName() != null ? filePath.getFileName().toString() : ""));
.anyMatch(wp -> wp.toAbsolutePath().normalize().equals(normalized));
if (!matched) return;

String eventName = switch (eventKind) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ public CompletableFuture<String> runAsync(String taskPrompt, List<Message> paren
* @return ForkedAgentResult(text + usage)
*/
public ForkedAgentResult runForked(String taskPrompt, List<Message> parentHistory) {
log.debug("ForkedAgent running task ({} parent messages)", parentHistory.size());
// 修复 Copilot 审查:原实现在 null 检查前访问 parentHistory.size(),导致 NPE
int historySize = parentHistory != null ? parentHistory.size() : 0;
log.debug("ForkedAgent running task ({} parent messages)", historySize);
try {
var spec = parentClient.prompt();
if (systemPrompt != null && !systemPrompt.isBlank()) {
Expand Down
20 changes: 17 additions & 3 deletions ccc-repl/src/main/java/com/ccj/repl/dialog/PermissionDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class PermissionDialog implements StreamingToolExecutor.PermissionCallbac
private static final Logger log = LoggerFactory.getLogger(PermissionDialog.class);

private final Terminal terminal;
// 修复 Copilot 审查:并发工具调用可能同时请求权限,用 synchronized 串行化权限提示
private final BlockingQueue<PermissionResponse> responseQueue = new LinkedBlockingQueue<>(1);

public PermissionDialog(Terminal terminal) {
Expand All @@ -42,10 +43,15 @@ public PermissionDialog(Terminal terminal) {
* 渲染权限请求并等待用户响应。
* 由 agent 线程调用(通过 StreamingToolExecutor.PermissionCallback.resolve)。
* 会阻塞直到用户响应。
*
* 修复 Copilot 审查:synchronized 防止并发工具调用同时渲染提示和竞争 responseQueue。
*/
@Override
public PermissionResult resolve(ContentBlock.ToolUseBlock block, Tool tool,
public synchronized PermissionResult resolve(ContentBlock.ToolUseBlock block, Tool tool,
PermissionResult.Ask askResult) {
// 清除可能残留的旧响应(防止上一次 offer 的响应被本次消费)
responseQueue.clear();

// 渲染提示
renderPermissionRequest(block, tool, askResult);

Expand Down Expand Up @@ -82,9 +88,17 @@ private void renderPermissionRequest(ContentBlock.ToolUseBlock block, Tool tool,
w.flush();
}

/** 用户响应(由 REPL 主线程读取按键后调用)。 */
/**
* 用户响应(由 REPL 主线程读取按键后调用)。
* 修复 Copilot 审查:先 clear 再 put,防止用户多次按键时响应被 offer 静默丢弃。
*/
public void provideResponse(PermissionResponse response) {
responseQueue.offer(response);
responseQueue.clear();
try {
responseQueue.put(response);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

/** 将用户响应转为 PermissionResult。 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
* 让 Spring AI 的 ToolCallingAdvisor 工具调用循环能驱动我们的 Tool 实现。
* Spring AI 调用 call(toolInputJson, toolContext) 时:
* 1. 反序列化 JSON 为 input Map
* 2. 构造 ToolUseContext(从 toolContext 取工作目录等)
* 3. 调用 tool.call(input, context)
* 4. 序列化结果为 JSON 字符串返回
* 2. 如果配置了 PermissionChecker,先检查权限(deny 则返回错误,不执行工具)
* 3. 构造 ToolUseContext(从 toolContext 取工作目录等)
* 4. 调用 tool.call(input, context)
* 5. 序列化结果为 JSON 字符串返回
*
* 注意:权限检查和 Hook 在此适配器之外(由 ToolExecutionPipeline
* PermissionAwareToolCallingAdvisor 处理)。此适配器只做纯执行委托
* 权限检查在适配器内部通过 PermissionChecker 执行。Hook 由 ToolExecutionPipeline
* 在非 Spring AI 路径处理
*/
public class ToolCallbackAdapter implements ToolCallback {

Expand All @@ -51,7 +52,8 @@ public ToolCallbackAdapter(Tool tool, ToolUseContext contextTemplate) {

public ToolCallbackAdapter(Tool tool, ToolUseContext contextTemplate, PermissionChecker permissionChecker) {
this.tool = tool;
this.contextTemplate = contextTemplate;
// 修复 Copilot 审查:null contextTemplate 会导致 tool.call 收到 null,引发 NPE
this.contextTemplate = contextTemplate != null ? contextTemplate : ToolUseContext.defaultContext();
this.permissionChecker = permissionChecker;
}

Expand Down Expand Up @@ -89,7 +91,8 @@ public String call(String toolInput, ToolContext toolContext) {
}
} catch (Exception e) {
// 权限检查器异常:fail-open(记录但不阻断)
log.warn("Permission checker exception for {}: {}", tool.name(), e.getMessage());
// 修复 Copilot 审查:传入异常对象本身以记录完整堆栈
log.warn("Permission checker exception for {}: {}", tool.name(), e.getMessage(), e);
}
}

Expand Down