mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-17 04:44:40 +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
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@ -17,7 +17,7 @@ public class SkillFileAccessPolicy {
|
|||||||
* 验证文件路径是否安全
|
* 验证文件路径是否安全
|
||||||
*
|
*
|
||||||
* @param skillDir 技能根目录
|
* @param skillDir 技能根目录
|
||||||
* @param relativePath 相对路径(必须以 references/ 或 scripts/ 开头)
|
* @param relativePath 相对路径(必须以 references/、scripts/ 或 templates/ 开头)
|
||||||
* @return 归一化后的绝对路径,如果不安全则返回 null
|
* @return 归一化后的绝对路径,如果不安全则返回 null
|
||||||
*/
|
*/
|
||||||
public Path validateAndResolve(Path skillDir, String relativePath) {
|
public Path validateAndResolve(Path skillDir, String relativePath) {
|
||||||
@ -28,8 +28,10 @@ public class SkillFileAccessPolicy {
|
|||||||
// 归一化路径分隔符
|
// 归一化路径分隔符
|
||||||
String normalized = relativePath.replace("\\", "/");
|
String normalized = relativePath.replace("\\", "/");
|
||||||
|
|
||||||
// 必须以 references/ 或 scripts/ 开头
|
// 必须以 references/、scripts/ 或 templates/ 开头
|
||||||
if (!normalized.startsWith("references/") && !normalized.startsWith("scripts/")) {
|
if (!normalized.startsWith("references/")
|
||||||
|
&& !normalized.startsWith("scripts/")
|
||||||
|
&& !normalized.startsWith("templates/")) {
|
||||||
log.warn("Invalid path prefix: {}", relativePath);
|
log.warn("Invalid path prefix: {}", relativePath);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,18 +40,19 @@ public class SkillFileTool {
|
|||||||
private final SkillUsageService usageService;
|
private final SkillUsageService usageService;
|
||||||
|
|
||||||
@Tool(description = """
|
@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.
|
Use this when you need to access skill documentation or reference files.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- skillName: Name of the skill (e.g., "channel_message")
|
- skillName: Name of the skill (e.g., "channel_message")
|
||||||
- filePath: Relative path within skill directory, must start with "references/" or "scripts/"
|
- filePath: Relative path within skill directory, must start with "references/", "scripts/",
|
||||||
(e.g., "references/config.md", "scripts/helper.py")
|
or "templates/" (e.g., "references/config.md", "scripts/helper.py",
|
||||||
|
"templates/template.html")
|
||||||
To read SKILL.md itself, use "SKILL.md" as filePath
|
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.
|
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(
|
public String readSkillFile(
|
||||||
@JsonProperty(required = true)
|
@JsonProperty(required = true)
|
||||||
@ -59,7 +60,7 @@ public class SkillFileTool {
|
|||||||
String skillName,
|
String skillName,
|
||||||
|
|
||||||
@JsonProperty(required = true)
|
@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,
|
String filePath,
|
||||||
|
|
||||||
@JsonProperty(required = false)
|
@JsonProperty(required = false)
|
||||||
|
|||||||
@ -39,8 +39,11 @@ public class GeneratedFileController {
|
|||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.parseMediaType(entry.mimeType()));
|
headers.setContentType(MediaType.parseMediaType(entry.mimeType()));
|
||||||
// RFC 5987 filename* lets non-ASCII names round-trip in browsers.
|
// 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,
|
headers.add(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
"attachment; filename=\"" + sanitizeAscii(entry.filename())
|
disposition + "; filename=\"" + sanitizeAscii(entry.filename())
|
||||||
+ "\"; filename*=UTF-8''" + encodedName);
|
+ "\"; filename*=UTF-8''" + encodedName);
|
||||||
headers.setContentLength(entry.bytes().length);
|
headers.setContentLength(entry.bytes().length);
|
||||||
return ResponseEntity.ok().headers(headers).body(entry.bytes());
|
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