From e88be95cd235d9a5f1d4c1daff038904c745a981 Mon Sep 17 00:00:00 2001 From: matevip Date: Mon, 24 Aug 2026 02:43:57 -0400 Subject: [PATCH] fix: pass workspace context to tool guard (#617) --- .../graph/executor/ToolExecutionExecutor.java | 10 +++ ...tionExecutorWorkspaceGuardContextTest.java | 72 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 mateclaw-server/src/test/java/vip/mate/agent/graph/executor/ToolExecutionExecutorWorkspaceGuardContextTest.java diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolExecutionExecutor.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolExecutionExecutor.java index 86c02be2..9e0f0663 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolExecutionExecutor.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolExecutionExecutor.java @@ -483,6 +483,12 @@ public class ToolExecutionExecutor { ChatOrigin origin, Set loadedSkills) { ChatOrigin safeOrigin = origin != null ? origin : ChatOrigin.EMPTY; + if (isBlank(safeOrigin.conversationId()) && !isBlank(conversationId)) { + safeOrigin = safeOrigin.withConversationId(conversationId); + } + if (isBlank(safeOrigin.workspaceBasePath()) && !isBlank(workspaceBasePath)) { + safeOrigin = safeOrigin.withWorkspace(safeOrigin.workspaceId(), workspaceBasePath); + } // Reset per-turn audit dedupe state. A retried denied tool inside the // same turn writes a single audit row; the set is repopulated by the // denial branch below. @@ -1297,6 +1303,10 @@ public class ToolExecutionExecutor { return GuardDecision.allowed(); } + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + /** * Deny an approval-required tool when the run is non-interactive (no human can * approve), returning an actionable message so the agent falls back to a diff --git a/mateclaw-server/src/test/java/vip/mate/agent/graph/executor/ToolExecutionExecutorWorkspaceGuardContextTest.java b/mateclaw-server/src/test/java/vip/mate/agent/graph/executor/ToolExecutionExecutorWorkspaceGuardContextTest.java new file mode 100644 index 00000000..d45db569 --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/agent/graph/executor/ToolExecutionExecutorWorkspaceGuardContextTest.java @@ -0,0 +1,72 @@ +package vip.mate.agent.graph.executor; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.definition.ToolDefinition; +import org.springframework.ai.tool.metadata.ToolMetadata; +import vip.mate.agent.AgentToolSet; +import vip.mate.agent.context.ChatOrigin; +import vip.mate.tool.guard.model.GuardEvaluation; +import vip.mate.tool.guard.model.ToolInvocationContext; +import vip.mate.tool.guard.service.ToolGuardService; + +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ToolExecutionExecutorWorkspaceGuardContextTest { + + @Test + @DisplayName("guard evaluation receives state workspaceBasePath when origin has no base path (#617)") + void guardUsesWorkspaceBasePathArgumentWhenOriginIsBlank(@TempDir Path workspaceRoot) { + ToolGuardService guardService = mock(ToolGuardService.class); + when(guardService.evaluate(any(ToolInvocationContext.class), eq(true))) + .thenReturn(GuardEvaluation.allow("write_file")); + ToolExecutionExecutor executor = new ToolExecutionExecutor( + AgentToolSet.fromCallbacks(List.of(), List.of(stub("write_file"))), + guardService, + null, + null); + + executor.execute( + List.of(new AssistantMessage.ToolCall( + "call_1", "function", "write_file", + "{\"filePath\":\"deck.md\",\"content\":\"# Deck\"}")), + "conv-617", + "agent-617", + false, + "alice", + workspaceRoot.toString(), + ChatOrigin.web("conv-617", "alice", 1L, null)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ToolInvocationContext.class); + verify(guardService).evaluate(captor.capture(), eq(true)); + assertThat(captor.getValue().workspaceBasePath()).isEqualTo(workspaceRoot.toString()); + } + + private static ToolCallback stub(String name) { + ToolDefinition def = ToolDefinition.builder() + .name(name) + .description("test tool " + name) + .inputSchema("{\"type\":\"object\",\"properties\":{}}") + .build(); + ToolMetadata md = ToolMetadata.builder().returnDirect(false).build(); + return new ToolCallback() { + @Override public ToolDefinition getToolDefinition() { return def; } + @Override public ToolMetadata getToolMetadata() { return md; } + @Override public String call(String arguments) { return "ok"; } + @Override public String call(String arguments, ToolContext toolContext) { return "ok"; } + }; + } +}