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 @@ -112,31 +112,48 @@ 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;
};
}

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 spaces with backslash (e.g. {@code "a b" -> "a\ b"}) so the value can be embedded
* unquoted in POSIX shell commands without word-splitting.
*
* <p>This is not a general-purpose shell escaping routine.
*/
static String escapeSpaces(String value) {
return value.indexOf(' ') >= 0 ? value.replace(" ", "\\ ") : value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
// =========================================================================
Expand Down Expand Up @@ -109,6 +164,17 @@ void rendersFilesRootAndCodeExecutionWhenAvailable() {
assertTrue(out.contains("<files-root>"));
}

@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("<files-root>/workspace/skills/V2Ray\\ 代理配置助手</files-root>"));
}

@Test
void filterRemovesHiddenSkills() {
HarnessSkillEntry visible = HarnessSkillEntry.of(skill("visible", "src"), null);
Expand Down
Loading