From b64522cd3e3f55506a1b785f1ffc28b28288426d Mon Sep 17 00:00:00 2001 From: wylovely <2016688030@qq.com> Date: Thu, 3 Sep 2026 12:42:28 +0800 Subject: [PATCH] fix(harness): sanitize session ids used in workspace file names --- .../agent/workspace/WorkspaceManager.java | 41 +++++++++++-- .../WorkspaceManagerPathSafetyTest.java | 58 +++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspaceManager.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspaceManager.java index df81e8da59..7888e23e44 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspaceManager.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspaceManager.java @@ -59,6 +59,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -105,6 +106,18 @@ public class WorkspaceManager implements AutoCloseable { private static final TypeReference> TASK_MAP_TYPE = new TypeReference<>() {}; + /** + * Characters that Windows/NTFS reserves inside a single path segment. Ids such as the session + * id can legitimately contain a colon (for example {@code agent::main:}), and using + * one verbatim in a file name turns path construction into an {@link + * java.nio.file.InvalidPathException} on Windows. Reserved characters are replaced with '-' + * rather than rejected, so ids keep working unchanged on every platform. + * + *

Forward slash and backslash are included as well: an id is a single file-name segment + * here, so either would otherwise be read as a directory separator. + */ + private static final Pattern UNSAFE_SEGMENT_CHARS = Pattern.compile("[<>:\"/\\\\|?*]"); + /** * Per-path locks for workspace-relative files to prevent concurrent read-modify-write races. * Keyed by workspace-relative path (e.g. {@code agents/X/tasks/Y.json}, @@ -343,18 +356,19 @@ public Path getSessionDir(RuntimeContext rc, String agentId) { */ @Deprecated public Path resolveSessionFile(RuntimeContext rc, String agentId, String sessionId) { - return getSessionDir(rc, agentId).resolve(sessionId + ".json"); + return getSessionDir(rc, agentId).resolve(safeSegment(sessionId) + ".json"); } /** Returns the JSONL session context file path (LLM-facing, compacted). */ public Path resolveSessionContextFile(RuntimeContext rc, String agentId, String sessionId) { return getSessionDir(rc, agentId) - .resolve(sessionId + WorkspaceConstants.SESSION_CONTEXT_EXT); + .resolve(safeSegment(sessionId) + WorkspaceConstants.SESSION_CONTEXT_EXT); } /** Returns the JSONL session log file path (full history, append-only). */ public Path resolveSessionLogFile(RuntimeContext rc, String agentId, String sessionId) { - return getSessionDir(rc, agentId).resolve(sessionId + WorkspaceConstants.SESSION_LOG_EXT); + return getSessionDir(rc, agentId) + .resolve(safeSegment(sessionId) + WorkspaceConstants.SESSION_LOG_EXT); } /** @@ -596,8 +610,27 @@ private Instant diskMtime(Path p) { } } + /** + * Replaces characters that are illegal in a Windows/NTFS file-name segment with '-'. + * + *

Session ids are caller-supplied and carry no documented character constraint, yet they are + * used verbatim to build file names. Sanitising here keeps an unexpected id from surfacing as a + * raw {@link java.nio.file.InvalidPathException} from deep inside the workspace layer, with no + * hint that the caller-supplied id is the problem. + */ + private static String safeSegment(String segment) { + return segment == null ? null : UNSAFE_SEGMENT_CHARS.matcher(segment).replaceAll("-"); + } + private String taskRecordPath(String agentId, String sessionId) { - return AGENTS_DIR + "/" + agentId + "/" + TASKS_DIR + "/" + sessionId + ".json"; + return AGENTS_DIR + + "/" + + agentId + + "/" + + TASKS_DIR + + "/" + + safeSegment(sessionId) + + ".json"; } /** diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/workspace/WorkspaceManagerPathSafetyTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/workspace/WorkspaceManagerPathSafetyTest.java index 06dcdd73c2..333d461434 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/workspace/WorkspaceManagerPathSafetyTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/workspace/WorkspaceManagerPathSafetyTest.java @@ -89,4 +89,62 @@ void workspaceWritesAllowLogicalAbsoluteAndLiteralDoubleDotNames(@TempDir Path r "LITERAL", Files.readString(backend.resolve("some..dir/note.txt"), StandardCharsets.UTF_8)); } + + @Test + void sessionFileNamesReplaceWindowsReservedCharacters(@TempDir Path root) throws Exception { + Path template = root.resolve("template"); + Path backend = root.resolve("backend"); + Files.createDirectories(template); + Files.createDirectories(backend); + RuntimeContext rc = RuntimeContext.empty(); + + try (WorkspaceManager manager = + new WorkspaceManager( + template, + new LocalFilesystem( + backend, LocalFsMode.ROOTED, PathPolicy.empty(), 10, null))) { + // The shape from #2937: a session id built from namespaced parts. + String sessionId = "agent:abc-123:main:main-9f3c"; + String sanitized = "agent-abc-123-main-main-9f3c"; + + assertFileNameIsSanitized( + manager.resolveSessionFile(rc, "agent-1", sessionId), sanitized); + assertFileNameIsSanitized( + manager.resolveSessionContextFile(rc, "agent-1", sessionId), sanitized); + assertFileNameIsSanitized( + manager.resolveSessionLogFile(rc, "agent-1", sessionId), sanitized); + + // Every character NTFS reserves, plus both separators, is replaced. + String allReserved = "ac:d\"e/f\\g|h?i*j"; + String allSanitized = "a-b-c-d-e-f-g-h-i-j"; + assertFileNameIsSanitized( + manager.resolveSessionFile(rc, "agent-1", allReserved), allSanitized); + } + } + + @Test + void sessionFileNamesLeaveOrdinaryIdsUnchanged(@TempDir Path root) throws Exception { + Path template = root.resolve("template"); + Path backend = root.resolve("backend"); + Files.createDirectories(template); + Files.createDirectories(backend); + RuntimeContext rc = RuntimeContext.empty(); + + try (WorkspaceManager manager = + new WorkspaceManager( + template, + new LocalFilesystem( + backend, LocalFsMode.ROOTED, PathPolicy.empty(), 10, null))) { + assertFileNameIsSanitized( + manager.resolveSessionFile(rc, "agent-1", "main-9f3c7a2e"), "main-9f3c7a2e"); + } + } + + private static void assertFileNameIsSanitized(Path file, String sanitizedBaseName) { + String name = file.getFileName().toString(); + assertEquals(sanitizedBaseName, name.substring(0, sanitizedBaseName.length())); + assertFalse( + name.substring(sanitizedBaseName.length()).matches(".*[<>:\"/\\\\|?*].*"), + "file name still contains a reserved character: " + name); + } }