diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index a1df4232..a1222fa6 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -1347,6 +1347,15 @@ public class AgentGraphBuilder { return agentOverride; } if (hasWorkspace) { + // Relative override resolves under the workspace root; reject any value + // that escapes it via "../" so attachment/media/tool I/O stays contained. + Path wsRoot = Paths.get(workspaceBase).toAbsolutePath().normalize(); + Path resolved = wsRoot.resolve(agentOverride).normalize(); + if (!resolved.startsWith(wsRoot)) { + throw new IllegalArgumentException( + "Agent workspaceBasePath override must stay inside the workspace root: " + + resolved + " escapes " + wsRoot); + } return Paths.get(workspaceBase).resolve(agentOverride).toString(); } return agentOverride; diff --git a/mateclaw-server/src/main/java/vip/mate/agent/BaseAgent.java b/mateclaw-server/src/main/java/vip/mate/agent/BaseAgent.java index e126020f..f3fe7208 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/BaseAgent.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/BaseAgent.java @@ -1218,12 +1218,14 @@ public abstract class BaseAgent { } /** - * 解析图片文件的绝对路径。 + * Resolve the absolute path of an image file. *
- * 上传文件的存储位置由 {@code ChatUploadLocationResolver} 按优先级解析: - * Agent 的 workspaceBasePath → Workspace 的 basePath → 可配置默认目录 - * ({@code mateclaw.chat.upload.base-dir},默认 {@code data/chat-uploads})。 - * MCP 工具的工作目录可能不同,所以这里直接解析为绝对路径。 + * The storage location of uploaded files is resolved by + * {@code ChatUploadLocationResolver} in priority order: the Agent's + * workspaceBasePath → the Workspace's basePath → a configurable default + * directory ({@code mateclaw.chat.upload.base-dir}, default + * {@code data/chat-uploads}). An MCP tool's working directory may differ, + * so this resolves directly to an absolute path. */ /** * 构建当前用户消息的 UserMessage(含 multimodal 图片注入)。 diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java index 1715e836..a795439e 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java @@ -1665,13 +1665,19 @@ public class ConversationService { } /** - * 清理会话关联的附件文件 + * Clean up the attachment files associated with a conversation. *
- * 遍历所有候选上传根(workspace/agent 感知根 + 默认根),逐一删除 - * 各根下该会话的附件目录。这样无论附件落在新的工作空间目录还是 - * 迁移前的默认目录,都能被清理。 + * Walks every candidate upload root (the workspace/agent-aware root plus the + * default root) and deletes that conversation's attachment directory under + * each, so attachments are removed whether they landed in the new workspace + * directory or the pre-migration default directory. */ public void cleanAttachmentFiles(String conversationId) { + if (conversationId == null || conversationId.isBlank()) { + // A blank id would resolve to the upload root itself and wipe every + // conversation's attachments — never walk/delete a bare root. + return; + } boolean cleanedAny = false; for (Path root : chatUploadLocationResolver.resolveCandidateUploadRoots(conversationId)) { Path dir; diff --git a/mateclaw-server/src/test/java/vip/mate/workspace/core/service/ChatUploadLocationResolverTest.java b/mateclaw-server/src/test/java/vip/mate/workspace/core/service/ChatUploadLocationResolverTest.java index 21b2abd1..900f7078 100644 --- a/mateclaw-server/src/test/java/vip/mate/workspace/core/service/ChatUploadLocationResolverTest.java +++ b/mateclaw-server/src/test/java/vip/mate/workspace/core/service/ChatUploadLocationResolverTest.java @@ -143,6 +143,23 @@ class ChatUploadLocationResolverTest { .resolve(ChatUploadLocationResolver.UPLOAD_SUBDIR)); } + @Test + @DisplayName("relative agent override that escapes the workspace root via ../ falls back to workspace basePath") + void relativeAgentOverrideEscapingWorkspaceFallsBackToWorkspace() { + stubConversation("c5b", 7L, 99L); + Path wsBase = tempDir.resolve("ws-root"); + when(workspaceService.getById(7L)).thenReturn(workspace(7L, wsBase.toString())); + // Relative override climbs out of the workspace root — resolveAgentBasePath + // rejects it; the resolver falls back to the workspace basePath. + when(agentService.getAgent(99L)).thenReturn(agent(99L, "../../escape", 7L)); + + ChatUploadLocationResolver r = resolver(tempDir); + Path root = r.resolveUploadRoot("c5b"); + + assertThat(root).isEqualTo(wsBase.toAbsolutePath().normalize() + .resolve(ChatUploadLocationResolver.UPLOAD_SUBDIR)); + } + @Test @DisplayName("candidate roots: workspace-scoped first, then default (dual-lookup order)") void candidateRootsOrderedScopedThenDefault() {