mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
fix(skill): allow template file access
This commit is contained in:
parent
cef1730e6e
commit
123d912f84
@ -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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user