From 17453d4be90839308a9de8eaf254c5411f903a35 Mon Sep 17 00:00:00 2001 From: guslegend0510 Date: Tue, 14 Jul 2026 09:50:58 +0800 Subject: [PATCH] fix(harness): resolve shared skill cache paths --- .../workspace/WorkspacePathNormalizer.java | 12 ++++++ .../agent/tool/FilesystemToolTest.java | 33 ++++++++++++++++ .../WorkspacePathNormalizerTest.java | 38 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 agentscope-harness/src/test/java/io/agentscope/harness/agent/workspace/WorkspacePathNormalizerTest.java diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspacePathNormalizer.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspacePathNormalizer.java index dd564208fa..81533d0630 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspacePathNormalizer.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspacePathNormalizer.java @@ -31,6 +31,8 @@ */ public final class WorkspacePathNormalizer { + private static final String SHARED_SKILL_CACHE = ".skills-cache"; + private final List prefixes; private WorkspacePathNormalizer(List prefixes) { @@ -81,12 +83,22 @@ public String normalize(String path) { for (String prefix : prefixes) { String stripped = tryStrip(path, prefix); if (stripped != null) { + // Marketplace skills are staged once at the workspace root and shared by all + // namespaces. Keep that root virtual-absolute so namespace-aware filesystems do + // not remap it to //.skills-cache. + if (isSharedSkillCachePath(stripped)) { + return "/" + stripped; + } return stripped; } } return path; } + private static boolean isSharedSkillCachePath(String path) { + return path.equals(SHARED_SKILL_CACHE) || path.startsWith(SHARED_SKILL_CACHE + "/"); + } + private static String tryStrip(String path, String prefix) { String normalizedPath = path.replace('\\', '/'); String normalizedPrefix = prefix.replace('\\', '/'); diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/FilesystemToolTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/FilesystemToolTest.java index 33d200dfd9..a175d890f8 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/FilesystemToolTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/FilesystemToolTest.java @@ -23,13 +23,21 @@ import io.agentscope.core.agent.RuntimeContext; import io.agentscope.harness.agent.filesystem.AbstractFilesystem; +import io.agentscope.harness.agent.filesystem.local.LocalFilesystem; import io.agentscope.harness.agent.filesystem.model.EditResult; import io.agentscope.harness.agent.filesystem.model.FileInfo; import io.agentscope.harness.agent.filesystem.model.LsResult; +import io.agentscope.harness.agent.workspace.LocalFsMode; +import io.agentscope.harness.agent.workspace.PathPolicy; import io.agentscope.harness.agent.workspace.WorkspacePathNormalizer; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; /** Unit tests for {@link FilesystemTool}. */ class FilesystemToolTest { @@ -85,4 +93,29 @@ void listFiles_normalizesWindowsAbsoluteWorkspacePath() { assertTrue(result.contains("[DIR]")); verify(filesystem).ls(RT, "memory"); } + + @Test + void readFile_readsSharedMarketplaceSkillResourceWithUserNamespace(@TempDir Path workspace) + throws IOException { + Path resource = workspace.resolve(".skills-cache/market/docx/docx-js.md"); + Files.createDirectories(resource.getParent()); + Files.writeString(resource, "shared resource", StandardCharsets.UTF_8); + + LocalFilesystem localFilesystem = + new LocalFilesystem( + workspace, + LocalFsMode.ROOTED, + PathPolicy.empty(), + 10, + rc -> List.of(rc.getUserId())); + FilesystemTool namespacedTool = + new FilesystemTool(localFilesystem, WorkspacePathNormalizer.of("/workspace")); + RuntimeContext userContext = RuntimeContext.builder().userId("alice").build(); + + String result = + namespacedTool.readFile( + userContext, "/workspace/.skills-cache/market/docx/docx-js.md", 0, 0); + + assertTrue(result.contains("shared resource")); + } } diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/workspace/WorkspacePathNormalizerTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/workspace/WorkspacePathNormalizerTest.java new file mode 100644 index 0000000000..b6dfd9f034 --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/workspace/WorkspacePathNormalizerTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.harness.agent.workspace; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class WorkspacePathNormalizerTest { + + private final WorkspacePathNormalizer normalizer = WorkspacePathNormalizer.of("/workspace"); + + @Test + void keepsMarketplaceSkillCacheInSharedWorkspaceRoot() { + assertEquals( + "/.skills-cache/market/docx/docx-js.md", + normalizer.normalize("/workspace/.skills-cache/market/docx/docx-js.md")); + } + + @Test + void stillNormalizesUserScopedWorkspacePaths() { + assertEquals( + "skills/docx/SKILL.md", normalizer.normalize("/workspace/skills/docx/SKILL.md")); + } +}