From 220a82af8a8592b8d168dc4d07a8b54e7e94529f Mon Sep 17 00:00:00 2001 From: BeginnerCong Date: Sun, 12 Jul 2026 19:11:59 +0800 Subject: [PATCH] fix(harness): prevent LocalFilesystemSpec from falling back to user.dir When project is not set, LocalFilesystemSpec now defaults to the agent workspace instead of System.getProperty(user.dir). This prevents unintended exposure of the JVM working directory in non-CLI scenarios (web apps, embedded agents). Fixes #2065 --- .../agent/HarnessAgentBuilderSupport.java | 2 +- .../filesystem/spec/LocalFilesystemSpec.java | 13 +- .../filesystem/LocalFilesystemSpecTest.java | 123 ++++++++++++++++++ 3 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/LocalFilesystemSpecTest.java diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgentBuilderSupport.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgentBuilderSupport.java index 23ea83cc91..323ca4ca59 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgentBuilderSupport.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgentBuilderSupport.java @@ -151,7 +151,7 @@ static AbstractFilesystem resolveFilesystem( if (b.localFilesystemSpec != null) { return b.localFilesystemSpec.toFilesystem(workspace, nsFactory); } - // Default: route through LocalFilesystemSpec so the default project (= ${user.dir}) + // Default: route through LocalFilesystemSpec so the default project (= workspace) // is overlaid below the agent workspace, matching the Claude-Code-style two-layer model. return new LocalFilesystemSpec().toFilesystem(workspace, nsFactory); } diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/filesystem/spec/LocalFilesystemSpec.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/filesystem/spec/LocalFilesystemSpec.java index 8e2531a208..aa601baee0 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/filesystem/spec/LocalFilesystemSpec.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/filesystem/spec/LocalFilesystemSpec.java @@ -26,7 +26,6 @@ import io.agentscope.harness.agent.workspace.LocalFsMode; import io.agentscope.harness.agent.workspace.PathPolicy; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -68,9 +67,8 @@ public class LocalFilesystemSpec { * this directory and copies-on-write into the agent {@code workspace} when modified. Also * the shell {@code pwd} for {@code execute()} so command output matches user expectation. * - *

