Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>{@code null} until {@link #project(Path)} is called; defaults to
* {@link System#getProperty(String) System.getProperty("user.dir")} at
* {@link #toFilesystem} time.
* <p>{@code null} until {@link #project(Path)} is called; defaults to the agent
* {@code workspace} at {@link #toFilesystem} time.
*/
private Path project;

Expand Down Expand Up @@ -249,7 +247,7 @@ public LocalFilesystemSpec additionalRoots(Collection<Path> 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.
*
* <p>Defaults to {@code System.getProperty("user.dir")} when not set.
* <p>Defaults to the agent {@code workspace} when not set.
*
* @param project project root path
* @return this spec
Expand All @@ -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;
}
Expand All @@ -286,8 +284,7 @@ public List<Path> 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<Path> policyRoots = new ArrayList<>();
policyRoots.add(effectiveProject);
policyRoots.add(workspace);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] When project == null, effectiveProject equals workspace, so policyRoots ends up with workspace added twice (lines 289 and 290). PathPolicy.of() handles duplicates gracefully, but a simple guard would keep the list clean:

policyRoots.add(effectiveProject);
if (!effectiveProject.equals(workspace)) {
    policyRoots.add(workspace);
}

Not blocking — purely cosmetic.

Comment thread
BeginnerCong marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[praise] Excellent test coverage. The four tests systematically verify: (1) default fallback to workspace works, (2) no access to files outside workspace when project is unset, (3) explicit project is respected, and (4) user.dir is not leaked when project is set. The use of @TempDir for isolation and the finally cleanup in projectSet_cannotReadUserDir are well done.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[praise] Excellent test coverage. The four tests systematically verify: (1) default fallback to workspace works, (2) no access to files outside workspace when project is unset, (3) explicit project is respected, and (4) user.dir is not leaked when project is set. The use of @TempDir for isolation and the finally cleanup in projectSet_cannotReadUserDir are well done.

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);
}
}
}
Loading