{"record":{"id":"0a505724b626b1b4","repo":"siyuan-note/siyuan","slug":"skill-storage-is-not-a-regular-directory","errorCode":null,"errorMessage":"skill storage is not a regular directory","messagePattern":"skill storage is not a regular directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"kernel/util/skill_manage.go","lineNumber":96,"sourceCode":"}\n\n// 从工作空间目录逐层打开根目录，拒绝把管理操作重定向到链接目标。\nfunc openManagedSkillsRoot() (*os.Root, error) {\n\tworkspace, err := os.OpenRoot(DataDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer workspace.Close()\n\tfor _, p := range []string{\"storage\", \"storage/ai\", \"storage/ai/agent\", \"storage/ai/agent/skills\"} {\n\t\tif err = workspace.Mkdir(p, 0755); err != nil && !errors.Is(err, os.ErrExist) {\n\t\t\treturn nil, err\n\t\t}\n\t\tinfo, statErr := workspace.Lstat(p)\n\t\tif statErr != nil {\n\t\t\treturn nil, statErr\n\t\t}\n\t\tif !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {\n\t\t\treturn nil, errors.New(\"skill storage is not a regular directory\")\n\t\t}\n\t}\n\treturn workspace.OpenRoot(\"storage/ai/agent/skills\")\n}\n\nfunc checkManagedSkillPath(root *os.Root, p string) error {\n\tif err := validateManagedSkillPath(p); err != nil {\n\t\treturn err\n\t}\n\tparts := strings.Split(p, \"/\")\n\tfor i := range parts {\n\t\tinfo, err := root.Lstat(strings.Join(parts[:i+1], \"/\"))\n\t\tif errors.Is(err, os.ErrNotExist) && i == len(parts)-1 {\n\t\t\treturn nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/skill_manage.go#L78-L114","documentation":"openManagedSkillsRoot walks data/storage/ai/agent/skills creating each level and Lstats it; if any level exists but is not a real directory (a regular file, or a symlink), the error is thrown. This guards against storage layout tampering and symlink redirection of skill management operations.","triggerScenarios":"Any ManageSkillFiles action or TestSkillManagementCommitConflict when data/storage, data/storage/ai, data/storage/ai/agent, or data/storage/ai/agent/skills is a file or a symlink instead of a directory.","commonSituations":"A user/script replaced 'skills' with a symlink to a synced folder; a stray file named 'ai' was created inside storage; an interrupted migration left a file where a directory belongs; restoring data from a partial backup.","solutions":["Inspect data/storage/ai/agent/skills (and each parent) — remove or rename the file/symlink and create a real directory in its place","Replace the symlink with a real directory (copy contents back if the symlink pointed at your data)","If a stray file occupies a path meant to be a directory, delete it so Mkdir can recreate the level","Restore the storage subtree from a good backup if the layout is corrupted"],"exampleFix":"// before\n$ ls -l data/storage/ai/agent\nskills -> /mnt/cloud/skills   # symlink\n\n// after\n$ rm data/storage/ai/agent/skills\n$ mkdir data/storage/ai/agent/skills\n$ cp /mnt/cloud/skills/* data/storage/ai/agent/skills/\n","handlingStrategy":"try-catch","validationCode":"const st = await fs.lstat('data/storage/ai/agent/skills');\nif (!st.isDirectory() || st.isSymbolicLink()) {\n  throw new Error('skills storage must be a real directory');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.manageSkillFiles({action: 'list', path: '.'});\n} catch (e) {\n  if (e.message.includes('not a regular directory')) {\n    // fix storage layout: replace file/symlink with a real directory\n  }\n}","preventionTips":["Do not symlink data/storage subdirectories to synced locations","Back up and verify data/storage layout after restores","Avoid creating stray files where directory levels are expected"],"tags":["filesystem","symlink","storage-layout","go"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}