{@code null} until {@link #project(Path)} is called; defaults to - * {@link System#getProperty(String) System.getProperty("user.dir")} at - * {@link #toFilesystem} time. + *

{@code null} until {@link #project(Path)} is called; defaults to the agent + * {@code workspace} at {@link #toFilesystem} time. */ private Path project; @@ -249,7 +247,7 @@ public LocalFilesystemSpec additionalRoots(Collection roots) { * etc. fall back to this directory when the agent {@code workspace} does not contain them; * shell {@code execute()} runs with {@code pwd} set to this directory. * - *

Defaults to {@code System.getProperty("user.dir")} when not set. + *

Defaults to the agent {@code workspace} when not set. * * @param project project root path * @return this spec @@ -270,7 +268,7 @@ public LocalFilesystemSpec project(Path project) { * @param localNamespaceFactory optional namespace factory for per-user/session folder scoping * @return an {@link OverlayFilesystem} wired with the options in this spec */ - /** Project root explicitly configured, or {@code null} to fall back to {@code ${user.dir}}. */ + /** Project root explicitly configured, or {@code null} to fall back to workspace. */ public Path getProject() { return project; } @@ -286,8 +284,7 @@ public List getAdditionalRoots() { } public AbstractFilesystem toFilesystem(Path workspace, NamespaceFactory localNamespaceFactory) { - Path effectiveProject = - project != null ? project : Paths.get(System.getProperty("user.dir")); + Path effectiveProject = project != null ? project : workspace; List policyRoots = new ArrayList<>(); policyRoots.add(effectiveProject); policyRoots.add(workspace); diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/LocalFilesystemSpecTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/LocalFilesystemSpecTest.java new file mode 100644 index 0000000000..c8236b7179 --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/LocalFilesystemSpecTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.harness.agent.filesystem; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.agent.RuntimeContext; +import io.agentscope.harness.agent.filesystem.model.ReadResult; +import io.agentscope.harness.agent.filesystem.spec.LocalFilesystemSpec; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Tests for {@link LocalFilesystemSpec} default behavior when {@code project} is not set. + * + *

When {@code project} is null, the lower layer should default to the agent workspace, + * preventing unintended exposure of the JVM process's working directory ({@code user.dir}). + */ +class LocalFilesystemSpecTest { + + private static final RuntimeContext RT = RuntimeContext.empty(); + + @Test + void nullProject_usesWorkspaceAsLowerLayer(@TempDir Path workspace) throws Exception { + // Create a file in workspace that the agent should be able to read + Path workspaceFile = workspace.resolve("workspace-file.txt"); + Files.writeString(workspaceFile, "content from workspace", StandardCharsets.UTF_8); + + // Create spec without setting project (simulates framework default) + LocalFilesystemSpec spec = new LocalFilesystemSpec(); + + // Build filesystem + AbstractFilesystem fs = spec.toFilesystem(workspace, ns -> List.of("test")); + + // Agent should be able to read workspace files + ReadResult result = fs.read(RT, workspaceFile.toAbsolutePath().toString(), 0, 0); + assertTrue(result.isSuccess(), "Agent should be able to read workspace files"); + assertEquals("content from workspace", result.fileData().content()); + } + + @Test + void nullProject_cannotReadFilesOutsideWorkspace(@TempDir Path workspace, @TempDir Path outside) + throws Exception { + // Create a file outside workspace that the agent should NOT be able to read + Path outsideFile = outside.resolve("secret.txt"); + Files.writeString(outsideFile, "secret content", StandardCharsets.UTF_8); + + // Create spec without setting project + LocalFilesystemSpec spec = new LocalFilesystemSpec(); + + // Build filesystem + AbstractFilesystem fs = spec.toFilesystem(workspace, ns -> List.of("test")); + + // Agent should NOT be able to read files outside workspace via lower layer + ReadResult result = fs.read(RT, outsideFile.toAbsolutePath().toString(), 0, 0); + assertFalse( + result.isSuccess(), + "Agent should NOT be able to read files outside workspace when project is not set"); + } + + @Test + void projectSet_usesProjectAsLowerLayer(@TempDir Path project, @TempDir Path workspace) + throws Exception { + // Create a file in project directory + Path projectFile = project.resolve("project-file.txt"); + Files.writeString(projectFile, "content from project", StandardCharsets.UTF_8); + + // Create spec with explicit project + LocalFilesystemSpec spec = new LocalFilesystemSpec().project(project); + + // Build filesystem + AbstractFilesystem fs = spec.toFilesystem(workspace, ns -> List.of("test")); + + // Agent should be able to read project files via lower layer + ReadResult result = fs.read(RT, projectFile.toAbsolutePath().toString(), 0, 0); + assertTrue(result.isSuccess(), "Agent should be able to read project files"); + assertEquals("content from project", result.fileData().content()); + } + + @Test + void projectSet_cannotReadUserDir(@TempDir Path project, @TempDir Path workspace) + throws Exception { + // Create a file in user.dir that the agent should NOT be able to read + String userDir = System.getProperty("user.dir"); + Path userDirFile = Path.of(userDir, "test-secret-userdir.txt"); + Files.writeString(userDirFile, "secret from user.dir", StandardCharsets.UTF_8); + try { + // Create spec with explicit project (different from user.dir) + LocalFilesystemSpec spec = new LocalFilesystemSpec().project(project); + + // Build filesystem + AbstractFilesystem fs = spec.toFilesystem(workspace, ns -> List.of("test")); + + // Agent should NOT be able to read files from user.dir via lower layer + ReadResult result = fs.read(RT, userDirFile.toAbsolutePath().toString(), 0, 0); + assertFalse( + result.isSuccess(), + "Agent should NOT be able to read files from user.dir when project is set" + + " explicitly"); + } finally { + Files.deleteIfExists(userDirFile); + } + } +}