Skip to content

Commit 422aaa1

Browse files
committed
🐛 Reject non-UTF-8 skill resources
1 parent 8154e66 commit 422aaa1

4 files changed

Lines changed: 51 additions & 8 deletions

File tree

kernel/mcp/server_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ import (
2222
"io"
2323
"net/http"
2424
"net/http/httptest"
25+
"os"
26+
"path/filepath"
2527
"strings"
2628
"testing"
2729

2830
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
2931
"github.com/siyuan-note/siyuan/kernel/mcp/tools"
32+
"github.com/siyuan-note/siyuan/kernel/util"
3033
)
3134

3235
func newTestHTTPServer(t *testing.T) (*mcpsdk.Server, *httptest.Server) {
@@ -392,6 +395,44 @@ func TestToolInputAndOutputValidation(t *testing.T) {
392395
}
393396
}
394397

398+
func TestSkillResourceContentSurvivesMCPConversion(t *testing.T) {
399+
originalDataDir, originalHomeDir := util.DataDir, util.HomeDir
400+
root := t.TempDir()
401+
util.DataDir = filepath.Join(root, "workspace", "data")
402+
util.HomeDir = filepath.Join(root, "home")
403+
t.Cleanup(func() {
404+
util.DataDir, util.HomeDir = originalDataDir, originalHomeDir
405+
})
406+
407+
skillDir := filepath.Join(util.SkillsDir(), "transport-skill")
408+
if err := os.MkdirAll(filepath.Join(skillDir, "references"), 0755); err != nil {
409+
t.Fatal(err)
410+
}
411+
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"),
412+
[]byte("---\nname: transport-skill\ndescription: transport test\n---\nbody"), 0644); err != nil {
413+
t.Fatal(err)
414+
}
415+
if err := os.WriteFile(filepath.Join(skillDir, "references", "valid.txt"), []byte("中文内容"), 0644); err != nil {
416+
t.Fatal(err)
417+
}
418+
419+
result, err := tools.SkillTool.Handler(map[string]any{
420+
"action": "load",
421+
"name": "transport-skill/references/valid.txt",
422+
})
423+
if err != nil || result.IsError || len(result.Content) != 1 {
424+
t.Fatalf("unexpected Skill resource result: %#v, %v", result, err)
425+
}
426+
converted, err := convertContentItem(result.Content[0])
427+
if err != nil {
428+
t.Fatal(err)
429+
}
430+
text, ok := converted.(*mcpsdk.TextContent)
431+
if !ok || text.Text != "<skill_resource skill=\"transport-skill\" path=\"references/valid.txt\">\n\n中文内容\n\n</skill_resource>" {
432+
t.Fatalf("Skill resource changed during MCP conversion: %#v", converted)
433+
}
434+
}
435+
395436
func TestNonTextToolContentIsPreserved(t *testing.T) {
396437
server, httpServer := newTestHTTPServer(t)
397438
var imageContent tools.ContentItem

kernel/mcp/tools/skill.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,11 @@ func skillLoad(args map[string]any) (CallToolResult, error) {
101101

102102
var result string
103103
if loaded.ResourcePath == "" {
104-
// 变量(非敏感)在技能正文注入对话时解析
104+
// Variables.Resolve 还会匹配 $NAME 和 ${NAME}
105105
// 资源可能包含脚本或模板,因此只解析技能正文,避免误改资源内容。
106106
loaded.Content = model.Conf.Variables.Resolve(loaded.Content)
107107
result = formatSkillContent(loaded)
108108
} else {
109-
// 不做变量解析注入,避免误杀 $NAME ${NAME} 等 reference 文件中常见模式
110109
result = formatSkillResource(loaded)
111110
}
112111
return CallToolResult{

kernel/util/skill.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"regexp"
2929
"sort"
3030
"strings"
31+
"unicode/utf8"
3132

3233
"github.com/88250/gulu"
3334
"github.com/siyuan-note/filelock"
@@ -386,6 +387,9 @@ func readSkillResource(skillDir, skillName, resource string) (string, error) {
386387
if len(data) > maxSkillResourceBytes {
387388
return "", fmt.Errorf("skill resource exceeds the %d byte limit: %s/%s", maxSkillResourceBytes, skillName, resource)
388389
}
390+
if !utf8.Valid(data) {
391+
return "", fmt.Errorf("skill resource is not valid UTF-8: %s/%s", skillName, resource)
392+
}
389393
return string(data), nil
390394
}
391395

kernel/util/skill_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,10 @@ func TestLoadSkillReadsOnlyEnabledUserResources(t *testing.T) {
222222
}
223223
}
224224

225-
func TestLoadSkillRejectsUnsafeOrOversizedResources(t *testing.T) {
225+
func TestLoadSkillRejectsUnsafeOrUnsupportedResources(t *testing.T) {
226226
workspaceRoot, _ := setSkillTestRoots(t)
227227
writeSkill(t, workspaceRoot, "safe", "safe", "description", "body")
228-
legacyEncoded := []byte{0xd6, 0xd0, 0xce, 0xc4}
229-
writeSkillResource(t, workspaceRoot, "safe", "legacy-gbk.txt", legacyEncoded)
228+
writeSkillResource(t, workspaceRoot, "safe", "invalid-utf8.txt", []byte{0xd6, 0xd0, 0xce, 0xc4})
230229
writeSkillResource(t, workspaceRoot, "safe", "limit.txt", bytes.Repeat([]byte("x"), maxSkillResourceBytes))
231230
writeSkillResource(t, workspaceRoot, "safe", "large.txt", bytes.Repeat([]byte("x"), maxSkillResourceBytes+1))
232231

@@ -240,9 +239,9 @@ func TestLoadSkillRejectsUnsafeOrOversizedResources(t *testing.T) {
240239
t.Errorf("LoadSkill(%q) error = %v", locator, err)
241240
}
242241
}
243-
legacy, err := LoadSkill("safe/legacy-gbk.txt", nil)
244-
if err != nil || !bytes.Equal([]byte(legacy.Content), legacyEncoded) {
245-
t.Fatalf("legacy-encoded resource = %v, %v", []byte(legacy.Content), err)
242+
if _, err := LoadSkill("safe/invalid-utf8.txt", nil); err == nil ||
243+
!strings.Contains(err.Error(), "skill resource is not valid UTF-8") {
244+
t.Fatalf("invalid UTF-8 resource error = %v", err)
246245
}
247246
atLimit, err := LoadSkill("safe/limit.txt", nil)
248247
if err != nil || len(atLimit.Content) != maxSkillResourceBytes {

0 commit comments

Comments
 (0)