From db5990590dd3135f9f00ff7ab4f1805d412ba8d6 Mon Sep 17 00:00:00 2001 From: zouyx Date: Wed, 1 Jul 2026 22:45:28 +0800 Subject: [PATCH 1/2] fix(shell): escape spaces in skill paths returned by ShellPathPolicy --- .../agent/skill/runtime/ShellPathPolicy.java | 45 ++++++++----- .../agent/skill/runtime/SkillRuntimeTest.java | 66 +++++++++++++++++++ 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/skill/runtime/ShellPathPolicy.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/skill/runtime/ShellPathPolicy.java index a6a5b7da7..852bfc8bd 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/skill/runtime/ShellPathPolicy.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/skill/runtime/ShellPathPolicy.java @@ -112,9 +112,14 @@ public String resolve(String skillName, StageResult stage) { private String joinSkills(String skillName) { return switch (mode) { - case SANDBOX -> sandboxPrefix + "/skills/" + skillName; + case SANDBOX -> escapeSpaces(sandboxPrefix + "/skills/" + skillName); case LOCAL_WITH_SHELL -> - workspaceRoot.resolve("skills").resolve(skillName).toAbsolutePath().toString(); + escapeSpaces( + workspaceRoot + .resolve("skills") + .resolve(skillName) + .toAbsolutePath() + .toString()); case NO_SHELL -> null; }; } @@ -122,21 +127,31 @@ private String joinSkills(String skillName) { private String joinCache(String sourceNs, String skillName) { return switch (mode) { case SANDBOX -> - sandboxPrefix - + "/" - + MarketplaceStager.CACHE_DIR - + "/" - + sourceNs - + "/" - + skillName; + escapeSpaces( + sandboxPrefix + + "/" + + MarketplaceStager.CACHE_DIR + + "/" + + sourceNs + + "/" + + skillName); case LOCAL_WITH_SHELL -> - workspaceRoot - .resolve(MarketplaceStager.CACHE_DIR) - .resolve(sourceNs) - .resolve(skillName) - .toAbsolutePath() - .toString(); + escapeSpaces( + workspaceRoot + .resolve(MarketplaceStager.CACHE_DIR) + .resolve(sourceNs) + .resolve(skillName) + .toAbsolutePath() + .toString()); case NO_SHELL -> null; }; } + + /** + * Escapes space characters with backslash so the path can be safely used in shell commands + * by the LLM. + */ + static String escapeSpaces(String value) { + return value.indexOf(' ') >= 0 ? value.replace(" ", "\\ ") : value; + } } diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/skill/runtime/SkillRuntimeTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/skill/runtime/SkillRuntimeTest.java index 68305a483..575fb6012 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/skill/runtime/SkillRuntimeTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/skill/runtime/SkillRuntimeTest.java @@ -30,6 +30,8 @@ import io.agentscope.core.tool.ToolCallParam; import io.agentscope.core.tool.Toolkit; import io.agentscope.harness.agent.skill.SkillResources; +import io.agentscope.harness.agent.skill.runtime.MarketplaceStager.StageResult; +import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -74,6 +76,59 @@ void laterEntryWithSameIdOverwrites() { } } + // ========================================================================= + // ShellPathPolicy + // ========================================================================= + + @Nested + class ShellPathPolicyTests { + + @Test + void escapeSpacesReplacesWithBackslashSpace() { + assertEquals("hello", ShellPathPolicy.escapeSpaces("hello")); + assertEquals("hello\\ world", ShellPathPolicy.escapeSpaces("hello world")); + assertEquals("V2Ray\\ 代理配置助手", ShellPathPolicy.escapeSpaces("V2Ray 代理配置助手")); + assertEquals("a\\ b\\ c", ShellPathPolicy.escapeSpaces("a b c")); + } + + @Test + void sandboxResolveEscapesSpacesInSkillName() { + ShellPathPolicy policy = ShellPathPolicy.sandbox(); + String result = policy.resolve("V2Ray 代理配置助手", new StageResult.WorkspaceNative()); + assertEquals("/workspace/skills/V2Ray\\ 代理配置助手", result); + } + + @Test + void sandboxResolveEscapesSpacesInCachedSkill() { + ShellPathPolicy policy = ShellPathPolicy.sandbox(); + String result = policy.resolve("ignored", new StageResult.Cached("ns", "my skill")); + assertEquals("/workspace/.skills-cache/ns/my\\ skill", result); + } + + @Test + void localWithShellResolveEscapesSpaces() { + ShellPathPolicy policy = ShellPathPolicy.localWithShell(Paths.get("/tmp/my workspace")); + String result = policy.resolve("my skill", new StageResult.WorkspaceNative()); + assertEquals("/tmp/my\\ workspace/skills/my\\ skill", result); + } + + @Test + void noShellAlwaysReturnsNull() { + ShellPathPolicy policy = ShellPathPolicy.noShell(); + assertNull(policy.resolve("any name", new StageResult.WorkspaceNative())); + assertNull(policy.resolve("any name", new StageResult.Cached("ns", "name"))); + assertNull(policy.resolve("any name", StageResult.NONE)); + assertNull(policy.resolve("any name", null)); + } + + @Test + void nullStageOrNoneReturnsNull() { + ShellPathPolicy policy = ShellPathPolicy.sandbox(); + assertNull(policy.resolve("alpha", null)); + assertNull(policy.resolve("alpha", StageResult.NONE)); + } + } + // ========================================================================= // SkillPromptBuilder // ========================================================================= @@ -109,6 +164,17 @@ void rendersFilesRootAndCodeExecutionWhenAvailable() { assertTrue(out.contains("")); } + @Test + void rendersEscapedFilesRootWhenPathContainsSpaces() { + HarnessSkillEntry e = + new HarnessSkillEntry( + skill("V2Ray 代理配置助手", "wkspace"), + null, + "/workspace/skills/V2Ray\\ 代理配置助手"); + String out = new SkillPromptBuilder().render(SkillCatalog.of(List.of(e))); + assertTrue(out.contains("/workspace/skills/V2Ray\\ 代理配置助手")); + } + @Test void filterRemovesHiddenSkills() { HarnessSkillEntry visible = HarnessSkillEntry.of(skill("visible", "src"), null); From 8d77878c262d3af741bfb2cd0672b6d2de45561b Mon Sep 17 00:00:00 2001 From: Joe Zou Date: Wed, 1 Jul 2026 23:16:59 +0800 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../harness/agent/skill/runtime/ShellPathPolicy.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/skill/runtime/ShellPathPolicy.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/skill/runtime/ShellPathPolicy.java index 852bfc8bd..6120ae608 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/skill/runtime/ShellPathPolicy.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/skill/runtime/ShellPathPolicy.java @@ -148,8 +148,10 @@ private String joinCache(String sourceNs, String skillName) { } /** - * Escapes space characters with backslash so the path can be safely used in shell commands - * by the LLM. + * Escapes spaces with backslash (e.g. {@code "a b" -> "a\ b"}) so the value can be embedded + * unquoted in POSIX shell commands without word-splitting. + * + *

This is not a general-purpose shell escaping routine. */ static String escapeSpaces(String value) { return value.indexOf(' ') >= 0 ? value.replace(" ", "\\ ") : value;