From 7f8f1ef387f0ca436f079db013b72d90257da1df Mon Sep 17 00:00:00 2001 From: kongweiguang <240524885@qq.com> Date: Wed, 6 May 2026 19:06:55 +0800 Subject: [PATCH 1/3] feat(harness): add projectWorkspace for filesystem tools --- .../harness/agent/HarnessAgent.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java index b889e9edf..c771f4b32 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java @@ -40,6 +40,7 @@ import io.agentscope.core.tool.Toolkit; import io.agentscope.harness.agent.filesystem.AbstractFilesystem; import io.agentscope.harness.agent.filesystem.AbstractSandboxFilesystem; +import io.agentscope.harness.agent.filesystem.LocalFilesystem; import io.agentscope.harness.agent.filesystem.LocalFilesystemSpec; import io.agentscope.harness.agent.filesystem.LocalFilesystemWithShell; import io.agentscope.harness.agent.filesystem.RemoteFilesystemSpec; @@ -435,6 +436,7 @@ public static class Builder { // Harness-specific params private Path workspace; + private Path projectWorkspace; private String environmentMemory; private AbstractFilesystem abstractFilesystem; private Session session; @@ -558,6 +560,23 @@ public Builder workspace(Path workspace) { return this; } + /** + * Sets the project workspace directory exposed to the built-in file tools. + * + *

{@link #workspace(Path)} remains the harness workspace used for {@code AGENTS.md}, + * memory, sessions, skills, and other agent state. When this option is set, built-in file + * tools such as {@code read_file}, {@code write_file}, {@code edit_file}, {@code grep_files} + * and {@code glob_files} operate against this project directory instead, without applying + * the per-user namespace that is used by the harness workspace backend. + * + *

This is useful for applications that keep agent state in one stable directory but need + * the agent to work on arbitrary project folders. + */ + public Builder projectWorkspace(Path projectWorkspace) { + this.projectWorkspace = projectWorkspace; + return this; + } + public Builder environmentMemory(String environmentMemory) { this.environmentMemory = environmentMemory; return this; @@ -854,6 +873,8 @@ public HarnessAgent build() { AtomicReference sessionIdRef = new AtomicReference<>(); AbstractFilesystem filesystem = resolveFilesystem(resolvedWorkspace, resolvedAgentId, userIdRef, sessionIdRef); + AbstractFilesystem toolFilesystem = + resolveProjectFilesystem(projectWorkspace, filesystem); // ---- Sandbox integration ---- SandboxLifecycleHook sandboxLifecycleHook = null; @@ -933,7 +954,7 @@ public HarnessAgent build() { } if (toolResultEvictionConfig != null) { - allHooks.add(new ToolResultEvictionHook(filesystem, toolResultEvictionConfig)); + allHooks.add(new ToolResultEvictionHook(toolFilesystem, toolResultEvictionConfig)); } SessionPersistenceHook sessionPersistenceHook = new SessionPersistenceHook(); @@ -957,7 +978,7 @@ public HarnessAgent build() { agentToolkit.registerTool(getTool); agentToolkit.registerTool(new SessionSearchTool(wsManager)); - agentToolkit.registerTool(new FilesystemTool(filesystem)); + agentToolkit.registerTool(new FilesystemTool(toolFilesystem)); if (filesystem instanceof AbstractSandboxFilesystem sandbox) { agentToolkit.registerTool(new ShellExecuteTool(sandbox)); @@ -1091,6 +1112,14 @@ private AbstractFilesystem resolveFilesystem( return new LocalFilesystemWithShell(workspace, nsFactory); } + private AbstractFilesystem resolveProjectFilesystem( + Path projectWorkspace, AbstractFilesystem workspaceFilesystem) { + if (projectWorkspace == null) { + return workspaceFilesystem; + } + return new LocalFilesystem(projectWorkspace, true, 10); + } + private void validateDistributedSandboxConfig( Session effectiveSession, SandboxContext sandboxContext) { if (sandboxStateStore == null && effectiveSession instanceof WorkspaceSession) { From 2c7b301546056cf4d8f67103d066dae46c4cd96e Mon Sep 17 00:00:00 2001 From: kongweiguang <240524885@qq.com> Date: Mon, 11 May 2026 10:53:11 +0800 Subject: [PATCH 2/3] fix harness sandbox file tool backend --- .../harness/agent/HarnessAgent.java | 10 ++- .../harness/agent/HarnessAgentTest.java | 63 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java index 88c8208ef..76c86bb27 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java @@ -592,7 +592,7 @@ public Builder workspace(Path workspace) { this.workspace = workspace; return this; } - + /** * Sets the project workspace directory exposed to the built-in file tools. * @@ -933,8 +933,6 @@ public HarnessAgent build() { AtomicReference sessionIdRef = new AtomicReference<>(); AbstractFilesystem filesystem = resolveFilesystem(resolvedWorkspace, resolvedAgentId, userIdRef, sessionIdRef); - AbstractFilesystem toolFilesystem = - resolveProjectFilesystem(projectWorkspace, filesystem); // ---- Sandbox integration ---- SandboxLifecycleHook sandboxLifecycleHook = null; @@ -968,6 +966,8 @@ public HarnessAgent build() { defaultSandboxContext.getClient(), stateStore, resolvedAgentId); sandboxLifecycleHook = new SandboxLifecycleHook(sandboxManager, capturedSandboxFs); } + AbstractFilesystem toolFilesystem = + resolveProjectFilesystem(projectWorkspace, filesystem); WorkspaceManager wsManager = new WorkspaceManager(resolvedWorkspace, filesystem); wsManager.validate(); @@ -1250,6 +1250,7 @@ private SubagentFactory buildGeneralPurposeFactory( final List capturedHooks = List.copyOf(this.hooks); final AgentSkillRepository capturedSkillRepo = this.skillRepository; final boolean capturedUseLegacyXmlWorkspaceContext = this.useLegacyXmlWorkspaceContext; + final Path capturedProjectWorkspace = this.projectWorkspace; return () -> { Builder sub = @@ -1267,6 +1268,9 @@ private SubagentFactory buildGeneralPurposeFactory( if (capturedSkillRepo != null) { sub.skillRepository(capturedSkillRepo); } + if (capturedProjectWorkspace != null) { + sub.projectWorkspace(capturedProjectWorkspace); + } if (capturedBackend != null) { sub.abstractFilesystem(capturedBackend); } diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java index f3f3ca386..c10bc944c 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java @@ -27,13 +27,19 @@ import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; import io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ToolResultBlock; +import io.agentscope.core.message.ToolUseBlock; import io.agentscope.core.model.ChatResponse; import io.agentscope.core.model.Model; import io.agentscope.core.model.ToolSchema; import io.agentscope.core.session.Session; +import io.agentscope.core.tool.ToolCallParam; +import io.agentscope.core.util.JsonUtils; import io.agentscope.harness.agent.filesystem.LocalFilesystem; import io.agentscope.harness.agent.filesystem.RemoteFilesystemSpec; import io.agentscope.harness.agent.hook.SubagentsHook.SubagentEntry; +import io.agentscope.harness.agent.sandbox.SandboxDistributedOptions; +import io.agentscope.harness.agent.sandbox.filesystem.DockerFilesystemSpec; import io.agentscope.harness.agent.store.InMemoryStore; import io.agentscope.harness.agent.workspace.WorkspaceConstants; import java.nio.file.Files; @@ -217,6 +223,52 @@ void remoteFilesystemSpec_sharesMemoryMdInNonsandboxMode() throws Exception { store.get(List.of("agents", "agent-a", "users", "_default"), "/MEMORY.md") != null); } + @Test + void sandboxFilesystemMode_fileToolsUseSandboxBackendByDefault() throws Exception { + Files.createDirectories(workspace); + Path localTarget = workspace.resolve("should-not-be-local.txt"); + + HarnessAgent agent = + HarnessAgent.builder() + .name("agent") + .model(stubModel("ok")) + .workspace(workspace) + .filesystem(new DockerFilesystemSpec()) + .sandboxDistributed( + SandboxDistributedOptions.builder() + .requireDistributed(false) + .build()) + .build(); + Map writeInput = + Map.of("path", "/should-not-be-local.txt", "content", "sandbox-only"); + + ToolResultBlock result = + agent.getDelegate() + .getToolkit() + .callTool( + ToolCallParam.builder() + .toolUseBlock( + ToolUseBlock.builder() + .id("call-write") + .name("write_file") + .input(writeInput) + .content( + JsonUtils.getJsonCodec() + .toJson(writeInput)) + .build()) + .input(writeInput) + .build()) + .block(); + + String text = joinToolResultText(result); + assertTrue( + text.contains("No active sandbox") || text.contains("sandbox filesystem"), + "file tool should use the sandbox proxy outside a sandbox call context: " + text); + assertTrue( + Files.notExists(localTarget), + "sandbox file tool must not fall back to the local workspace"); + } + private static Msg userText(String text) { return Msg.builder() .role(MsgRole.USER) @@ -228,6 +280,17 @@ private static String joinAllText(List msgs) { return msgs.stream().map(Msg::getTextContent).collect(Collectors.joining("\n")); } + private static String joinToolResultText(ToolResultBlock result) { + if (result == null) { + return ""; + } + return result.getOutput().stream() + .filter(TextBlock.class::isInstance) + .map(TextBlock.class::cast) + .map(TextBlock::getText) + .collect(Collectors.joining("\n")); + } + private static Model stubModel(String assistantText) { Model model = mock(Model.class); when(model.getModelName()).thenReturn("stub-model"); From d6d691bd00da6fd566415f6fe2f1cce87d73b8ba Mon Sep 17 00:00:00 2001 From: kongweiguang <240524885@qq.com> Date: Mon, 11 May 2026 11:08:29 +0800 Subject: [PATCH 3/3] test harness project workspace inheritance --- .../harness/agent/HarnessAgentTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java index c10bc944c..404e75907 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.agentscope.core.agent.Agent; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; @@ -269,6 +270,46 @@ void sandboxFilesystemMode_fileToolsUseSandboxBackendByDefault() throws Exceptio "sandbox file tool must not fall back to the local workspace"); } + @Test + void generalPurposeSubagent_inheritsProjectWorkspaceForFileTools() throws Exception { + Files.createDirectories(workspace); + Path projectWorkspace = Files.createTempDirectory("agentscope-project-workspace"); + Path projectTarget = projectWorkspace.resolve("from-subagent.txt"); + Path stateWorkspaceTarget = workspace.resolve("from-subagent.txt"); + + List entries = + HarnessAgent.builder() + .name("main") + .model(stubModel("ok")) + .workspace(workspace) + .projectWorkspace(projectWorkspace) + .buildSubagentEntries(workspace); + + Agent createdAgent = + entries.stream() + .filter(entry -> entry.name().equals("general-purpose")) + .findFirst() + .orElseThrow() + .factory() + .create(); + assertTrue(createdAgent instanceof HarnessAgent); + HarnessAgent subagent = (HarnessAgent) createdAgent; + + Map writeInput = + Map.of("path", "/from-subagent.txt", "content", "subagent-project"); + ToolResultBlock result = callTool(subagent, "write_file", writeInput); + + assertTrue( + joinToolResultText(result).contains("Written to /from-subagent.txt"), + "subagent file tool should write successfully: " + joinToolResultText(result)); + assertTrue( + Files.readString(projectTarget).contains("subagent-project"), + "subagent file tool should use inherited projectWorkspace"); + assertTrue( + Files.notExists(stateWorkspaceTarget), + "subagent file tool must not write into the Harness state workspace"); + } + private static Msg userText(String text) { return Msg.builder() .role(MsgRole.USER) @@ -280,6 +321,24 @@ private static String joinAllText(List msgs) { return msgs.stream().map(Msg::getTextContent).collect(Collectors.joining("\n")); } + private static ToolResultBlock callTool( + HarnessAgent agent, String toolName, Map input) { + return agent.getDelegate() + .getToolkit() + .callTool( + ToolCallParam.builder() + .toolUseBlock( + ToolUseBlock.builder() + .id("call-" + toolName) + .name(toolName) + .input(input) + .content(JsonUtils.getJsonCodec().toJson(input)) + .build()) + .input(input) + .build()) + .block(); + } + private static String joinToolResultText(ToolResultBlock result) { if (result == null) { return "";