Add installer filters for risk, category, and tags so maintainers and users can ship smaller skill surfaces to context-sensitive runtimes. Document the reduced-install flow for OpenCode-style hosts, add the humanize-chinese community skill, and sync the generated catalog and plugin-safe artifacts that now reflect the release batch. Refs #437 Refs #440 Refs #443
143 lines
4.2 KiB
JavaScript
143 lines
4.2 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
|
|
const { listSkillIds, listSkillIdsRecursive, readSkill } = 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;
|
|
}
|
|
});
|
|
|
|
withTempDir((root) => {
|
|
const skillsDir = path.join(root, "skills");
|
|
const skillDir = path.join(skillsDir, "metadata-skill");
|
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: metadata-skill
|
|
category: backend
|
|
risk: safe
|
|
metadata:
|
|
tags: "[api, saas]"
|
|
---
|
|
|
|
# metadata-skill
|
|
`,
|
|
"utf8",
|
|
);
|
|
|
|
const skill = readSkill(skillsDir, "metadata-skill");
|
|
|
|
assert.strictEqual(skill.category, "backend", "readSkill should expose category metadata");
|
|
assert.strictEqual(skill.risk, "safe", "readSkill should expose risk metadata");
|
|
assert.deepStrictEqual(
|
|
skill.tags,
|
|
["api", "saas"],
|
|
"readSkill should normalize inline tag lists from metadata.tags",
|
|
);
|
|
});
|