📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -44,6 +44,8 @@ function parseArgs(argv = process.argv.slice(2)) {
|
||||
let skillsArg = null;
|
||||
let versionInfo = false;
|
||||
let dryRun = false;
|
||||
let installAll = false;
|
||||
let auditOnly = false;
|
||||
let cursor = false,
|
||||
claude = false,
|
||||
gemini = false,
|
||||
@@ -77,6 +79,10 @@ function parseArgs(argv = process.argv.slice(2)) {
|
||||
dryRun = true;
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "--all") {
|
||||
installAll = true;
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "--cursor") {
|
||||
cursor = true;
|
||||
continue;
|
||||
@@ -106,6 +112,10 @@ function parseArgs(argv = process.argv.slice(2)) {
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "install") continue;
|
||||
if (a[i] === "audit") {
|
||||
auditOnly = true;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown option or command: ${a[i]}`);
|
||||
}
|
||||
|
||||
@@ -119,6 +129,8 @@ function parseArgs(argv = process.argv.slice(2)) {
|
||||
skillsArg,
|
||||
versionInfo,
|
||||
dryRun,
|
||||
installAll,
|
||||
auditOnly,
|
||||
cursor,
|
||||
claude,
|
||||
gemini,
|
||||
@@ -189,23 +201,26 @@ Options:
|
||||
--category <csv> Install only skills matching these categories
|
||||
--tags <csv> Install only skills matching these tags
|
||||
--skills <csv> Set exact managed skill names, ids, or nested skill paths
|
||||
--all Explicitly select the complete catalog, including offensive/unknown skills
|
||||
--dry-run Preview installs/updates/removals for every target without writing
|
||||
--version Print the installer version
|
||||
--release <ver> Clone tag v<ver> (e.g. 4.6.0 -> v4.6.0)
|
||||
--tag <tag> Clone this tag or branch (e.g. v4.6.0, main)
|
||||
|
||||
Examples:
|
||||
npx agentic-awesome-skills
|
||||
npx agentic-awesome-skills --cursor
|
||||
npx agentic-awesome-skills --kiro
|
||||
npx agentic-awesome-skills --antigravity
|
||||
npx agentic-awesome-skills --agy
|
||||
npx agentic-awesome-skills --skills brainstorming --dry-run
|
||||
npx agentic-awesome-skills audit --skills brainstorming
|
||||
npx agentic-awesome-skills --cursor --risk safe,none
|
||||
npx agentic-awesome-skills --all --dry-run
|
||||
npx agentic-awesome-skills --kiro --skills brainstorming
|
||||
npx agentic-awesome-skills --antigravity --risk safe,none
|
||||
npx agentic-awesome-skills --agy --skills brainstorming
|
||||
npx agentic-awesome-skills --path .agents/skills --category development,backend --risk safe,none
|
||||
npx agentic-awesome-skills --path .agents/skills --tags debugging,typescript-legacy-
|
||||
npx agentic-awesome-skills --codex --skills frontend-design,game-development/2d-games --dry-run
|
||||
npx agentic-awesome-skills --release 4.6.0
|
||||
npx agentic-awesome-skills --path ./my-skills
|
||||
npx agentic-awesome-skills --claude --codex Install to multiple targets
|
||||
npx agentic-awesome-skills --release 4.6.0 --skills brainstorming
|
||||
npx agentic-awesome-skills --path ./my-skills --skills brainstorming
|
||||
npx agentic-awesome-skills --claude --codex --skills brainstorming
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -271,6 +286,33 @@ function hasInstallSelectors(selectors) {
|
||||
return Object.values(selectors).some(hasActiveSelector);
|
||||
}
|
||||
|
||||
function assertExplicitInstallSelection(opts, selectors, requestedSkills) {
|
||||
const hasSelection = hasInstallSelectors(selectors) || requestedSkills.length > 0;
|
||||
if (opts.installAll && hasSelection) {
|
||||
throw new Error("--all cannot be combined with --skills, --risk, --category, or --tags.");
|
||||
}
|
||||
if (opts.auditOnly && requestedSkills.length === 0) {
|
||||
throw new Error("The audit command requires --skills with one or more exact skill ids.");
|
||||
}
|
||||
}
|
||||
|
||||
function buildRiskSummary(repoRoot, installEntries) {
|
||||
const skillsRoot = path.join(repoRoot, "skills");
|
||||
const summary = {};
|
||||
for (const entry of installEntries.filter((item) => item !== "docs")) {
|
||||
const risk = normalizeFilterValue(readSkill(skillsRoot, normalizeSourceEntry(entry)).risk) || "unclassified";
|
||||
summary[risk] = (summary[risk] || 0) + 1;
|
||||
}
|
||||
return Object.fromEntries(Object.entries(summary).sort(([left], [right]) => left.localeCompare(right)));
|
||||
}
|
||||
|
||||
function printImplicitFullInstallWarning(riskSummary) {
|
||||
console.warn("\nWARNING: no skill selection was supplied, so the complete catalog will be installed for backward compatibility.");
|
||||
console.warn(`Risk summary: ${Object.entries(riskSummary).map(([risk, count]) => `${risk}=${count}`).join(", ")}`);
|
||||
console.warn("This can include offensive and unknown-risk skills. Prefer --skills, --risk, --category, or --tags.");
|
||||
console.warn("Use audit --skills <ids> and --dry-run to inspect content and the write plan before installation.\n");
|
||||
}
|
||||
|
||||
function matchesScalarSelector(value, selector) {
|
||||
const normalized = normalizeFilterValue(value);
|
||||
if (normalized && selector.exclude.includes(normalized)) {
|
||||
@@ -501,6 +543,86 @@ function getInstallEntries(tempDir, selectors = buildInstallSelectors({}), reque
|
||||
return entries;
|
||||
}
|
||||
|
||||
const AUDIT_PATTERNS = [
|
||||
["external-install", /\b(?:git\s+clone|npm\s+install|npx\s+|pip(?:3)?\s+install|brew\s+install|apt(?:-get)?\s+install|plugin\s+marketplace\s+add)\b/i],
|
||||
["network", /\b(?:curl|wget|Invoke-WebRequest|irm\s+https?:\/\/|fetch\s*\(|https?:\/\/)\b/i],
|
||||
["credential", /\b(?:api[_ -]?key|access[_ -]?token|secret|password|private[_ -]?key|wallet|seed phrase)\b/i],
|
||||
["filesystem-write", /\b(?:cp|mv|rm|chmod|chown|mkdir|git\s+reset|Set-Content|Add-Content|Remove-Item)\b/i],
|
||||
["privileged", /\b(?:sudo|runas|administrator|systemctl|launchctl)\b/i],
|
||||
["destructive-or-irreversible", /\b(?:rm\s+-rf|git\s+reset\s+--hard|drop\s+(?:database|table)|broadcast(?:ing)?\s+(?:a\s+)?transaction|format\s+[A-Z]:)\b/i],
|
||||
];
|
||||
|
||||
function listAuditFiles(rootDir) {
|
||||
const files = [];
|
||||
const walk = (current) => {
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const entryPath = path.join(current, entry.name);
|
||||
if (entry.isSymbolicLink()) {
|
||||
files.push({ path: entryPath, symlink: true });
|
||||
} else if (entry.isDirectory()) {
|
||||
walk(entryPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.push({ path: entryPath, symlink: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(rootDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
function auditSkillEntries(repoRoot, installEntries) {
|
||||
const skillsRoot = path.join(repoRoot, "skills");
|
||||
return installEntries
|
||||
.filter((entry) => entry !== "docs")
|
||||
.map((entry) => {
|
||||
const skillRoot = path.join(skillsRoot, normalizeSourceEntry(entry));
|
||||
const findings = [];
|
||||
for (const item of listAuditFiles(skillRoot)) {
|
||||
const relativePath = path.relative(skillRoot, item.path).split(path.sep).join("/");
|
||||
if (item.symlink) {
|
||||
findings.push({ file: relativePath, line: null, categories: ["symlink"], text: "Symbolic link" });
|
||||
continue;
|
||||
}
|
||||
const stats = fs.statSync(item.path);
|
||||
if (stats.size > 1024 * 1024) {
|
||||
findings.push({ file: relativePath, line: null, categories: ["large-unread-file"], text: `${stats.size} bytes` });
|
||||
continue;
|
||||
}
|
||||
const buffer = fs.readFileSync(item.path);
|
||||
if (buffer.includes(0)) {
|
||||
findings.push({ file: relativePath, line: null, categories: ["binary-file"], text: `${stats.size} bytes` });
|
||||
continue;
|
||||
}
|
||||
const lines = buffer.toString("utf8").split(/\r?\n/);
|
||||
lines.forEach((line, index) => {
|
||||
const categories = AUDIT_PATTERNS.filter(([, pattern]) => pattern.test(line)).map(([name]) => name);
|
||||
if (categories.length > 0) {
|
||||
findings.push({ file: relativePath, line: index + 1, categories, text: line.trim().slice(0, 240) });
|
||||
}
|
||||
});
|
||||
}
|
||||
return { skill: normalizeInstallEntry(entry), findings };
|
||||
});
|
||||
}
|
||||
|
||||
function printAuditReport(report, ref) {
|
||||
console.log("\nStatic pre-install audit. No skill content was executed or installed.");
|
||||
console.log(`Ref: ${ref || "default release"}`);
|
||||
for (const skill of report) {
|
||||
console.log(`\n${skill.skill}: ${skill.findings.length} review signal(s)`);
|
||||
if (skill.findings.length === 0) {
|
||||
console.log(" No command, network, credential, filesystem, privilege, binary, or symlink signals found.");
|
||||
continue;
|
||||
}
|
||||
for (const finding of skill.findings) {
|
||||
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
||||
console.log(` [${finding.categories.join(", ")}] ${location}`);
|
||||
console.log(` ${finding.text}`);
|
||||
}
|
||||
}
|
||||
console.log("\nReview every signal and every external source before installation. This static report is not a guarantee of safety.");
|
||||
}
|
||||
|
||||
function installSkillsIntoTarget(tempDir, target, installEntries) {
|
||||
const repoSkills = path.join(tempDir, "skills");
|
||||
const selectedSkillEntries = new Set(
|
||||
@@ -937,8 +1059,16 @@ function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const targets = getTargets(opts);
|
||||
if (!targets.length || (!HOME && !opts.pathArg)) {
|
||||
try {
|
||||
assertExplicitInstallSelection(opts, selectors, requestedSkills);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const targets = opts.auditOnly ? [] : getTargets(opts);
|
||||
if (!opts.auditOnly && (!targets.length || (!HOME && !opts.pathArg))) {
|
||||
console.error(
|
||||
"Could not resolve home directory. Use --path <absolute-path>.",
|
||||
);
|
||||
@@ -966,6 +1096,15 @@ function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.installAll && requestedSkills.length === 0 && !hasInstallSelectors(selectors)) {
|
||||
printImplicitFullInstallWarning(buildRiskSummary(tempDir, installEntries));
|
||||
}
|
||||
|
||||
if (opts.auditOnly) {
|
||||
printAuditReport(auditSkillEntries(tempDir, installEntries), ref);
|
||||
return;
|
||||
}
|
||||
|
||||
// Preflight every target before mutating the first one. The same plan is
|
||||
// printed for --dry-run and acts as the multi-target safety gate for apply.
|
||||
let dryRunPlan;
|
||||
@@ -1022,6 +1161,9 @@ module.exports = {
|
||||
buildCloneArgs,
|
||||
buildDryRunPlan,
|
||||
buildDryRunTargetPlan,
|
||||
assertExplicitInstallSelection,
|
||||
auditSkillEntries,
|
||||
buildRiskSummary,
|
||||
buildInstallSelectors,
|
||||
getInstallEntries,
|
||||
getManagedEntries,
|
||||
@@ -1037,6 +1179,8 @@ module.exports = {
|
||||
parseSelectorArg,
|
||||
printDryRunPlan,
|
||||
parseArgs,
|
||||
printImplicitFullInstallWarning,
|
||||
printAuditReport,
|
||||
pruneRemovedEntries,
|
||||
readInstallManifest,
|
||||
resolveExactSkillSelections,
|
||||
|
||||
@@ -41,7 +41,18 @@ LIMITATIONS_HEADING_PATTERNS = [
|
||||
]
|
||||
MARKDOWN_LINK_PATTERN = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
|
||||
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
SECURITY_DISCLAIMER_PATTERN = re.compile(r"AUTHORIZED USE ONLY", re.IGNORECASE)
|
||||
SECURITY_DISCLAIMER_PATTERN = re.compile(
|
||||
r"> \*\*⚠️ AUTHORIZED USE ONLY\*\*\s*\n"
|
||||
r"> This skill is for educational purposes or authorized security assessments only\.\s*\n"
|
||||
r"> You must have explicit, written permission from the system owner before using this tool\.\s*\n"
|
||||
r"> Misuse of this tool is illegal and strictly prohibited\.",
|
||||
)
|
||||
OFFENSIVE_CONFIRMATION_PATTERN = re.compile(
|
||||
r"Mandatory confirmation gate[\s\S]{0,900}"
|
||||
r"exact target URL, IP, account, or resource[\s\S]{0,900}"
|
||||
r"Wait for explicit confirmation in the current conversation",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
VALID_RISK_LEVELS = {"none", "safe", "critical", "offensive", "unknown"}
|
||||
DEFAULT_MARKDOWN_TOP_FINDINGS = 15
|
||||
DEFAULT_MARKDOWN_TOP_SKILLS = 20
|
||||
@@ -244,6 +255,14 @@ def build_skill_report(
|
||||
"Offensive skill is missing the required 'AUTHORIZED USE ONLY' disclaimer.",
|
||||
)
|
||||
)
|
||||
if risk == "offensive" and not OFFENSIVE_CONFIRMATION_PATTERN.search(content):
|
||||
findings.append(
|
||||
Finding(
|
||||
"error",
|
||||
"missing_offensive_confirmation_gate",
|
||||
"Offensive skill is missing the mandatory target, authorization, command preview, and explicit confirmation gate.",
|
||||
)
|
||||
)
|
||||
|
||||
return finalize_skill_report(
|
||||
rel_dir,
|
||||
|
||||
@@ -21,6 +21,7 @@ from git_change_records import (
|
||||
ChangeRecord,
|
||||
list_tree,
|
||||
materialize_tree,
|
||||
read_blob,
|
||||
read_change_records,
|
||||
read_path,
|
||||
resolve_commit,
|
||||
@@ -118,11 +119,14 @@ def canonical_skill_id(path: str | None, roots: set[str]) -> str | None:
|
||||
if path is None or not path.startswith("skills/"):
|
||||
return None
|
||||
relative = path[len("skills/") :]
|
||||
candidates = [
|
||||
root
|
||||
for root in roots
|
||||
if relative == f"{root}/SKILL.md" or relative.startswith(f"{root}/")
|
||||
]
|
||||
parts = relative.split("/")
|
||||
candidates = []
|
||||
for end in range(1, len(parts)):
|
||||
root = "/".join(parts[:end])
|
||||
if root in roots and (
|
||||
relative == f"{root}/SKILL.md" or relative.startswith(f"{root}/")
|
||||
):
|
||||
candidates.append(root)
|
||||
return max(candidates, key=lambda root: (root.count("/"), len(root))) if candidates else None
|
||||
|
||||
|
||||
@@ -275,6 +279,44 @@ def evaluate_snapshot(snapshot_root: Path, skill_id: str) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def materialize_skill_snapshot(
|
||||
repo: Path,
|
||||
commit_oid: str,
|
||||
skill_id: str,
|
||||
snapshot_root: Path,
|
||||
) -> list[dict[str, str]]:
|
||||
"""Materialize a skill, parsing a legacy executable SKILL.md as inert data.
|
||||
|
||||
The generic tree helper intentionally rejects every executable blob. Some
|
||||
historical canonical SKILL.md files nevertheless use mode 100755. Reading
|
||||
that exact documentation blob into a private 0600 file lets the evidence
|
||||
evaluator compare before/after metadata without executing it or admitting
|
||||
any other unsafe tree entry.
|
||||
"""
|
||||
skill_root = snapshot_root / "skills" / skill_id
|
||||
unsafe = materialize_tree(
|
||||
repo,
|
||||
commit_oid,
|
||||
f"skills/{skill_id}",
|
||||
skill_root,
|
||||
)
|
||||
skill_file = skill_root / "SKILL.md"
|
||||
if not skill_file.exists():
|
||||
expected_path = f"skills/{skill_id}/SKILL.md"
|
||||
legacy_entry = next(
|
||||
(
|
||||
entry
|
||||
for entry in unsafe
|
||||
if entry["path"] == expected_path and entry["mode"] == "100755"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if legacy_entry is not None:
|
||||
skill_file.write_bytes(read_blob(repo, legacy_entry["oid"]))
|
||||
skill_file.chmod(0o600)
|
||||
return unsafe
|
||||
|
||||
|
||||
def _audit_severities(snapshot: dict[str, object] | None) -> dict[str, list[str]]:
|
||||
severities: dict[str, list[str]] = defaultdict(list)
|
||||
if not snapshot:
|
||||
@@ -454,12 +496,12 @@ def build_report(
|
||||
after_unsafe: list[dict[str, str]] = []
|
||||
if base_exists and old_id:
|
||||
before_root = temp_root / f"{index}-before"
|
||||
before_unsafe = materialize_tree(root, base_oid, f"skills/{old_id}", before_root / "skills" / old_id)
|
||||
before_unsafe = materialize_skill_snapshot(root, base_oid, old_id, before_root)
|
||||
if (before_root / "skills" / old_id / "SKILL.md").is_file():
|
||||
before = evaluate_snapshot(before_root, old_id)
|
||||
if head_exists and new_id:
|
||||
after_root = temp_root / f"{index}-after"
|
||||
after_unsafe = materialize_tree(root, head_oid, f"skills/{new_id}", after_root / "skills" / new_id)
|
||||
after_unsafe = materialize_skill_snapshot(root, head_oid, new_id, after_root)
|
||||
if (after_root / "skills" / new_id / "SKILL.md").is_file():
|
||||
after = evaluate_snapshot(after_root, new_id)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ const DISALLOWED_COAUTHOR_TRAILER_PATTERNS = [
|
||||
];
|
||||
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
||||
const EVIDENCE_SCHEMA_VERSION = 1;
|
||||
const EVIDENCE_TIMEOUT_MS = 120_000;
|
||||
const EVIDENCE_TIMEOUT_MS = 300_000;
|
||||
const MAX_EVIDENCE_BYTES = 8 * 1024 * 1024;
|
||||
const APPROVAL_WORKFLOW_PATHS = new Set([
|
||||
".github/workflows/actionlint.yml",
|
||||
@@ -1314,6 +1314,7 @@ if (require.main === module) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EVIDENCE_TIMEOUT_MS,
|
||||
approvalWorkflowPaths: APPROVAL_WORKFLOW_PATHS,
|
||||
approveActionRequiredRuns,
|
||||
approveWorkflowRun,
|
||||
|
||||
@@ -99,6 +99,12 @@ const eclEnvironmentGuide = fs.readFileSync(
|
||||
'utf8',
|
||||
);
|
||||
const lovableCleanupSkill = fs.readFileSync(path.join(repoRoot, 'skills', 'lovable-cleanup', 'SKILL.md'), 'utf8');
|
||||
const blueprintSkill = fs.readFileSync(path.join(repoRoot, 'skills', 'blueprint', 'SKILL.md'), 'utf8');
|
||||
const uiUpdateSkill = fs.readFileSync(path.join(repoRoot, 'skills', 'ui-update', 'SKILL.md'), 'utf8');
|
||||
const xTwitterScraperSkill = fs.readFileSync(path.join(repoRoot, 'skills', 'x-twitter-scraper', 'SKILL.md'), 'utf8');
|
||||
const agentMemoryMcpSkill = fs.readFileSync(path.join(repoRoot, 'skills', 'agent-memory-mcp', 'SKILL.md'), 'utf8');
|
||||
const speckitUpdaterSkill = fs.readFileSync(path.join(repoRoot, 'skills', 'speckit-updater', 'SKILL.md'), 'utf8');
|
||||
const securityAntivirusGuide = fs.readFileSync(path.join(repoRoot, 'docs', 'users', 'security-and-antivirus.md'), 'utf8');
|
||||
|
||||
function fencedBlocks(content, language) {
|
||||
const blocks = [];
|
||||
@@ -522,6 +528,21 @@ assert.match(
|
||||
/react-native-keychain|expo-secure-store/,
|
||||
'React Native reference should direct token storage to platform-backed secure storage',
|
||||
);
|
||||
for (const [name, content] of [
|
||||
['blueprint', blueprintSkill],
|
||||
['ui-update', uiUpdateSkill],
|
||||
['x-twitter-scraper', xTwitterScraperSkill],
|
||||
['agent-memory-mcp', agentMemoryMcpSkill],
|
||||
]) {
|
||||
assert.match(content, /checkout --detach [0-9a-f]{40}/, `${name} must pin its reviewed external source`);
|
||||
assert.match(content, /mktemp -d/, `${name} must review external content outside active skill paths`);
|
||||
assert.match(content, /explicit (?:user )?approval/i, `${name} must require approval before activation`);
|
||||
}
|
||||
assert.doesNotMatch(uiUpdateSkill, /git reset --hard|Always safe \(do without asking\)|safe and reversible/i);
|
||||
assert.doesNotMatch(speckitUpdaterSkill, /C:\\Users\\bobby/i);
|
||||
assert.match(securityAntivirusGuide, /does not itself run/i);
|
||||
assert.match(securityAntivirusGuide, /does not prove safety/i);
|
||||
assert.match(securityAntivirusGuide, /evidence of execution/i);
|
||||
|
||||
for (const scriptName of ['generate_slides.py', 'create_pdf_slides.py']) {
|
||||
const helpRun = spawnSync(
|
||||
|
||||
@@ -23,6 +23,8 @@ const exactPreview = installer.parseArgs([
|
||||
]);
|
||||
assert.strictEqual(exactPreview.skillsArg, 'frontend-design,game-development/2d-games');
|
||||
assert.strictEqual(exactPreview.dryRun, true);
|
||||
assert.strictEqual(installer.parseArgs(['--all', '--dry-run']).installAll, true);
|
||||
assert.strictEqual(installer.parseArgs(['audit', '--skills', 'frontend-design']).auditOnly, true);
|
||||
assert.deepStrictEqual(
|
||||
installer.parseExactSkillArg(exactPreview.skillsArg),
|
||||
['frontend-design', 'game-development/2d-games'],
|
||||
@@ -40,3 +42,20 @@ assert.doesNotMatch(version.stdout, /Cloning repository/i);
|
||||
const invalid = spawnSync(process.execPath, [installerPath, '--unknown'], { encoding: 'utf8' });
|
||||
assert.notStrictEqual(invalid.status, 0);
|
||||
assert.match(invalid.stderr, /unknown option/i);
|
||||
|
||||
assert.throws(
|
||||
() => installer.assertExplicitInstallSelection(
|
||||
installer.parseArgs(['--all', '--skills', 'frontend-design']),
|
||||
installer.buildInstallSelectors({}),
|
||||
['frontend-design'],
|
||||
),
|
||||
/--all cannot be combined/i,
|
||||
);
|
||||
assert.throws(
|
||||
() => installer.assertExplicitInstallSelection(
|
||||
installer.parseArgs(['audit']),
|
||||
installer.buildInstallSelectors({}),
|
||||
[],
|
||||
),
|
||||
/audit command requires --skills/i,
|
||||
);
|
||||
|
||||
@@ -203,3 +203,27 @@ withTempDir((root) => {
|
||||
"a nested skill that does not match filters must not leak into the installation",
|
||||
);
|
||||
});
|
||||
|
||||
withTempDir((root) => {
|
||||
const repoRoot = path.join(root, "repo");
|
||||
fs.mkdirSync(path.join(repoRoot, "skills"), { recursive: true });
|
||||
fs.mkdirSync(path.join(repoRoot, "docs"), { recursive: true });
|
||||
writeSkill(
|
||||
repoRoot,
|
||||
"audited-skill",
|
||||
'name: audited-skill\ncategory: testing\nrisk: unknown\ntags: [audit]',
|
||||
);
|
||||
fs.appendFileSync(
|
||||
path.join(repoRoot, "skills", "audited-skill", "SKILL.md"),
|
||||
"\n```bash\ngit clone https://example.com/tool.git /tmp/tool\nrm -rf /tmp/tool\n```\n",
|
||||
);
|
||||
const report = installer.auditSkillEntries(repoRoot, ["audited-skill", "docs"]);
|
||||
assert.strictEqual(report.length, 1);
|
||||
assert.strictEqual(report[0].skill, "audited-skill");
|
||||
assert.ok(report[0].findings.some((finding) => finding.categories.includes("external-install")));
|
||||
assert.ok(report[0].findings.some((finding) => finding.categories.includes("destructive-or-irreversible")));
|
||||
assert.deepStrictEqual(
|
||||
installer.buildRiskSummary(repoRoot, ["audited-skill", "docs"]),
|
||||
{ unknown: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,12 @@ const HEAD_SHA = "2".repeat(40);
|
||||
const BLOB_SHA = "3".repeat(40);
|
||||
const ZERO_SHA = "0".repeat(40);
|
||||
|
||||
assert.strictEqual(
|
||||
mergeBatch.EVIDENCE_TIMEOUT_MS,
|
||||
300_000,
|
||||
"repository-wide trusted evidence must have a five-minute execution budget",
|
||||
);
|
||||
|
||||
function makeCheckRun(name, status, conclusion, startedAt, id, suiteId = 1) {
|
||||
return {
|
||||
name,
|
||||
|
||||
@@ -209,3 +209,10 @@ test("canonical security fixes are synchronized to distributed plugin mirrors",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("BrowserAct never delegates its operating policy to mutable provider guides", () => {
|
||||
const skill = read("skills/browser-act/SKILL.md");
|
||||
assert.doesNotMatch(skill, /^browser-act get-skills\b/m);
|
||||
assert.match(skill, /checked-in Skill remains the complete operating policy/);
|
||||
assert.match(skill, /browser-act <subcommand> --help/);
|
||||
});
|
||||
|
||||
@@ -96,6 +96,23 @@ def init_repo(*, with_skill: bool = True) -> tuple[Path, str]:
|
||||
|
||||
|
||||
class ChangedSkillEvidenceTests(unittest.TestCase):
|
||||
def test_canonical_skill_lookup_checks_only_path_ancestors(self):
|
||||
class NonIterableRoots(set):
|
||||
def __iter__(self):
|
||||
raise AssertionError("canonical lookup must not scan every skill root")
|
||||
|
||||
roots = NonIterableRoots({"parent", "parent/child", "unrelated"})
|
||||
|
||||
self.assertEqual(
|
||||
changed_skill_evidence.canonical_skill_id(
|
||||
"skills/parent/child/references/example.md", roots
|
||||
),
|
||||
"parent/child",
|
||||
)
|
||||
self.assertIsNone(
|
||||
changed_skill_evidence.canonical_skill_id("docs/example.md", roots)
|
||||
)
|
||||
|
||||
def test_mixed_copy_and_rename_keep_distinct_change_types(self):
|
||||
root, base = init_repo()
|
||||
original = root / "skills/example/SKILL.md"
|
||||
@@ -150,6 +167,30 @@ class ChangedSkillEvidenceTests(unittest.TestCase):
|
||||
|
||||
self.assertTrue(any("unsafe_snapshot_regression:100755:run.sh" in reason for reason in report["reasons"]))
|
||||
|
||||
def test_legacy_executable_skill_markdown_can_be_normalized_and_compared(self):
|
||||
root, _ = init_repo()
|
||||
path = root / "skills/example/SKILL.md"
|
||||
os.chmod(path, 0o755)
|
||||
git(root, "add", ".")
|
||||
git(root, "commit", "-m", "legacy executable skill markdown")
|
||||
base = git(root, "rev-parse", "HEAD")
|
||||
path.write_text(
|
||||
path.read_text(encoding="utf-8").replace("risk: safe", "risk: critical"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.chmod(path, 0o644)
|
||||
git(root, "add", ".")
|
||||
git(root, "commit", "-m", "normalize skill markdown")
|
||||
|
||||
report = changed_skill_evidence.build_report(root, base, "HEAD")
|
||||
|
||||
change = report["changes"][0]
|
||||
self.assertIsNotNone(change["before"])
|
||||
self.assertIsNotNone(change["after"])
|
||||
self.assertEqual(change["before"]["risk"]["declared"], "safe")
|
||||
self.assertEqual(change["after"]["risk"]["declared"], "critical")
|
||||
self.assertFalse(change["blocking"])
|
||||
|
||||
def test_skill_markdown_replaced_by_gitlink_is_not_treated_as_deletion(self):
|
||||
root, base = init_repo()
|
||||
git(root, "rm", "skills/example/SKILL.md")
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = REPO_ROOT / "skills" / "find-complementary-founders" / "scripts"
|
||||
|
||||
|
||||
def load_module(name: str, filename: str):
|
||||
spec = importlib.util.spec_from_file_location(name, SCRIPT_DIR / filename)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Cannot load {filename}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
PUBLISHER = load_module("findmate_moltbook_publish_test", "moltbook_publish.py")
|
||||
GITHUB_THREAD = load_module("findmate_github_thread_test", "github_thread.py")
|
||||
COMMIT = "0123456789abcdef0123456789abcdef01234567"
|
||||
IMMUTABLE_URL = (
|
||||
f"https://github.com/owner/repo/blob/{COMMIT}/profiles/owner-profile.public.json"
|
||||
)
|
||||
|
||||
|
||||
class FindMateProfileUrlTests(unittest.TestCase):
|
||||
def test_accepts_full_sha_pinned_github_blob(self):
|
||||
self.assertEqual(
|
||||
PUBLISHER.immutable_github_profile_url(IMMUTABLE_URL),
|
||||
IMMUTABLE_URL,
|
||||
)
|
||||
body = f"Owner-approved profile: {IMMUTABLE_URL}"
|
||||
self.assertEqual(GITHUB_THREAD.safe_profile_url(body), IMMUTABLE_URL)
|
||||
|
||||
def test_rejects_mutable_or_non_github_profile_urls(self):
|
||||
invalid_urls = [
|
||||
"https://github.com/owner/repo/blob/main/owner-profile.public.json",
|
||||
f"https://example.com/owner/repo/blob/{COMMIT}/owner-profile.public.json",
|
||||
f"https://github.com/owner/repo/blob/{COMMIT}/owner-profile.public.json?raw=1",
|
||||
f"https://github.com/owner/repo/blob/{COMMIT}/profiles/../owner-profile.public.json",
|
||||
]
|
||||
for url in invalid_urls:
|
||||
with self.subTest(url=url):
|
||||
with self.assertRaises(PUBLISHER.PublishError):
|
||||
PUBLISHER.immutable_github_profile_url(url)
|
||||
self.assertIsNone(
|
||||
GITHUB_THREAD.safe_profile_url(
|
||||
f"Owner-approved profile: {url}"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,75 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
TOOLS_SCRIPTS_DIR = REPO_ROOT / "tools" / "scripts"
|
||||
if str(TOOLS_SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(TOOLS_SCRIPTS_DIR))
|
||||
|
||||
|
||||
def load_module(relative_path: str, name: str):
|
||||
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relative_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
validate_skills = load_module("tools/scripts/validate_skills.py", "validate_skills_offensive_guardrails")
|
||||
|
||||
|
||||
EXACT_GUARDRAIL = """
|
||||
> **⚠️ AUTHORIZED USE ONLY**
|
||||
> This skill is for educational purposes or authorized security assessments only.
|
||||
> You must have explicit, written permission from the system owner before using this tool.
|
||||
> Misuse of this tool is illegal and strictly prohibited.
|
||||
|
||||
> **Mandatory confirmation gate**
|
||||
> Before running any command that probes, exploits, changes, persists on, extracts data from, or attempts credential access against a target:
|
||||
> 1. Ask the user to state the exact target URL, IP, account, or resource.
|
||||
> 2. Ask the user to confirm written authorization and the permitted scope.
|
||||
> 3. Show the exact command(s) and explain their expected effect.
|
||||
> 4. Wait for explicit confirmation in the current conversation.
|
||||
"""
|
||||
|
||||
|
||||
def write_skill(root: Path, body: str):
|
||||
skill_dir = root / "offensive-test"
|
||||
skill_dir.mkdir(parents=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\n"
|
||||
"name: offensive-test\n"
|
||||
"description: Test an authorized offensive workflow.\n"
|
||||
"risk: offensive\n"
|
||||
"source: self\n"
|
||||
"date_added: \"2026-07-29\"\n"
|
||||
"---\n\n"
|
||||
f"{body}\n\n"
|
||||
"## When to Use\n\n- Authorized tests only.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class OffensiveSkillGuardrailTests(unittest.TestCase):
|
||||
def test_generic_disclaimer_without_confirmation_fails(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
skills_dir = Path(tmp) / "skills"
|
||||
write_skill(skills_dir, "> AUTHORIZED USE ONLY: authorized tests only.")
|
||||
results = validate_skills.collect_validation_results(str(skills_dir))
|
||||
self.assertTrue(any("EXACT AUTHORIZED-USE DISCLAIMER" in error for error in results["errors"]))
|
||||
self.assertTrue(any("PER-ACTION CONFIRMATION GATE" in error for error in results["errors"]))
|
||||
|
||||
def test_exact_guardrail_passes_security_checks(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
skills_dir = Path(tmp) / "skills"
|
||||
write_skill(skills_dir, EXACT_GUARDRAIL)
|
||||
results = validate_skills.collect_validation_results(str(skills_dir))
|
||||
self.assertFalse(any("OFFENSIVE SKILL" in error for error in results["errors"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -87,7 +87,18 @@ def collect_validation_results(skills_dir, strict_mode=False):
|
||||
skill_count = 0
|
||||
|
||||
# Pre-compiled regex
|
||||
security_disclaimer_pattern = re.compile(r"AUTHORIZED USE ONLY", re.IGNORECASE)
|
||||
security_disclaimer_pattern = re.compile(
|
||||
r"> \*\*⚠️ AUTHORIZED USE ONLY\*\*\s*\n"
|
||||
r"> This skill is for educational purposes or authorized security assessments only\.\s*\n"
|
||||
r"> You must have explicit, written permission from the system owner before using this tool\.\s*\n"
|
||||
r"> Misuse of this tool is illegal and strictly prohibited\.",
|
||||
)
|
||||
offensive_confirmation_pattern = re.compile(
|
||||
r"Mandatory confirmation gate[\s\S]{0,900}"
|
||||
r"exact target URL, IP, account, or resource[\s\S]{0,900}"
|
||||
r"Wait for explicit confirmation in the current conversation",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
valid_risk_levels = ["none", "safe", "critical", "offensive", "unknown"]
|
||||
date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$') # YYYY-MM-DD format
|
||||
@@ -183,7 +194,9 @@ def collect_validation_results(skills_dir, strict_mode=False):
|
||||
# 4. Security Guardrails
|
||||
if metadata.get("risk") == "offensive":
|
||||
if not security_disclaimer_pattern.search(content):
|
||||
errors.append(f"🚨 {rel_path}: OFFENSIVE SKILL MISSING SECURITY DISCLAIMER! (Must contain 'AUTHORIZED USE ONLY')")
|
||||
errors.append(f"🚨 {rel_path}: OFFENSIVE SKILL MISSING THE EXACT AUTHORIZED-USE DISCLAIMER")
|
||||
if not offensive_confirmation_pattern.search(content):
|
||||
errors.append(f"🚨 {rel_path}: OFFENSIVE SKILL MISSING THE MANDATORY PER-ACTION CONFIRMATION GATE")
|
||||
|
||||
# 5. Dangling Links Validation
|
||||
# Look for markdown links: [text](href)
|
||||
|
||||
Reference in New Issue
Block a user