fix(skill): allow template file access

This commit is contained in:
matevip 2026-05-23 09:08:01 +08:00
parent cef1730e6e
commit 123d912f84
4 changed files with 51 additions and 10 deletions

View File

@ -7,7 +7,7 @@ import java.nio.file.Path;
/**
* 技能文件访问策略
* 确保只能访问 skillDir 内的 references/ scripts/ 文件
* 确保只能访问 skillDir 内的 references/scripts/ templates/ 文件
*/
@Slf4j
@Component
@ -17,7 +17,7 @@ public class SkillFileAccessPolicy {
* 验证文件路径是否安全
*
* @param skillDir 技能根目录
* @param relativePath 相对路径必须以 references/ scripts/ 开头
* @param relativePath 相对路径必须以 references/scripts/ templates/ 开头
* @return 归一化后的绝对路径如果不安全则返回 null
*/
public Path validateAndResolve(Path skillDir, String relativePath) {
@ -28,8 +28,10 @@ public class SkillFileAccessPolicy {
// 归一化路径分隔符
String normalized = relativePath.replace("\\", "/");
// 必须以 references/ scripts/ 开头
if (!normalized.startsWith("references/") && !normalized.startsWith("scripts/")) {
// 必须以 references/scripts/ templates/ 开头
if (!normalized.startsWith("references/")
&& !normalized.startsWith("scripts/")
&& !normalized.startsWith("templates/")) {
log.warn("Invalid path prefix: {}", relativePath);
return null;
}

View File

@ -40,18 +40,19 @@ public class SkillFileTool {
private final SkillUsageService usageService;
@Tool(description = """
Read a file from a skill's directory (SKILL.md, references/, or scripts/).
Read a file from a skill's directory (SKILL.md, references/, scripts/, or templates/).
Use this when you need to access skill documentation or reference files.
Parameters:
- skillName: Name of the skill (e.g., "channel_message")
- filePath: Relative path within skill directory, must start with "references/" or "scripts/"
(e.g., "references/config.md", "scripts/helper.py")
- filePath: Relative path within skill directory, must start with "references/", "scripts/",
or "templates/" (e.g., "references/config.md", "scripts/helper.py",
"templates/template.html")
To read SKILL.md itself, use "SKILL.md" as filePath
Returns: File content as string, or error message if file not found or access denied.
Security: Only files under references/ and scripts/ can be accessed. Path traversal is blocked.
Security: Only files under references/, scripts/, and templates/ can be accessed. Path traversal is blocked.
""")
public String readSkillFile(
@JsonProperty(required = true)
@ -59,7 +60,7 @@ public class SkillFileTool {
String skillName,
@JsonProperty(required = true)
@JsonPropertyDescription("Relative file path (e.g., 'references/doc.md' or 'scripts/run.py')")
@JsonPropertyDescription("Relative file path (e.g., 'references/doc.md', 'scripts/run.py', or 'templates/template.html')")
String filePath,
@JsonProperty(required = false)

View File

@ -39,8 +39,11 @@ public class GeneratedFileController {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(entry.mimeType()));
// RFC 5987 filename* lets non-ASCII names round-trip in browsers.
String disposition = entry.mimeType() != null && entry.mimeType().startsWith("image/")
? "inline"
: "attachment";
headers.add(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + sanitizeAscii(entry.filename())
disposition + "; filename=\"" + sanitizeAscii(entry.filename())
+ "\"; filename*=UTF-8''" + encodedName);
headers.setContentLength(entry.bytes().length);
return ResponseEntity.ok().headers(headers).body(entry.bytes());

View File

@ -0,0 +1,35 @@
package vip.mate.skill.runtime;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class SkillFileAccessPolicyTest {
private final SkillFileAccessPolicy policy = new SkillFileAccessPolicy();
private final Path skillDir = Path.of("/workspace/skills/architecture-diagram");
@Test
@DisplayName("allows architecture skill templates")
void allowsTemplatesDirectory() {
Path resolved = policy.validateAndResolve(skillDir, "templates/template.html");
assertEquals(skillDir.resolve("templates/template.html"), resolved);
}
@Test
@DisplayName("still rejects unsupported top-level paths")
void rejectsUnsupportedTopLevelPaths() {
assertNull(policy.validateAndResolve(skillDir, "assets/logo.svg"));
}
@Test
@DisplayName("rejects traversal from allowed directories")
void rejectsTraversal() {
assertNull(policy.validateAndResolve(skillDir, "templates/../SKILL.md"));
}
}