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 @@ -56,11 +56,10 @@ public class LocalFilesystemWithShell extends LocalFilesystem implements Abstrac
private final Map<String, String> env;

/**
* Working directory passed to {@link ProcessBuilder#directory(java.io.File)} for shell
* commands. When {@code null}, falls back to {@link #getCwd()} (with per-call namespace
* prefix). Decouples shell {@code pwd} from the filesystem root so overlay-mode callers can
* keep filesystem operations rooted at the agent workspace while shell sees the user's
* project directory.
* Shell working directory used when the current call has no namespace. Namespaced calls run
* under {@link #getCwd()} with the per-call namespace prefix instead. Decouples the default
* shell {@code pwd} from the filesystem root so overlay-mode callers can keep filesystem
* operations rooted at the agent workspace while shell sees the user's project directory.
*/
private final Path shellCwd;

Expand Down Expand Up @@ -195,8 +194,8 @@ public LocalFilesystemWithShell(
* @param env environment variables for shell commands ({@code null} for empty)
* @param inheritEnv whether to inherit the parent process's environment variables
* @param namespaceFactory optional namespace factory for path scoping ({@code null} for none)
* @param shellCwd working directory for shell command execution; when {@code null}, falls
* back to {@code rootDir} (with namespace prefix when configured)
* @param shellCwd working directory for shell command execution when no namespace applies;
* when {@code null}, falls back to {@code rootDir}
*/
public LocalFilesystemWithShell(
Path rootDir,
Expand All @@ -222,7 +221,8 @@ public LocalFilesystemWithShell(
/**
* Most-complete constructor: filesystem operations follow {@code mode} and {@code pathPolicy}
* (see {@link LocalFilesystem#LocalFilesystem(Path, LocalFsMode, PathPolicy, int, NamespaceFactory)});
* shell commands run with {@code pwd = shellCwd} when set, otherwise the filesystem root.
* shell commands run inside the namespaced filesystem root when a namespace applies, or with
* {@code pwd = shellCwd} when set otherwise.
*
* @param rootDir filesystem root for relative-path operations
* @param mode path-resolution policy ({@code null} treated as {@link LocalFsMode#UNRESTRICTED})
Expand All @@ -232,8 +232,8 @@ public LocalFilesystemWithShell(
* @param env environment variables for shell commands ({@code null} for empty)
* @param inheritEnv whether to inherit the parent process environment
* @param namespaceFactory optional per-user/session namespace factory
* @param shellCwd shell {@code pwd}; {@code null} falls back to {@code rootDir} (with
* namespace prefix when configured)
* @param shellCwd shell {@code pwd} when no namespace applies; {@code null} falls back to
* {@code rootDir}
*/
public LocalFilesystemWithShell(
Path rootDir,
Expand Down Expand Up @@ -296,9 +296,9 @@ public String id() {
}

/**
* Returns the working directory configured for shell {@code execute()} calls, or {@code null}
* when shell falls back to the filesystem root (with namespace prefix). Used by upstream
* code that needs to expose the user-visible project directory in prompts or diagnostics.
* Returns the working directory configured for non-namespaced shell {@code execute()} calls,
* or {@code null} when shell falls back to the filesystem root. Used by upstream code that
* needs to expose the user-visible project directory in prompts or diagnostics.
*/
public Path getShellCwd() {
return shellCwd;
Expand Down Expand Up @@ -408,26 +408,25 @@ public ExecuteResponse execute(
}

private Path resolveExecuteCwd(RuntimeContext rc) {
if (shellCwd != null) {
return shellCwd;
}
NamespaceFactory nsf = getNamespaceFactory();
if (nsf == null) {
return getCwd();
}
List<String> ns = nsf.getNamespace(rc);
if (ns == null || ns.isEmpty()) {
return getCwd();
}
Path namespaced = getCwd();
for (String segment : ns) {
namespaced = namespaced.resolve(segment);
}
try {
Files.createDirectories(namespaced);
} catch (IOException e) {
log.warn("Failed to create namespace directory {}: {}", namespaced, e.getMessage());
if (nsf != null) {
List<String> ns = nsf.getNamespace(rc);
if (ns != null && !ns.isEmpty()) {
Path namespaced = getCwd();
for (String segment : ns) {
namespaced = namespaced.resolve(segment);
}
try {
Files.createDirectories(namespaced);
} catch (IOException e) {
log.warn(
"Failed to create namespace directory {}: {}",
namespaced,
e.getMessage());
}
return namespaced;
}
}
return namespaced;
return shellCwd != null ? shellCwd : getCwd();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.harness.agent.IsolationScope;
import io.agentscope.harness.agent.filesystem.local.LocalFilesystem;
import io.agentscope.harness.agent.filesystem.local.LocalFilesystemWithShell;
import io.agentscope.harness.agent.filesystem.model.EditResult;
import io.agentscope.harness.agent.filesystem.model.ExecuteResponse;
import io.agentscope.harness.agent.filesystem.model.FileUploadResponse;
import io.agentscope.harness.agent.filesystem.model.ReadResult;
import io.agentscope.harness.agent.filesystem.model.WriteResult;
import io.agentscope.harness.agent.filesystem.remote.store.NamespaceFactory;
import io.agentscope.harness.agent.filesystem.sandbox.AbstractSandboxFilesystem;
import io.agentscope.harness.agent.workspace.LocalFsMode;
import io.agentscope.harness.agent.workspace.PathPolicy;
Expand Down Expand Up @@ -240,4 +243,88 @@ void execute_delegatesToShellBackend() {
assertTrue(r.output().contains("hello"));
assertEquals(0, r.exitCode());
}

@Test
void execute_sessionIsolation_startsInNamespacedWorkspace() throws Exception {
assertShellWorkingDirectory(IsolationScope.SESSION, "session-1");
}

@Test
void execute_userIsolation_startsInNamespacedWorkspace() throws Exception {
assertShellWorkingDirectory(IsolationScope.USER, "alice");
}

@Test
void execute_globalIsolation_startsInProjectDirectory() throws Exception {
assertShellWorkingDirectory(IsolationScope.GLOBAL, null);
}

@Test
void execute_withoutNamespaceOrProjectDirectory_startsInWorkspace() throws Exception {
LocalFilesystemWithShell shell = new LocalFilesystemWithShell(workspace);

ExecuteResponse response =
shell.execute(RuntimeContext.empty(), printWorkingDirectoryCommand(), 10);

assertEquals(0, response.exitCode());
assertEquals(
workspace.toRealPath().toString(),
Path.of(response.output().strip()).toRealPath().toString());
}

@Test
void execute_nullNamespace_fallsBackToProjectDirectory() throws Exception {
LocalFilesystemWithShell shell = createShell(context -> null, project);

ExecuteResponse response =
shell.execute(RuntimeContext.empty(), printWorkingDirectoryCommand(), 10);

assertEquals(0, response.exitCode());
assertEquals(
project.toRealPath().toString(),
Path.of(response.output().strip()).toRealPath().toString());
}

@Test
void execute_namespaceDirectoryCreationFailure_returnsError() throws Exception {
Files.writeString(workspace.resolve("blocked"), "not a directory");
LocalFilesystemWithShell shell = createShell(context -> List.of("blocked"), project);

ExecuteResponse response = shell.execute(RuntimeContext.empty(), "echo hello", 10);

assertEquals(1, response.exitCode());
assertTrue(response.output().startsWith("Error executing command (IOException):"));
}

private void assertShellWorkingDirectory(IsolationScope isolationScope, String namespace)
throws Exception {
LocalFilesystemWithShell shell = createShell(isolationScope.toNamespaceFactory(), project);
RuntimeContext context =
RuntimeContext.builder().userId("alice").sessionId("session-1").build();

ExecuteResponse response = shell.execute(context, printWorkingDirectoryCommand(), 10);

assertEquals(0, response.exitCode());
Path expected = namespace == null ? project : workspace.resolve(namespace);
assertEquals(
expected.toRealPath().toString(),
Path.of(response.output().strip()).toRealPath().toString());
}

private LocalFilesystemWithShell createShell(NamespaceFactory namespaceFactory, Path shellCwd) {
return new LocalFilesystemWithShell(
workspace,
LocalFsMode.ROOTED,
PathPolicy.of(project, workspace),
120,
100_000,
null,
false,
namespaceFactory,
shellCwd);
}

private static String printWorkingDirectoryCommand() {
return System.getProperty("os.name").startsWith("Windows") ? "cd" : "pwd";
}
}
Loading