-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(harness): sanitize session ids used in workspace file names #2952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Map<String, TaskRecord>> 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:<id>:main:<uuid>}), 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. | ||
| * | ||
| * <p>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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only changes the local path. |
||
| } | ||
|
|
||
| /** 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 '-'. | ||
| * | ||
| * <p>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("-"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replacing every unsafe character with |
||
| } | ||
|
|
||
| private String taskRecordPath(String agentId, String sessionId) { | ||
| return AGENTS_DIR + "/" + agentId + "/" + TASKS_DIR + "/" + sessionId + ".json"; | ||
| return AGENTS_DIR | ||
| + "/" | ||
| + agentId | ||
| + "/" | ||
| + TASKS_DIR | ||
| + "/" | ||
| + safeSegment(sessionId) | ||
| + ".json"; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern omits ASCII control characters (U+0000 through U+001F), which Windows also forbids in file-name segments. A caller-supplied id such as
a\u0001bis therefore unchanged and still fails when the Windows path is constructed. Include those characters in the transformation and cover them in the test.