feat(skills): add static scanner and block unsafe skills
This commit is contained in:
+42
-34
@@ -10,6 +10,7 @@ import { resolve, join, basename } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { platform } from 'os';
|
||||
import type { Skill, SkillManifest, SkillRequirements, SkillTier } from './types.js';
|
||||
import { scanSkillDirectory } from './scanner.js';
|
||||
|
||||
function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
||||
@@ -198,7 +199,23 @@ export function loadSkill(directory: string, tier: SkillTier): Skill | null {
|
||||
|
||||
const instructions = readFileSync(instructionsPath, 'utf-8');
|
||||
|
||||
let manifest: SkillManifest;
|
||||
const scan = scanSkillDirectory(absDir);
|
||||
const scanReasons = scan.issues.map((i) => `${i.code}: ${i.message}${i.path ? ` (${basename(i.path)})` : ''}`);
|
||||
|
||||
const inferManifest = (): SkillManifest => {
|
||||
const name = basename(absDir);
|
||||
const firstLine = instructions.split('\n').find((line) => line.trim().length > 0) ?? name;
|
||||
const description = firstLine.replace(/^#+\s*/, '').trim();
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
version: '0.0.0',
|
||||
tier,
|
||||
};
|
||||
};
|
||||
|
||||
let manifest: SkillManifest = inferManifest();
|
||||
const manifestReasons: string[] = [];
|
||||
|
||||
if (existsSync(manifestPath)) {
|
||||
// Parse manifest.json
|
||||
@@ -207,55 +224,46 @@ export function loadSkill(directory: string, tier: SkillTier): Skill | null {
|
||||
|
||||
// Validate required fields
|
||||
if (!raw.name || !raw.description || !raw.version) {
|
||||
console.warn(`Skill manifest at ${manifestPath} missing required fields (name, description, version)`);
|
||||
return null;
|
||||
manifestReasons.push('manifest.missing_required_fields: manifest.json missing required fields (name, description, version)');
|
||||
} else if (!hasValidInstallers(raw)) {
|
||||
manifestReasons.push('manifest.invalid_installers: manifest.json has invalid installers specification');
|
||||
} else if (!hasValidPermissions(raw)) {
|
||||
manifestReasons.push('manifest.invalid_permissions: manifest.json has invalid permissions specification');
|
||||
} else {
|
||||
manifest = {
|
||||
...raw,
|
||||
tier, // Override tier from argument
|
||||
};
|
||||
}
|
||||
if (!hasValidInstallers(raw)) {
|
||||
console.warn(`Skill manifest at ${manifestPath} has invalid installers specification`);
|
||||
return null;
|
||||
}
|
||||
if (!hasValidPermissions(raw)) {
|
||||
console.warn(`Skill manifest at ${manifestPath} has invalid permissions specification`);
|
||||
return null;
|
||||
}
|
||||
|
||||
manifest = {
|
||||
...raw,
|
||||
tier, // Override tier from argument
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to parse manifest.json at ${manifestPath}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
manifestReasons.push(
|
||||
`manifest.invalid_json: Failed to parse manifest.json (${error instanceof Error ? error.message : 'Unknown error'})`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// Infer minimal manifest from directory name and SKILL.md content
|
||||
const name = basename(absDir);
|
||||
const firstLine = instructions.split('\n').find((line) => line.trim().length > 0) ?? name;
|
||||
// Strip leading markdown heading markers for a cleaner description
|
||||
const description = firstLine.replace(/^#+\s*/, '').trim();
|
||||
|
||||
manifest = {
|
||||
name,
|
||||
description,
|
||||
version: '0.0.0',
|
||||
tier,
|
||||
};
|
||||
manifest = inferManifest();
|
||||
}
|
||||
|
||||
// Check system requirements
|
||||
const { available, reasons } = checkRequirements(manifest.requirements);
|
||||
|
||||
const unavailableReasons = [...reasons];
|
||||
if (!scan.ok) {
|
||||
unavailableReasons.push(...scanReasons);
|
||||
}
|
||||
if (manifestReasons.length > 0) {
|
||||
unavailableReasons.push(...manifestReasons);
|
||||
}
|
||||
|
||||
const skill: Skill = {
|
||||
manifest,
|
||||
instructions,
|
||||
directory: absDir,
|
||||
available,
|
||||
available: available && scan.ok && manifestReasons.length === 0,
|
||||
};
|
||||
|
||||
if (!available) {
|
||||
skill.unavailableReasons = reasons;
|
||||
if (!skill.available) {
|
||||
skill.unavailableReasons = unavailableReasons;
|
||||
}
|
||||
|
||||
return skill;
|
||||
|
||||
Reference in New Issue
Block a user