const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); const { listSkillIds, listSkillIdsRecursive } = require("../../lib/skill-utils"); function withTempDir(fn) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skill-utils-security-")); try { fn(dir); } finally { fs.rmSync(dir, { recursive: true, force: true }); } } withTempDir((root) => { const skillsDir = path.join(root, "skills"); const outsideDir = path.join(root, "outside-secret"); fs.mkdirSync(skillsDir, { recursive: true }); fs.mkdirSync(outsideDir, { recursive: true }); fs.mkdirSync(path.join(skillsDir, "safe-skill")); fs.writeFileSync(path.join(skillsDir, "safe-skill", "SKILL.md"), "# safe\n"); fs.writeFileSync(path.join(outsideDir, "SKILL.md"), "# secret\n"); fs.symlinkSync(outsideDir, path.join(skillsDir, "linked-secret")); const skillIds = listSkillIds(skillsDir); assert.deepStrictEqual( skillIds, ["safe-skill"], "symlinked skill directories must not be treated as local skills", ); }); withTempDir((root) => { const skillsDir = path.join(root, "skills"); const outsideDir = path.join(root, "outside-secret"); fs.mkdirSync(skillsDir, { recursive: true }); fs.mkdirSync(outsideDir, { recursive: true }); fs.mkdirSync(path.join(skillsDir, "nested", "safe-skill"), { recursive: true }); fs.writeFileSync(path.join(skillsDir, "nested", "safe-skill", "SKILL.md"), "# safe\n"); fs.mkdirSync(path.join(outsideDir, "loop-target"), { recursive: true }); fs.symlinkSync(outsideDir, path.join(skillsDir, "nested", "linked-secret")); const skillIds = listSkillIdsRecursive(skillsDir); assert.deepStrictEqual( skillIds, ["nested/safe-skill"], "recursive skill listing must ignore symlinked directories", ); }); withTempDir((root) => { const skillsDir = path.join(root, "skills"); let currentDir = skillsDir; fs.mkdirSync(skillsDir, { recursive: true }); currentDir = skillsDir; const originalReaddirSync = fs.readdirSync; const originalLstatSync = fs.lstatSync; try { const depth = 1500; const directorySet = new Set([skillsDir]); for (let i = 0; i < depth; i += 1) { currentDir = path.join(currentDir, `d${i}`); directorySet.add(currentDir); } const deepestSkill = path.join(currentDir, "SKILL.md"); fs.readdirSync = (targetPath, options) => { if (targetPath === skillsDir) { return [{ name: "d0", isDirectory: () => true }]; } const match = targetPath.match(/\/d(\d+)$/); if (!match) return originalReaddirSync(targetPath, options); const index = Number(match[1]); if (index >= depth - 1) { return []; } return [{ name: `d${index + 1}`, isDirectory: () => true }]; }; fs.lstatSync = (targetPath) => { if (directorySet.has(targetPath)) { return { isDirectory: () => true, isSymbolicLink: () => false, isFile: () => false }; } if (targetPath === deepestSkill) { return { isDirectory: () => false, isSymbolicLink: () => false, isFile: () => true }; } return originalLstatSync(targetPath); }; const skillIds = listSkillIdsRecursive(skillsDir); assert.strictEqual(skillIds.length, 1, "deep trees should still produce exactly one skill"); assert.match(skillIds[0], /d1499$/, "deepest nested skill should be discovered without stack overflow"); } finally { fs.readdirSync = originalReaddirSync; fs.lstatSync = originalLstatSync; } });