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 "";