reskill 1.16.0-beta.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/publish.d.ts.map +1 -1
- package/dist/cli/index.js +885 -861
- package/dist/core/content-scanner.d.ts.map +1 -1
- package/dist/index.js +28 -4
- package/dist/scanner.js +28 -4
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -7483,932 +7483,956 @@ const logoutCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('logout'
|
|
|
7483
7483
|
}
|
|
7484
7484
|
});
|
|
7485
7485
|
/**
|
|
7486
|
-
*
|
|
7486
|
+
* ContentScanner - Detect malicious patterns in SKILL.md content
|
|
7487
7487
|
*
|
|
7488
|
-
*
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
//
|
|
7496
|
-
// Publisher Class
|
|
7488
|
+
* Features:
|
|
7489
|
+
* - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
|
|
7490
|
+
* - 6 built-in detection rules across 3 risk levels
|
|
7491
|
+
* - Configurable: override levels, disable rules, add custom rules
|
|
7492
|
+
* - Pure string operations in scan() — no fs dependency, suitable for server use
|
|
7493
|
+
* - scanFile() convenience method for CLI use
|
|
7494
|
+
*/ // ============================================================================
|
|
7495
|
+
// Safe Zone Masking
|
|
7497
7496
|
// ============================================================================
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
cwd: skillPath,
|
|
7527
|
-
encoding: 'utf-8'
|
|
7528
|
-
}).trim();
|
|
7529
|
-
// Parse to sourceRef format
|
|
7530
|
-
info.sourceRef = this.parseRemoteToSourceRef(info.remoteUrl);
|
|
7531
|
-
} catch {
|
|
7532
|
-
// No remote configured
|
|
7533
|
-
}
|
|
7534
|
-
// Get current branch
|
|
7535
|
-
try {
|
|
7536
|
-
info.currentBranch = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git branch --show-current', {
|
|
7537
|
-
cwd: skillPath,
|
|
7538
|
-
encoding: 'utf-8'
|
|
7539
|
-
}).trim() || null;
|
|
7540
|
-
} catch {
|
|
7541
|
-
// Detached HEAD or other error
|
|
7542
|
-
}
|
|
7543
|
-
// Get current commit
|
|
7544
|
-
try {
|
|
7545
|
-
info.currentCommit = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git rev-parse HEAD', {
|
|
7546
|
-
cwd: skillPath,
|
|
7547
|
-
encoding: 'utf-8'
|
|
7548
|
-
}).trim();
|
|
7549
|
-
// Get commit date
|
|
7550
|
-
info.commitDate = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git show -s --format=%cI HEAD', {
|
|
7551
|
-
cwd: skillPath,
|
|
7552
|
-
encoding: 'utf-8'
|
|
7553
|
-
}).trim();
|
|
7554
|
-
} catch {
|
|
7555
|
-
// No commits yet
|
|
7556
|
-
}
|
|
7557
|
-
// Get tag
|
|
7558
|
-
if (specifiedTag) // Use specified tag
|
|
7559
|
-
try {
|
|
7560
|
-
info.tagCommit = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)(`git rev-parse "${specifiedTag}^{commit}"`, {
|
|
7561
|
-
cwd: skillPath,
|
|
7562
|
-
encoding: 'utf-8'
|
|
7563
|
-
}).trim();
|
|
7564
|
-
info.tag = specifiedTag;
|
|
7565
|
-
} catch {
|
|
7566
|
-
throw new PublishError(`Tag "${specifiedTag}" not found`);
|
|
7567
|
-
}
|
|
7568
|
-
else // Try to get tag on current commit
|
|
7569
|
-
try {
|
|
7570
|
-
const tag = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git describe --exact-match --tags HEAD', {
|
|
7571
|
-
cwd: skillPath,
|
|
7572
|
-
encoding: 'utf-8'
|
|
7573
|
-
}).trim();
|
|
7574
|
-
info.tag = tag;
|
|
7575
|
-
info.tagCommit = info.currentCommit;
|
|
7576
|
-
} catch {
|
|
7577
|
-
// No tag on current commit
|
|
7497
|
+
/**
|
|
7498
|
+
* Mask safe zones in Markdown content with spaces, preserving line structure.
|
|
7499
|
+
*
|
|
7500
|
+
* Safe zones (content replaced with spaces):
|
|
7501
|
+
* - YAML frontmatter (`---` ... `---` at file start)
|
|
7502
|
+
* - Fenced code blocks (``` or ~~~)
|
|
7503
|
+
* - Indented code blocks (4 spaces / tab after blank line)
|
|
7504
|
+
* - Blockquotes (`> ` prefix)
|
|
7505
|
+
* - Inline code (`` `...` ``)
|
|
7506
|
+
* - Double-quoted text (`"..."`, min 3 chars between quotes)
|
|
7507
|
+
*
|
|
7508
|
+
* Line breaks are preserved so line numbers remain correct.
|
|
7509
|
+
*/ function maskSafeZones(content) {
|
|
7510
|
+
const lines = content.split('\n');
|
|
7511
|
+
const result = [];
|
|
7512
|
+
let inFrontmatter = false;
|
|
7513
|
+
let inFencedCode = false;
|
|
7514
|
+
let fenceChar = '';
|
|
7515
|
+
let fenceLength = 0;
|
|
7516
|
+
let prevLineBlank = false;
|
|
7517
|
+
let prevLineIndentedCode = false;
|
|
7518
|
+
for(let i = 0; i < lines.length; i++){
|
|
7519
|
+
const line = lines[i];
|
|
7520
|
+
// --- YAML Frontmatter (only at file start) ---
|
|
7521
|
+
if (0 === i && '---' === line.trim()) {
|
|
7522
|
+
inFrontmatter = true;
|
|
7523
|
+
result.push(maskLine(line));
|
|
7524
|
+
continue;
|
|
7578
7525
|
}
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
encoding: 'utf-8'
|
|
7584
|
-
}).trim();
|
|
7585
|
-
info.isDirty = status.length > 0;
|
|
7586
|
-
} catch {
|
|
7587
|
-
// Ignore errors
|
|
7526
|
+
if (inFrontmatter) {
|
|
7527
|
+
result.push(maskLine(line));
|
|
7528
|
+
if ('---' === line.trim()) inFrontmatter = false;
|
|
7529
|
+
continue;
|
|
7588
7530
|
}
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
return `${registry}:${owner}/${repo.replace(/\.git$/, '')}`;
|
|
7531
|
+
// --- Fenced code blocks (``` or ~~~) ---
|
|
7532
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
7533
|
+
if (!inFencedCode && fenceMatch) {
|
|
7534
|
+
inFencedCode = true;
|
|
7535
|
+
fenceChar = fenceMatch[1][0];
|
|
7536
|
+
fenceLength = fenceMatch[1].length;
|
|
7537
|
+
result.push(maskLine(line));
|
|
7538
|
+
prevLineBlank = false;
|
|
7539
|
+
prevLineIndentedCode = false;
|
|
7540
|
+
continue;
|
|
7600
7541
|
}
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7542
|
+
if (inFencedCode) {
|
|
7543
|
+
result.push(maskLine(line));
|
|
7544
|
+
const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
|
|
7545
|
+
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
|
|
7546
|
+
prevLineBlank = false;
|
|
7547
|
+
prevLineIndentedCode = false;
|
|
7548
|
+
continue;
|
|
7607
7549
|
}
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
if ('gitlab.com' === host) return 'gitlab';
|
|
7615
|
-
return host;
|
|
7616
|
-
}
|
|
7617
|
-
/**
|
|
7618
|
-
* Build publish payload
|
|
7619
|
-
*/ buildPayload(skill, gitInfo, integrity) {
|
|
7620
|
-
const { skillJson, skillMd, readme, files } = skill;
|
|
7621
|
-
const payload = {
|
|
7622
|
-
version: skillJson.version,
|
|
7623
|
-
description: skillJson.description || '',
|
|
7624
|
-
gitRef: gitInfo.tag || gitInfo.currentCommit || 'HEAD',
|
|
7625
|
-
gitCommit: gitInfo.tagCommit || gitInfo.currentCommit || '',
|
|
7626
|
-
gitCommitDate: gitInfo.commitDate || void 0,
|
|
7627
|
-
repositoryUrl: gitInfo.remoteUrl || '',
|
|
7628
|
-
sourceRef: gitInfo.sourceRef || '',
|
|
7629
|
-
skillJson,
|
|
7630
|
-
files,
|
|
7631
|
-
entry: skillJson.entry || 'SKILL.md',
|
|
7632
|
-
integrity
|
|
7633
|
-
};
|
|
7634
|
-
// Add optional fields
|
|
7635
|
-
if (skillMd) payload.skillMd = {
|
|
7636
|
-
name: skillMd.name,
|
|
7637
|
-
description: skillMd.description,
|
|
7638
|
-
license: skillMd.license,
|
|
7639
|
-
compatibility: skillMd.compatibility,
|
|
7640
|
-
allowedTools: skillMd.allowedTools
|
|
7641
|
-
};
|
|
7642
|
-
if (readme) payload.readmePreview = readme;
|
|
7643
|
-
if (skillJson.keywords && skillJson.keywords.length > 0) payload.keywords = skillJson.keywords;
|
|
7644
|
-
if (skillJson.compatibility) {
|
|
7645
|
-
// Filter out undefined values from compatibility
|
|
7646
|
-
const compat = {};
|
|
7647
|
-
for (const [key, value] of Object.entries(skillJson.compatibility))if (void 0 !== value) compat[key] = value;
|
|
7648
|
-
if (Object.keys(compat).length > 0) payload.compatibility = compat;
|
|
7550
|
+
// --- Blockquote ---
|
|
7551
|
+
if (/^>\s?/.test(line)) {
|
|
7552
|
+
result.push(maskLine(line));
|
|
7553
|
+
prevLineBlank = false;
|
|
7554
|
+
prevLineIndentedCode = false;
|
|
7555
|
+
continue;
|
|
7649
7556
|
}
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
7657
|
-
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
7658
|
-
}
|
|
7659
|
-
/**
|
|
7660
|
-
* Calculate total size of files
|
|
7661
|
-
*/ calculateTotalSize(skillPath, files) {
|
|
7662
|
-
let total = 0;
|
|
7663
|
-
for (const file of files){
|
|
7664
|
-
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
7665
|
-
if (external_node_fs_.existsSync(filePath)) {
|
|
7666
|
-
const stats = external_node_fs_.statSync(filePath);
|
|
7667
|
-
total += stats.size;
|
|
7668
|
-
}
|
|
7557
|
+
// --- Indented code block (4 spaces or tab, after blank line) ---
|
|
7558
|
+
if (/^(?: |\t)/.test(line) && (prevLineBlank || prevLineIndentedCode)) {
|
|
7559
|
+
result.push(maskLine(line));
|
|
7560
|
+
prevLineBlank = false;
|
|
7561
|
+
prevLineIndentedCode = true;
|
|
7562
|
+
continue;
|
|
7669
7563
|
}
|
|
7670
|
-
|
|
7564
|
+
// --- Normal line: mask inline code and double-quoted text ---
|
|
7565
|
+
result.push(maskInline(line));
|
|
7566
|
+
prevLineBlank = '' === line.trim();
|
|
7567
|
+
prevLineIndentedCode = false;
|
|
7671
7568
|
}
|
|
7569
|
+
return result.join('\n');
|
|
7570
|
+
}
|
|
7571
|
+
/** Replace all characters in a line with spaces (preserving length) */ function maskLine(line) {
|
|
7572
|
+
return ' '.repeat(line.length);
|
|
7672
7573
|
}
|
|
7673
7574
|
/**
|
|
7674
|
-
*
|
|
7675
|
-
*
|
|
7676
|
-
*
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
const MAX_NAME_LENGTH = 64;
|
|
7686
|
-
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
7687
|
-
const MAX_KEYWORDS = 10;
|
|
7688
|
-
const SINGLE_CHAR_NAME_PATTERN = /^[a-z0-9]$/;
|
|
7689
|
-
const DEFAULT_VERSION = '0.0.0';
|
|
7690
|
-
// Default files to include in publish
|
|
7691
|
-
const DEFAULT_FILES = [
|
|
7692
|
-
'SKILL.md',
|
|
7693
|
-
'README.md',
|
|
7694
|
-
'LICENSE'
|
|
7695
|
-
];
|
|
7575
|
+
* Mask inline code (`` `...` ``) and double-quoted text (`"..."`) within a line.
|
|
7576
|
+
* Uses regex replacement for efficiency (avoids char-by-char concatenation on long lines).
|
|
7577
|
+
* Single quotes are NOT masked to avoid false matches with apostrophes.
|
|
7578
|
+
*/ function maskInline(line) {
|
|
7579
|
+
let result = line;
|
|
7580
|
+
// Inline code: `...`
|
|
7581
|
+
result = result.replace(/`[^`]+`/g, (m)=>' '.repeat(m.length));
|
|
7582
|
+
// Double-quoted text: "..." (min 3 chars between quotes)
|
|
7583
|
+
result = result.replace(/"[^"]{3,}"/g, (m)=>' '.repeat(m.length));
|
|
7584
|
+
return result;
|
|
7585
|
+
}
|
|
7696
7586
|
// ============================================================================
|
|
7697
|
-
//
|
|
7587
|
+
// Rule Helpers
|
|
7698
7588
|
// ============================================================================
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
* - 1-64 characters
|
|
7706
|
-
* - Cannot start or end with hyphen
|
|
7707
|
-
* - Cannot have consecutive hyphens
|
|
7708
|
-
*/ validateName(name) {
|
|
7709
|
-
const errors = [];
|
|
7710
|
-
if (!name) {
|
|
7711
|
-
errors.push({
|
|
7712
|
-
field: 'name',
|
|
7713
|
-
message: 'Skill name is required',
|
|
7714
|
-
suggestion: 'Add "name" field to SKILL.md frontmatter'
|
|
7715
|
-
});
|
|
7716
|
-
return {
|
|
7717
|
-
valid: false,
|
|
7718
|
-
errors,
|
|
7719
|
-
warnings: []
|
|
7720
|
-
};
|
|
7721
|
-
}
|
|
7722
|
-
if (name.length > MAX_NAME_LENGTH) errors.push({
|
|
7723
|
-
field: 'name',
|
|
7724
|
-
message: `Skill name must be at most ${MAX_NAME_LENGTH} characters`,
|
|
7725
|
-
suggestion: `Shorten the name to ${MAX_NAME_LENGTH} characters or less`
|
|
7726
|
-
});
|
|
7727
|
-
// Check for uppercase
|
|
7728
|
-
if (/[A-Z]/.test(name)) errors.push({
|
|
7729
|
-
field: 'name',
|
|
7730
|
-
message: 'Skill name must be lowercase',
|
|
7731
|
-
suggestion: `Change "${name}" to "${name.toLowerCase()}"`
|
|
7732
|
-
});
|
|
7733
|
-
// Check for invalid characters
|
|
7734
|
-
if (/[^a-z0-9-]/.test(name)) errors.push({
|
|
7735
|
-
field: 'name',
|
|
7736
|
-
message: 'Skill name can only contain lowercase letters, numbers, and hyphens',
|
|
7737
|
-
suggestion: 'Remove special characters from the name'
|
|
7589
|
+
/** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
|
|
7590
|
+
const lines = content.split('\n');
|
|
7591
|
+
const matches = [];
|
|
7592
|
+
for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
|
|
7593
|
+
matches.push({
|
|
7594
|
+
line: i + 1
|
|
7738
7595
|
});
|
|
7739
|
-
|
|
7740
|
-
if (1 === name.length) {
|
|
7741
|
-
if (!SINGLE_CHAR_NAME_PATTERN.test(name)) errors.push({
|
|
7742
|
-
field: 'name',
|
|
7743
|
-
message: 'Single character name must be a lowercase letter or number'
|
|
7744
|
-
});
|
|
7745
|
-
} else if (name.length > 1) {
|
|
7746
|
-
// Check start/end with hyphen
|
|
7747
|
-
if (name.startsWith('-')) errors.push({
|
|
7748
|
-
field: 'name',
|
|
7749
|
-
message: 'Skill name cannot start with a hyphen'
|
|
7750
|
-
});
|
|
7751
|
-
if (name.endsWith('-')) errors.push({
|
|
7752
|
-
field: 'name',
|
|
7753
|
-
message: 'Skill name cannot end with a hyphen'
|
|
7754
|
-
});
|
|
7755
|
-
// Check consecutive hyphens
|
|
7756
|
-
if (/--/.test(name)) errors.push({
|
|
7757
|
-
field: 'name',
|
|
7758
|
-
message: 'Skill name cannot contain consecutive hyphens'
|
|
7759
|
-
});
|
|
7760
|
-
}
|
|
7761
|
-
return {
|
|
7762
|
-
valid: 0 === errors.length,
|
|
7763
|
-
errors,
|
|
7764
|
-
warnings: []
|
|
7765
|
-
};
|
|
7596
|
+
break;
|
|
7766
7597
|
}
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7598
|
+
return matches;
|
|
7599
|
+
}
|
|
7600
|
+
// ============================================================================
|
|
7601
|
+
// Default Rules
|
|
7602
|
+
// ============================================================================
|
|
7603
|
+
const SNIPPET_MAX_LENGTH = 120;
|
|
7604
|
+
/** Built-in detection rules */ const DEFAULT_RULES = [
|
|
7605
|
+
// Rule 1: Prompt Injection (high)
|
|
7606
|
+
{
|
|
7607
|
+
id: 'prompt-injection',
|
|
7608
|
+
level: 'high',
|
|
7609
|
+
message: 'Detected prompt injection attempt',
|
|
7610
|
+
skipSafeZones: true,
|
|
7611
|
+
check: (content)=>findLineMatches(content, [
|
|
7612
|
+
// English patterns
|
|
7613
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
7614
|
+
/disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
|
|
7615
|
+
/you\s+are\s+now\s+(?:(?:a|an)\s+)?(?:(?:\w+\s+){0,3}(?:agent|ai|assistant|bot|model|character|persona|entity|system)|DAN\b|jailbr\w*|unrestricted|unfiltered|free\s+from)/i,
|
|
7616
|
+
/from\s+now\s+on[,\s]+you\s+are/i,
|
|
7617
|
+
/new\s+system\s+prompt/i,
|
|
7618
|
+
/override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
|
|
7619
|
+
/forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
|
|
7620
|
+
/(?:you\s+are|you're)\s+(?:now\s+)?entering\s+(?:a\s+)?new\s+(?:mode|context|session)/i,
|
|
7621
|
+
// Chinese patterns (中文提示词注入)
|
|
7622
|
+
/[忽无][略视]\s*(所有\s*)?(之前的?|先前的?|以前的?)?\s*(指令|指示|规则|约束|限制)/,
|
|
7623
|
+
/你现在是/,
|
|
7624
|
+
/从现在开始.{0,10}你是/,
|
|
7625
|
+
/新的系统提示词/,
|
|
7626
|
+
/[覆改]写?\s*(你的|系统)\s*(提示词|规则|指令|安全)/,
|
|
7627
|
+
/忘记\s*(所有\s*)?(之前的?|先前的?)?\s*(指令|指示|规则|约束)/,
|
|
7628
|
+
/进入.{0,5}新的?\s*(模式|上下文|会话)/,
|
|
7629
|
+
/不要遵守.{0,10}(安全|限制|规则|约束)/,
|
|
7630
|
+
/解除.{0,5}(限制|约束|安全)/,
|
|
7631
|
+
/无限制模式/,
|
|
7632
|
+
/安全模式已关闭/
|
|
7633
|
+
])
|
|
7634
|
+
},
|
|
7635
|
+
// Rule 2: Data Exfiltration (high)
|
|
7636
|
+
{
|
|
7637
|
+
id: 'data-exfiltration',
|
|
7638
|
+
level: 'high',
|
|
7639
|
+
message: 'Detected potential data exfiltration command',
|
|
7640
|
+
skipSafeZones: true,
|
|
7641
|
+
check: (content)=>{
|
|
7642
|
+
const lines = content.split('\n');
|
|
7643
|
+
const matches = [];
|
|
7644
|
+
const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
|
|
7645
|
+
const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
|
|
7646
|
+
for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
|
|
7647
|
+
line: i + 1
|
|
7776
7648
|
});
|
|
7777
|
-
return
|
|
7778
|
-
valid: false,
|
|
7779
|
-
errors,
|
|
7780
|
-
warnings: []
|
|
7781
|
-
};
|
|
7649
|
+
return matches;
|
|
7782
7650
|
}
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7651
|
+
},
|
|
7652
|
+
// Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
|
|
7653
|
+
{
|
|
7654
|
+
id: 'obfuscation',
|
|
7655
|
+
level: 'high',
|
|
7656
|
+
message: 'Detected content obfuscation',
|
|
7657
|
+
skipSafeZones: false,
|
|
7658
|
+
check: (content)=>{
|
|
7659
|
+
const matches = [];
|
|
7660
|
+
const lines = content.split('\n');
|
|
7661
|
+
// Zero-width characters (suspicious in any context)
|
|
7662
|
+
const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
|
|
7663
|
+
for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
|
|
7664
|
+
line: i + 1,
|
|
7665
|
+
snippet: 'Zero-width Unicode characters detected'
|
|
7789
7666
|
});
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
}
|
|
7796
|
-
if (!__WEBPACK_EXTERNAL_MODULE_semver__.valid(version)) errors.push({
|
|
7797
|
-
field: 'version',
|
|
7798
|
-
message: `Invalid version format: "${version}". Must follow semver (x.y.z)`,
|
|
7799
|
-
suggestion: 'Use format like "1.0.0" or "1.0.0-beta.1"'
|
|
7800
|
-
});
|
|
7801
|
-
return {
|
|
7802
|
-
valid: 0 === errors.length,
|
|
7803
|
-
errors,
|
|
7804
|
-
warnings: []
|
|
7805
|
-
};
|
|
7806
|
-
}
|
|
7807
|
-
/**
|
|
7808
|
-
* Validate description
|
|
7809
|
-
*
|
|
7810
|
-
* Following agentskills.io specification:
|
|
7811
|
-
* - Max 1024 characters
|
|
7812
|
-
* - Non-empty
|
|
7813
|
-
*/ validateDescription(description) {
|
|
7814
|
-
const errors = [];
|
|
7815
|
-
if (!description) {
|
|
7816
|
-
errors.push({
|
|
7817
|
-
field: 'description',
|
|
7818
|
-
message: 'Description is required',
|
|
7819
|
-
suggestion: 'Add "description" field to SKILL.md frontmatter'
|
|
7667
|
+
// Long base64-like strings (>200 continuous chars)
|
|
7668
|
+
const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
|
|
7669
|
+
for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
|
|
7670
|
+
line: i + 1,
|
|
7671
|
+
snippet: 'Suspicious base64-encoded block detected'
|
|
7820
7672
|
});
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7673
|
+
// Large HTML comments (>200 chars of content)
|
|
7674
|
+
const commentRegex = /<!--([\s\S]{200,}?)-->/g;
|
|
7675
|
+
let match;
|
|
7676
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
|
|
7677
|
+
while(null !== (match = commentRegex.exec(content))){
|
|
7678
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
7679
|
+
matches.push({
|
|
7680
|
+
line: lineNum,
|
|
7681
|
+
snippet: `Large HTML comment block (${match[1].length} chars)`
|
|
7682
|
+
});
|
|
7683
|
+
}
|
|
7684
|
+
return matches;
|
|
7826
7685
|
}
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7686
|
+
},
|
|
7687
|
+
// Rule 4: Sensitive File Access (medium)
|
|
7688
|
+
{
|
|
7689
|
+
id: 'sensitive-file-access',
|
|
7690
|
+
level: 'medium',
|
|
7691
|
+
message: 'References sensitive file path',
|
|
7692
|
+
skipSafeZones: true,
|
|
7693
|
+
check: (content)=>findLineMatches(content, [
|
|
7694
|
+
/~\/\.ssh\b/,
|
|
7695
|
+
/~\/\.aws\b/,
|
|
7696
|
+
/~\/\.gnupg\b/,
|
|
7697
|
+
/~\/\.config\/gcloud\b/,
|
|
7698
|
+
/\bid_rsa\b/i,
|
|
7699
|
+
/\bid_ed25519\b/i,
|
|
7700
|
+
/\/etc\/passwd\b/,
|
|
7701
|
+
/\/etc\/shadow\b/,
|
|
7702
|
+
/\.env\b(?!\.\w)/
|
|
7703
|
+
])
|
|
7704
|
+
},
|
|
7705
|
+
// Rule 5: Stealth Instructions (medium) — phrase + action verb matching
|
|
7706
|
+
{
|
|
7707
|
+
id: 'stealth-instructions',
|
|
7708
|
+
level: 'medium',
|
|
7709
|
+
message: 'Detected instruction to hide actions from user',
|
|
7710
|
+
skipSafeZones: true,
|
|
7711
|
+
check: (content)=>{
|
|
7712
|
+
const actionVerbs = 'execute|delete|remove|send|transmit|modify|overwrite|install|download|upload|run|write|create|destroy|drop';
|
|
7713
|
+
const patterns = [
|
|
7714
|
+
// English patterns
|
|
7715
|
+
new RegExp(`silently\\s+(?:${actionVerbs})`, 'i'),
|
|
7716
|
+
new RegExp(`without\\s+telling\\s+the\\s+user.{0,30}(?:${actionVerbs})`, 'i'),
|
|
7717
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+show\\s+.{0,40}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
7718
|
+
new RegExp("hide\\s+(?:this|the|these|all)\\s+.{0,30}(?:from\\s+the\\s+user|from\\s+user)", 'i'),
|
|
7719
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+mention\\s+.{0,30}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
7720
|
+
new RegExp("keep\\s+(?:this|it)\\s+(?:a\\s+)?secret\\s+from\\s+(?:the\\s+)?user", 'i'),
|
|
7721
|
+
// Chinese patterns (中文隐蔽指令)
|
|
7722
|
+
/悄悄地?\s*(?:执行|删除|移除|发送|传输|修改|覆盖|安装|下载|上传|运行|写入|创建|销毁|丢弃)/,
|
|
7723
|
+
/不要告诉用户/,
|
|
7724
|
+
/不要让用户知道/,
|
|
7725
|
+
/对用户隐藏/,
|
|
7726
|
+
/在用户不知情的情况下/,
|
|
7727
|
+
/瞒着用户/
|
|
7728
|
+
];
|
|
7729
|
+
// Safe patterns to exclude (common in legitimate DevOps/automation skills)
|
|
7730
|
+
const safePatterns = [
|
|
7731
|
+
/silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i,
|
|
7732
|
+
// Chinese safe patterns (中文合法自动化用语)
|
|
7733
|
+
/悄悄地?\s*(?:忽略|跳过|丢弃|抑制|继续|静默)/
|
|
7734
|
+
];
|
|
7735
|
+
const lines = content.split('\n');
|
|
7736
|
+
const matches = [];
|
|
7737
|
+
for(let i = 0; i < lines.length; i++){
|
|
7738
|
+
const line = lines[i];
|
|
7739
|
+
if (!safePatterns.some((p)=>p.test(line))) {
|
|
7740
|
+
for (const pattern of patterns)if (pattern.test(line)) {
|
|
7741
|
+
matches.push({
|
|
7742
|
+
line: i + 1
|
|
7743
|
+
});
|
|
7744
|
+
break;
|
|
7745
|
+
}
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
return matches;
|
|
7860
7749
|
}
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7750
|
+
},
|
|
7751
|
+
// Rule 6: Oversized Content (low) — scans ALL content
|
|
7752
|
+
{
|
|
7753
|
+
id: 'oversized-content',
|
|
7754
|
+
level: 'low',
|
|
7755
|
+
message: 'Content exceeds recommended size limit',
|
|
7756
|
+
skipSafeZones: false,
|
|
7757
|
+
check: (content)=>{
|
|
7758
|
+
const MAX_SIZE_BYTES = 51200;
|
|
7759
|
+
const sizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
7760
|
+
if (sizeBytes > MAX_SIZE_BYTES) return [
|
|
7761
|
+
{
|
|
7762
|
+
snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
|
|
7763
|
+
}
|
|
7764
|
+
];
|
|
7765
|
+
return [];
|
|
7867
7766
|
}
|
|
7868
|
-
// Scan files (use files array from SKILL.md metadata if available)
|
|
7869
|
-
const filesPattern = result.skillMd?.metadata?.files;
|
|
7870
|
-
result.files = this.scanFiles(skillPath, filesPattern);
|
|
7871
|
-
return result;
|
|
7872
7767
|
}
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
const
|
|
7883
|
-
|
|
7884
|
-
name: skillMd.name,
|
|
7885
|
-
version,
|
|
7886
|
-
description: skillMd.description,
|
|
7887
|
-
license: skillMd.license,
|
|
7888
|
-
keywords,
|
|
7889
|
-
entry: 'SKILL.md'
|
|
7890
|
-
};
|
|
7768
|
+
];
|
|
7769
|
+
// ============================================================================
|
|
7770
|
+
// ContentScanner
|
|
7771
|
+
// ============================================================================
|
|
7772
|
+
/** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
|
|
7773
|
+
let rules = DEFAULT_RULES.map((r)=>({
|
|
7774
|
+
...r
|
|
7775
|
+
}));
|
|
7776
|
+
if (options?.disabledRules?.length) {
|
|
7777
|
+
const disabled = new Set(options.disabledRules);
|
|
7778
|
+
rules = rules.filter((r)=>!disabled.has(r.id));
|
|
7891
7779
|
}
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
* If includePatterns is specified, only include those files/directories.
|
|
7896
|
-
* Otherwise, scan all files in the directory (excluding ignored patterns).
|
|
7897
|
-
*/ scanFiles(skillPath, includePatterns) {
|
|
7898
|
-
const files = [];
|
|
7899
|
-
const seen = new Set();
|
|
7900
|
-
// If includePatterns specified, use selective scanning
|
|
7901
|
-
if (includePatterns && includePatterns.length > 0) {
|
|
7902
|
-
// Add default files first
|
|
7903
|
-
for (const file of DEFAULT_FILES){
|
|
7904
|
-
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
7905
|
-
if (external_node_fs_.existsSync(filePath) && !seen.has(file)) {
|
|
7906
|
-
files.push(file);
|
|
7907
|
-
seen.add(file);
|
|
7908
|
-
}
|
|
7909
|
-
}
|
|
7910
|
-
// Add files from SKILL.md metadata.files array
|
|
7911
|
-
for (const pattern of includePatterns){
|
|
7912
|
-
const targetPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, pattern);
|
|
7913
|
-
if (external_node_fs_.existsSync(targetPath)) {
|
|
7914
|
-
const stat = external_node_fs_.statSync(targetPath);
|
|
7915
|
-
if (stat.isDirectory()) // Recursively add all files in directory
|
|
7916
|
-
this.addFilesFromDir(skillPath, pattern, files, seen);
|
|
7917
|
-
else if (!seen.has(pattern)) {
|
|
7918
|
-
files.push(pattern);
|
|
7919
|
-
seen.add(pattern);
|
|
7920
|
-
}
|
|
7921
|
-
}
|
|
7922
|
-
}
|
|
7923
|
-
} else // No includePatterns: scan entire directory (default behavior)
|
|
7924
|
-
this.addFilesFromDir(skillPath, '', files, seen);
|
|
7925
|
-
return files;
|
|
7780
|
+
if (options?.overrides) for (const rule of rules){
|
|
7781
|
+
const override = options.overrides[rule.id];
|
|
7782
|
+
if (override) rule.level = override;
|
|
7926
7783
|
}
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7784
|
+
if (options?.customRules?.length) rules.push(...options.customRules);
|
|
7785
|
+
return rules;
|
|
7786
|
+
}
|
|
7787
|
+
/**
|
|
7788
|
+
* Content scanner for SKILL.md files.
|
|
7789
|
+
*
|
|
7790
|
+
* Detects prompt injection, data exfiltration, obfuscation, sensitive file
|
|
7791
|
+
* access, stealth instructions, and oversized content.
|
|
7792
|
+
*
|
|
7793
|
+
* @example
|
|
7794
|
+
* ```typescript
|
|
7795
|
+
* // Default usage (CLI)
|
|
7796
|
+
* const scanner = new ContentScanner();
|
|
7797
|
+
* const result = scanner.scan(content);
|
|
7798
|
+
*
|
|
7799
|
+
* // Custom usage (private registry server)
|
|
7800
|
+
* const scanner = new ContentScanner({
|
|
7801
|
+
* overrides: { 'prompt-injection': 'medium' },
|
|
7802
|
+
* disabledRules: ['stealth-instructions'],
|
|
7803
|
+
* });
|
|
7804
|
+
* ```
|
|
7805
|
+
*/ class ContentScanner {
|
|
7806
|
+
rules;
|
|
7807
|
+
constructor(options){
|
|
7808
|
+
this.rules = buildRuleSet(options);
|
|
7952
7809
|
}
|
|
7953
7810
|
/**
|
|
7954
|
-
*
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
const
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
for (const
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
const
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7811
|
+
* Scan content string for malicious patterns.
|
|
7812
|
+
* Pure string operation — no file system access.
|
|
7813
|
+
*/ scan(content) {
|
|
7814
|
+
const originalLines = content.split('\n');
|
|
7815
|
+
const maskedContent = maskSafeZones(content);
|
|
7816
|
+
const findings = [];
|
|
7817
|
+
for (const rule of this.rules){
|
|
7818
|
+
const targetContent = rule.skipSafeZones ? maskedContent : content;
|
|
7819
|
+
const matches = rule.check(targetContent);
|
|
7820
|
+
for (const match of matches){
|
|
7821
|
+
// Use custom snippet if provided, otherwise generate from original content
|
|
7822
|
+
const snippet = match.snippet ?? (null != match.line ? originalLines[match.line - 1]?.trim().slice(0, SNIPPET_MAX_LENGTH) : void 0);
|
|
7823
|
+
findings.push({
|
|
7824
|
+
rule: rule.id,
|
|
7825
|
+
level: rule.level,
|
|
7826
|
+
message: rule.message,
|
|
7827
|
+
line: match.line,
|
|
7828
|
+
snippet
|
|
7829
|
+
});
|
|
7968
7830
|
}
|
|
7969
7831
|
}
|
|
7832
|
+
const hasHighRisk = findings.some((f)=>'high' === f.level);
|
|
7833
|
+
return {
|
|
7834
|
+
passed: !hasHighRisk,
|
|
7835
|
+
findings
|
|
7836
|
+
};
|
|
7970
7837
|
}
|
|
7971
7838
|
/**
|
|
7972
|
-
*
|
|
7973
|
-
*
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
const errors = [];
|
|
7979
|
-
const warnings = [];
|
|
7980
|
-
// Check SKILL.md exists (REQUIRED per spec)
|
|
7981
|
-
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'SKILL.md');
|
|
7982
|
-
if (!external_node_fs_.existsSync(skillMdPath)) {
|
|
7983
|
-
errors.push({
|
|
7984
|
-
field: 'SKILL.md',
|
|
7985
|
-
message: 'SKILL.md not found. This file is required for publishing.',
|
|
7986
|
-
suggestion: 'Create a SKILL.md file with name and description in YAML frontmatter'
|
|
7987
|
-
});
|
|
7988
|
-
return {
|
|
7989
|
-
valid: false,
|
|
7990
|
-
errors,
|
|
7991
|
-
warnings
|
|
7992
|
-
};
|
|
7993
|
-
}
|
|
7994
|
-
// Parse SKILL.md
|
|
7995
|
-
let skillMd;
|
|
7996
|
-
try {
|
|
7997
|
-
skillMd = parseSkillMdFile(skillMdPath);
|
|
7998
|
-
if (!skillMd) {
|
|
7999
|
-
errors.push({
|
|
8000
|
-
field: 'SKILL.md',
|
|
8001
|
-
message: 'SKILL.md must have valid YAML frontmatter with name and description',
|
|
8002
|
-
suggestion: 'Add frontmatter: ---\\nname: your-skill\\ndescription: Your description\\n---'
|
|
8003
|
-
});
|
|
8004
|
-
return {
|
|
8005
|
-
valid: false,
|
|
8006
|
-
errors,
|
|
8007
|
-
warnings
|
|
8008
|
-
};
|
|
8009
|
-
}
|
|
8010
|
-
} catch (error) {
|
|
8011
|
-
errors.push({
|
|
8012
|
-
field: 'SKILL.md',
|
|
8013
|
-
message: `Failed to parse SKILL.md: ${error.message}`,
|
|
8014
|
-
suggestion: 'Check the YAML frontmatter syntax is valid'
|
|
8015
|
-
});
|
|
8016
|
-
return {
|
|
8017
|
-
valid: false,
|
|
8018
|
-
errors,
|
|
8019
|
-
warnings
|
|
8020
|
-
};
|
|
8021
|
-
}
|
|
8022
|
-
// Validate name from SKILL.md
|
|
8023
|
-
const nameResult = this.validateName(skillMd.name);
|
|
8024
|
-
errors.push(...nameResult.errors);
|
|
8025
|
-
// Validate description from SKILL.md
|
|
8026
|
-
const descResult = this.validateDescription(skillMd.description);
|
|
8027
|
-
errors.push(...descResult.errors);
|
|
8028
|
-
// Check version in SKILL.md
|
|
8029
|
-
const skillMdVersion = skillMd.version || skillMd.metadata?.version;
|
|
8030
|
-
if (skillMdVersion) {
|
|
8031
|
-
// Validate the version from SKILL.md
|
|
8032
|
-
const versionResult = this.validateVersion(skillMdVersion);
|
|
8033
|
-
errors.push(...versionResult.errors);
|
|
8034
|
-
} else warnings.push({
|
|
8035
|
-
field: 'version',
|
|
8036
|
-
message: `No version specified, defaulting to "${DEFAULT_VERSION}"`,
|
|
8037
|
-
suggestion: 'Add version in SKILL.md frontmatter'
|
|
8038
|
-
});
|
|
8039
|
-
// Check keywords count (only if metadata.keywords is a valid array)
|
|
8040
|
-
const keywords = skillMd.metadata?.keywords;
|
|
8041
|
-
if (Array.isArray(keywords) && keywords.length > MAX_KEYWORDS) warnings.push({
|
|
8042
|
-
field: 'keywords',
|
|
8043
|
-
message: `Too many keywords (${keywords.length}). Recommended max: ${MAX_KEYWORDS}`
|
|
8044
|
-
});
|
|
8045
|
-
// Check license
|
|
8046
|
-
if (!skillMd.license) warnings.push({
|
|
8047
|
-
field: 'license',
|
|
8048
|
-
message: 'No license specified',
|
|
8049
|
-
suggestion: 'Add license in SKILL.md frontmatter'
|
|
8050
|
-
});
|
|
8051
|
-
return {
|
|
8052
|
-
valid: 0 === errors.length,
|
|
8053
|
-
errors,
|
|
8054
|
-
warnings
|
|
8055
|
-
};
|
|
8056
|
-
}
|
|
8057
|
-
/**
|
|
8058
|
-
* Generate integrity hash for files
|
|
8059
|
-
*/ generateIntegrity(skillPath, files) {
|
|
8060
|
-
const hash = __WEBPACK_EXTERNAL_MODULE_node_crypto__.createHash('sha256');
|
|
8061
|
-
// Sort files for consistent ordering
|
|
8062
|
-
const sortedFiles = [
|
|
8063
|
-
...files
|
|
8064
|
-
].sort();
|
|
8065
|
-
for (const file of sortedFiles){
|
|
8066
|
-
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
8067
|
-
if (external_node_fs_.existsSync(filePath)) {
|
|
8068
|
-
hash.update(file);
|
|
8069
|
-
const content = external_node_fs_.readFileSync(filePath);
|
|
8070
|
-
hash.update(content);
|
|
8071
|
-
}
|
|
8072
|
-
}
|
|
8073
|
-
return `sha256-${hash.digest('hex')}`;
|
|
7839
|
+
* Scan a file for malicious patterns.
|
|
7840
|
+
* Convenience wrapper that reads the file then calls scan().
|
|
7841
|
+
*/ scanFile(filePath) {
|
|
7842
|
+
if (!external_node_fs_.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
7843
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
7844
|
+
return this.scan(content);
|
|
8074
7845
|
}
|
|
8075
7846
|
}
|
|
8076
7847
|
/**
|
|
8077
|
-
*
|
|
7848
|
+
* Publisher - Handle Git information and publish payload building
|
|
8078
7849
|
*
|
|
8079
|
-
*
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
// Safe Zone Masking
|
|
7850
|
+
* Extracts Git metadata and builds the payload for publishing to registry.
|
|
7851
|
+
*/ class PublishError extends Error {
|
|
7852
|
+
constructor(message){
|
|
7853
|
+
super(message);
|
|
7854
|
+
this.name = 'PublishError';
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
8087
7857
|
// ============================================================================
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
result.push(maskLine(line));
|
|
8115
|
-
continue;
|
|
7858
|
+
// Publisher Class
|
|
7859
|
+
// ============================================================================
|
|
7860
|
+
class Publisher {
|
|
7861
|
+
/**
|
|
7862
|
+
* Get Git information from a skill directory
|
|
7863
|
+
*/ async getGitInfo(skillPath, specifiedTag) {
|
|
7864
|
+
const info = {
|
|
7865
|
+
isRepo: false,
|
|
7866
|
+
remoteUrl: null,
|
|
7867
|
+
currentBranch: null,
|
|
7868
|
+
currentCommit: null,
|
|
7869
|
+
commitDate: null,
|
|
7870
|
+
tag: null,
|
|
7871
|
+
tagCommit: null,
|
|
7872
|
+
isDirty: false,
|
|
7873
|
+
sourceRef: null
|
|
7874
|
+
};
|
|
7875
|
+
// Check if it's a git repository
|
|
7876
|
+
try {
|
|
7877
|
+
(0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git rev-parse --git-dir', {
|
|
7878
|
+
cwd: skillPath,
|
|
7879
|
+
stdio: 'pipe'
|
|
7880
|
+
});
|
|
7881
|
+
info.isRepo = true;
|
|
7882
|
+
} catch {
|
|
7883
|
+
return info;
|
|
8116
7884
|
}
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
7885
|
+
// Get remote URL
|
|
7886
|
+
try {
|
|
7887
|
+
info.remoteUrl = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git remote get-url origin', {
|
|
7888
|
+
cwd: skillPath,
|
|
7889
|
+
encoding: 'utf-8'
|
|
7890
|
+
}).trim();
|
|
7891
|
+
// Parse to sourceRef format
|
|
7892
|
+
info.sourceRef = this.parseRemoteToSourceRef(info.remoteUrl);
|
|
7893
|
+
} catch {
|
|
7894
|
+
// No remote configured
|
|
8121
7895
|
}
|
|
8122
|
-
//
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
prevLineIndentedCode = false;
|
|
8131
|
-
continue;
|
|
7896
|
+
// Get current branch
|
|
7897
|
+
try {
|
|
7898
|
+
info.currentBranch = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git branch --show-current', {
|
|
7899
|
+
cwd: skillPath,
|
|
7900
|
+
encoding: 'utf-8'
|
|
7901
|
+
}).trim() || null;
|
|
7902
|
+
} catch {
|
|
7903
|
+
// Detached HEAD or other error
|
|
8132
7904
|
}
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
7905
|
+
// Get current commit
|
|
7906
|
+
try {
|
|
7907
|
+
info.currentCommit = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git rev-parse HEAD', {
|
|
7908
|
+
cwd: skillPath,
|
|
7909
|
+
encoding: 'utf-8'
|
|
7910
|
+
}).trim();
|
|
7911
|
+
// Get commit date
|
|
7912
|
+
info.commitDate = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git show -s --format=%cI HEAD', {
|
|
7913
|
+
cwd: skillPath,
|
|
7914
|
+
encoding: 'utf-8'
|
|
7915
|
+
}).trim();
|
|
7916
|
+
} catch {
|
|
7917
|
+
// No commits yet
|
|
8140
7918
|
}
|
|
8141
|
-
//
|
|
8142
|
-
if (
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
7919
|
+
// Get tag
|
|
7920
|
+
if (specifiedTag) // Use specified tag
|
|
7921
|
+
try {
|
|
7922
|
+
info.tagCommit = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)(`git rev-parse "${specifiedTag}^{commit}"`, {
|
|
7923
|
+
cwd: skillPath,
|
|
7924
|
+
encoding: 'utf-8'
|
|
7925
|
+
}).trim();
|
|
7926
|
+
info.tag = specifiedTag;
|
|
7927
|
+
} catch {
|
|
7928
|
+
throw new PublishError(`Tag "${specifiedTag}" not found`);
|
|
8147
7929
|
}
|
|
8148
|
-
//
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
7930
|
+
else // Try to get tag on current commit
|
|
7931
|
+
try {
|
|
7932
|
+
const tag = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git describe --exact-match --tags HEAD', {
|
|
7933
|
+
cwd: skillPath,
|
|
7934
|
+
encoding: 'utf-8'
|
|
7935
|
+
}).trim();
|
|
7936
|
+
info.tag = tag;
|
|
7937
|
+
info.tagCommit = info.currentCommit;
|
|
7938
|
+
} catch {
|
|
7939
|
+
// No tag on current commit
|
|
8154
7940
|
}
|
|
8155
|
-
//
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
}
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
7941
|
+
// Check if working tree is dirty
|
|
7942
|
+
try {
|
|
7943
|
+
const status = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git status --porcelain', {
|
|
7944
|
+
cwd: skillPath,
|
|
7945
|
+
encoding: 'utf-8'
|
|
7946
|
+
}).trim();
|
|
7947
|
+
info.isDirty = status.length > 0;
|
|
7948
|
+
} catch {
|
|
7949
|
+
// Ignore errors
|
|
7950
|
+
}
|
|
7951
|
+
return info;
|
|
7952
|
+
}
|
|
7953
|
+
/**
|
|
7954
|
+
* Parse remote URL to sourceRef format (e.g., github:user/repo)
|
|
7955
|
+
*/ parseRemoteToSourceRef(remoteUrl) {
|
|
7956
|
+
// SSH format: git@github.com:user/repo.git
|
|
7957
|
+
const sshMatch = remoteUrl.match(/^git@([^:]+):([^/]+)\/(.+?)(\.git)?$/);
|
|
7958
|
+
if (sshMatch) {
|
|
7959
|
+
const [, host, owner, repo] = sshMatch;
|
|
7960
|
+
const registry = this.normalizeHost(host);
|
|
7961
|
+
return `${registry}:${owner}/${repo.replace(/\.git$/, '')}`;
|
|
7962
|
+
}
|
|
7963
|
+
// HTTPS format: https://github.com/user/repo.git
|
|
7964
|
+
const httpsMatch = remoteUrl.match(/^https?:\/\/([^/]+)\/([^/]+)\/(.+?)(\.git)?$/);
|
|
7965
|
+
if (httpsMatch) {
|
|
7966
|
+
const [, host, owner, repo] = httpsMatch;
|
|
7967
|
+
const registry = this.normalizeHost(host);
|
|
7968
|
+
return `${registry}:${owner}/${repo.replace(/\.git$/, '')}`;
|
|
7969
|
+
}
|
|
7970
|
+
return null;
|
|
7971
|
+
}
|
|
7972
|
+
/**
|
|
7973
|
+
* Normalize host to registry name
|
|
7974
|
+
*/ normalizeHost(host) {
|
|
7975
|
+
if ('github.com' === host) return 'github';
|
|
7976
|
+
if ('gitlab.com' === host) return 'gitlab';
|
|
7977
|
+
return host;
|
|
7978
|
+
}
|
|
7979
|
+
/**
|
|
7980
|
+
* Build publish payload
|
|
7981
|
+
*/ buildPayload(skill, gitInfo, integrity) {
|
|
7982
|
+
const { skillJson, skillMd, readme, files } = skill;
|
|
7983
|
+
const payload = {
|
|
7984
|
+
version: skillJson.version,
|
|
7985
|
+
description: skillJson.description || '',
|
|
7986
|
+
gitRef: gitInfo.tag || gitInfo.currentCommit || 'HEAD',
|
|
7987
|
+
gitCommit: gitInfo.tagCommit || gitInfo.currentCommit || '',
|
|
7988
|
+
gitCommitDate: gitInfo.commitDate || void 0,
|
|
7989
|
+
repositoryUrl: gitInfo.remoteUrl || '',
|
|
7990
|
+
sourceRef: gitInfo.sourceRef || '',
|
|
7991
|
+
skillJson,
|
|
7992
|
+
files,
|
|
7993
|
+
entry: skillJson.entry || 'SKILL.md',
|
|
7994
|
+
integrity
|
|
7995
|
+
};
|
|
7996
|
+
// Add optional fields
|
|
7997
|
+
if (skillMd) payload.skillMd = {
|
|
7998
|
+
name: skillMd.name,
|
|
7999
|
+
description: skillMd.description,
|
|
8000
|
+
license: skillMd.license,
|
|
8001
|
+
compatibility: skillMd.compatibility,
|
|
8002
|
+
allowedTools: skillMd.allowedTools
|
|
8003
|
+
};
|
|
8004
|
+
if (readme) payload.readmePreview = readme;
|
|
8005
|
+
if (skillJson.keywords && skillJson.keywords.length > 0) payload.keywords = skillJson.keywords;
|
|
8006
|
+
if (skillJson.compatibility) {
|
|
8007
|
+
// Filter out undefined values from compatibility
|
|
8008
|
+
const compat = {};
|
|
8009
|
+
for (const [key, value] of Object.entries(skillJson.compatibility))if (void 0 !== value) compat[key] = value;
|
|
8010
|
+
if (Object.keys(compat).length > 0) payload.compatibility = compat;
|
|
8011
|
+
}
|
|
8012
|
+
return payload;
|
|
8013
|
+
}
|
|
8014
|
+
/**
|
|
8015
|
+
* Format bytes for display
|
|
8016
|
+
*/ formatBytes(bytes) {
|
|
8017
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
8018
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
8019
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
8020
|
+
}
|
|
8021
|
+
/**
|
|
8022
|
+
* Calculate total size of files
|
|
8023
|
+
*/ calculateTotalSize(skillPath, files) {
|
|
8024
|
+
let total = 0;
|
|
8025
|
+
for (const file of files){
|
|
8026
|
+
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
8027
|
+
if (external_node_fs_.existsSync(filePath)) {
|
|
8028
|
+
const stats = external_node_fs_.statSync(filePath);
|
|
8029
|
+
total += stats.size;
|
|
8030
|
+
}
|
|
8031
|
+
}
|
|
8032
|
+
return total;
|
|
8033
|
+
}
|
|
8176
8034
|
}
|
|
8035
|
+
/**
|
|
8036
|
+
* SkillValidator - Validate skills for publishing
|
|
8037
|
+
*
|
|
8038
|
+
* Following agentskills.io specification: https://agentskills.io/specification
|
|
8039
|
+
*
|
|
8040
|
+
* Key points:
|
|
8041
|
+
* - SKILL.md is the SOLE source of metadata (name, description, version, etc.)
|
|
8042
|
+
* - skill.json is NOT used - all metadata comes from SKILL.md frontmatter
|
|
8043
|
+
* - Version defaults to "0.0.0" if not specified in SKILL.md
|
|
8044
|
+
*/ // ============================================================================
|
|
8045
|
+
// Constants
|
|
8177
8046
|
// ============================================================================
|
|
8178
|
-
|
|
8047
|
+
const MAX_NAME_LENGTH = 64;
|
|
8048
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
8049
|
+
const MAX_KEYWORDS = 10;
|
|
8050
|
+
const SINGLE_CHAR_NAME_PATTERN = /^[a-z0-9]$/;
|
|
8051
|
+
const DEFAULT_VERSION = '0.0.0';
|
|
8052
|
+
// Default files to include in publish
|
|
8053
|
+
const DEFAULT_FILES = [
|
|
8054
|
+
'SKILL.md',
|
|
8055
|
+
'README.md',
|
|
8056
|
+
'LICENSE'
|
|
8057
|
+
];
|
|
8179
8058
|
// ============================================================================
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8059
|
+
// SkillValidator Class
|
|
8060
|
+
// ============================================================================
|
|
8061
|
+
class SkillValidator {
|
|
8062
|
+
/**
|
|
8063
|
+
* Validate skill name format
|
|
8064
|
+
*
|
|
8065
|
+
* Requirements:
|
|
8066
|
+
* - Lowercase letters, numbers, and hyphens only
|
|
8067
|
+
* - 1-64 characters
|
|
8068
|
+
* - Cannot start or end with hyphen
|
|
8069
|
+
* - Cannot have consecutive hyphens
|
|
8070
|
+
*/ validateName(name) {
|
|
8071
|
+
const errors = [];
|
|
8072
|
+
if (!name) {
|
|
8073
|
+
errors.push({
|
|
8074
|
+
field: 'name',
|
|
8075
|
+
message: 'Skill name is required',
|
|
8076
|
+
suggestion: 'Add "name" field to SKILL.md frontmatter'
|
|
8077
|
+
});
|
|
8078
|
+
return {
|
|
8079
|
+
valid: false,
|
|
8080
|
+
errors,
|
|
8081
|
+
warnings: []
|
|
8082
|
+
};
|
|
8083
|
+
}
|
|
8084
|
+
if (name.length > MAX_NAME_LENGTH) errors.push({
|
|
8085
|
+
field: 'name',
|
|
8086
|
+
message: `Skill name must be at most ${MAX_NAME_LENGTH} characters`,
|
|
8087
|
+
suggestion: `Shorten the name to ${MAX_NAME_LENGTH} characters or less`
|
|
8186
8088
|
});
|
|
8187
|
-
|
|
8089
|
+
// Check for uppercase
|
|
8090
|
+
if (/[A-Z]/.test(name)) errors.push({
|
|
8091
|
+
field: 'name',
|
|
8092
|
+
message: 'Skill name must be lowercase',
|
|
8093
|
+
suggestion: `Change "${name}" to "${name.toLowerCase()}"`
|
|
8094
|
+
});
|
|
8095
|
+
// Check for invalid characters
|
|
8096
|
+
if (/[^a-z0-9-]/.test(name)) errors.push({
|
|
8097
|
+
field: 'name',
|
|
8098
|
+
message: 'Skill name can only contain lowercase letters, numbers, and hyphens',
|
|
8099
|
+
suggestion: 'Remove special characters from the name'
|
|
8100
|
+
});
|
|
8101
|
+
// Check pattern for multi-char names
|
|
8102
|
+
if (1 === name.length) {
|
|
8103
|
+
if (!SINGLE_CHAR_NAME_PATTERN.test(name)) errors.push({
|
|
8104
|
+
field: 'name',
|
|
8105
|
+
message: 'Single character name must be a lowercase letter or number'
|
|
8106
|
+
});
|
|
8107
|
+
} else if (name.length > 1) {
|
|
8108
|
+
// Check start/end with hyphen
|
|
8109
|
+
if (name.startsWith('-')) errors.push({
|
|
8110
|
+
field: 'name',
|
|
8111
|
+
message: 'Skill name cannot start with a hyphen'
|
|
8112
|
+
});
|
|
8113
|
+
if (name.endsWith('-')) errors.push({
|
|
8114
|
+
field: 'name',
|
|
8115
|
+
message: 'Skill name cannot end with a hyphen'
|
|
8116
|
+
});
|
|
8117
|
+
// Check consecutive hyphens
|
|
8118
|
+
if (/--/.test(name)) errors.push({
|
|
8119
|
+
field: 'name',
|
|
8120
|
+
message: 'Skill name cannot contain consecutive hyphens'
|
|
8121
|
+
});
|
|
8122
|
+
}
|
|
8123
|
+
return {
|
|
8124
|
+
valid: 0 === errors.length,
|
|
8125
|
+
errors,
|
|
8126
|
+
warnings: []
|
|
8127
|
+
};
|
|
8188
8128
|
}
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
id: 'prompt-injection',
|
|
8199
|
-
level: 'high',
|
|
8200
|
-
message: 'Detected prompt injection attempt',
|
|
8201
|
-
skipSafeZones: true,
|
|
8202
|
-
check: (content)=>findLineMatches(content, [
|
|
8203
|
-
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
8204
|
-
/disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
|
|
8205
|
-
/you\s+are\s+now\s+/i,
|
|
8206
|
-
/from\s+now\s+on[,\s]+you\s+are/i,
|
|
8207
|
-
/new\s+system\s+prompt/i,
|
|
8208
|
-
/override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
|
|
8209
|
-
/forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
|
|
8210
|
-
/entering\s+(a\s+)?new\s+(mode|context|session)/i
|
|
8211
|
-
])
|
|
8212
|
-
},
|
|
8213
|
-
// Rule 2: Data Exfiltration (high)
|
|
8214
|
-
{
|
|
8215
|
-
id: 'data-exfiltration',
|
|
8216
|
-
level: 'high',
|
|
8217
|
-
message: 'Detected potential data exfiltration command',
|
|
8218
|
-
skipSafeZones: true,
|
|
8219
|
-
check: (content)=>{
|
|
8220
|
-
const lines = content.split('\n');
|
|
8221
|
-
const matches = [];
|
|
8222
|
-
const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
|
|
8223
|
-
const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
|
|
8224
|
-
for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
|
|
8225
|
-
line: i + 1
|
|
8129
|
+
/**
|
|
8130
|
+
* Validate version format (semver)
|
|
8131
|
+
*/ validateVersion(version) {
|
|
8132
|
+
const errors = [];
|
|
8133
|
+
if (!version) {
|
|
8134
|
+
errors.push({
|
|
8135
|
+
field: 'version',
|
|
8136
|
+
message: 'Version is required',
|
|
8137
|
+
suggestion: 'Add "version" field to SKILL.md frontmatter (e.g., "1.0.0")'
|
|
8226
8138
|
});
|
|
8227
|
-
return
|
|
8139
|
+
return {
|
|
8140
|
+
valid: false,
|
|
8141
|
+
errors,
|
|
8142
|
+
warnings: []
|
|
8143
|
+
};
|
|
8228
8144
|
}
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
skipSafeZones: false,
|
|
8236
|
-
check: (content)=>{
|
|
8237
|
-
const matches = [];
|
|
8238
|
-
const lines = content.split('\n');
|
|
8239
|
-
// Zero-width characters (suspicious in any context)
|
|
8240
|
-
const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
|
|
8241
|
-
for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
|
|
8242
|
-
line: i + 1,
|
|
8243
|
-
snippet: 'Zero-width Unicode characters detected'
|
|
8145
|
+
// Check for v prefix
|
|
8146
|
+
if (version.startsWith('v')) {
|
|
8147
|
+
errors.push({
|
|
8148
|
+
field: 'version',
|
|
8149
|
+
message: 'Version should not have "v" prefix',
|
|
8150
|
+
suggestion: `Change "${version}" to "${version.slice(1)}"`
|
|
8244
8151
|
});
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8152
|
+
return {
|
|
8153
|
+
valid: false,
|
|
8154
|
+
errors,
|
|
8155
|
+
warnings: []
|
|
8156
|
+
};
|
|
8157
|
+
}
|
|
8158
|
+
if (!__WEBPACK_EXTERNAL_MODULE_semver__.valid(version)) errors.push({
|
|
8159
|
+
field: 'version',
|
|
8160
|
+
message: `Invalid version format: "${version}". Must follow semver (x.y.z)`,
|
|
8161
|
+
suggestion: 'Use format like "1.0.0" or "1.0.0-beta.1"'
|
|
8162
|
+
});
|
|
8163
|
+
return {
|
|
8164
|
+
valid: 0 === errors.length,
|
|
8165
|
+
errors,
|
|
8166
|
+
warnings: []
|
|
8167
|
+
};
|
|
8168
|
+
}
|
|
8169
|
+
/**
|
|
8170
|
+
* Validate description
|
|
8171
|
+
*
|
|
8172
|
+
* Following agentskills.io specification:
|
|
8173
|
+
* - Max 1024 characters
|
|
8174
|
+
* - Non-empty
|
|
8175
|
+
*/ validateDescription(description) {
|
|
8176
|
+
const errors = [];
|
|
8177
|
+
if (!description) {
|
|
8178
|
+
errors.push({
|
|
8179
|
+
field: 'description',
|
|
8180
|
+
message: 'Description is required',
|
|
8181
|
+
suggestion: 'Add "description" field to SKILL.md frontmatter'
|
|
8250
8182
|
});
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
8257
|
-
matches.push({
|
|
8258
|
-
line: lineNum,
|
|
8259
|
-
snippet: `Large HTML comment block (${match[1].length} chars)`
|
|
8260
|
-
});
|
|
8261
|
-
}
|
|
8262
|
-
return matches;
|
|
8183
|
+
return {
|
|
8184
|
+
valid: false,
|
|
8185
|
+
errors,
|
|
8186
|
+
warnings: []
|
|
8187
|
+
};
|
|
8263
8188
|
}
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8189
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) errors.push({
|
|
8190
|
+
field: 'description',
|
|
8191
|
+
message: `Description must be at most ${MAX_DESCRIPTION_LENGTH} characters`
|
|
8192
|
+
});
|
|
8193
|
+
// Note: angle brackets are allowed per agentskills.io spec
|
|
8194
|
+
return {
|
|
8195
|
+
valid: 0 === errors.length,
|
|
8196
|
+
errors,
|
|
8197
|
+
warnings: []
|
|
8198
|
+
};
|
|
8199
|
+
}
|
|
8200
|
+
/**
|
|
8201
|
+
* Load skill information from directory
|
|
8202
|
+
*
|
|
8203
|
+
* Following agentskills.io specification:
|
|
8204
|
+
* - SKILL.md is the SOLE source of metadata
|
|
8205
|
+
* - skillJson is synthesized from SKILL.md for backward compatibility with publish API
|
|
8206
|
+
*/ loadSkill(skillPath) {
|
|
8207
|
+
const result = {
|
|
8208
|
+
path: skillPath,
|
|
8209
|
+
skillJson: null,
|
|
8210
|
+
skillMd: null,
|
|
8211
|
+
readme: null,
|
|
8212
|
+
files: []
|
|
8213
|
+
};
|
|
8214
|
+
// Load SKILL.md (sole source of metadata)
|
|
8215
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'SKILL.md');
|
|
8216
|
+
if (external_node_fs_.existsSync(skillMdPath)) try {
|
|
8217
|
+
result.skillMd = parseSkillMdFile(skillMdPath);
|
|
8218
|
+
// Always synthesize skillJson from SKILL.md for backward compatibility
|
|
8219
|
+
if (result.skillMd) result.skillJson = this.synthesizeSkillJson(result.skillMd);
|
|
8220
|
+
} catch {
|
|
8221
|
+
// Will be caught in validation
|
|
8222
|
+
}
|
|
8223
|
+
// Load README.md
|
|
8224
|
+
const readmePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'README.md');
|
|
8225
|
+
if (external_node_fs_.existsSync(readmePath)) {
|
|
8226
|
+
const content = external_node_fs_.readFileSync(readmePath, 'utf-8');
|
|
8227
|
+
// Only keep first 500 chars as preview
|
|
8228
|
+
result.readme = content.slice(0, 500);
|
|
8229
|
+
}
|
|
8230
|
+
// Scan files (use files array from SKILL.md metadata if available)
|
|
8231
|
+
const filesPattern = result.skillMd?.metadata?.files;
|
|
8232
|
+
result.files = this.scanFiles(skillPath, filesPattern);
|
|
8233
|
+
return result;
|
|
8234
|
+
}
|
|
8235
|
+
/**
|
|
8236
|
+
* Synthesize a SkillJson object from SKILL.md frontmatter
|
|
8237
|
+
*
|
|
8238
|
+
* This creates a SkillJson representation from SKILL.md for backward compatibility
|
|
8239
|
+
* with the publish API. All metadata comes from SKILL.md.
|
|
8240
|
+
*/ synthesizeSkillJson(skillMd) {
|
|
8241
|
+
// Extract version: first from top-level frontmatter, then from metadata, then default
|
|
8242
|
+
const version = skillMd.version || skillMd.metadata?.version || DEFAULT_VERSION;
|
|
8243
|
+
// Only include keywords if it's a valid array
|
|
8244
|
+
const keywords = Array.isArray(skillMd.metadata?.keywords) ? skillMd.metadata.keywords : void 0;
|
|
8245
|
+
return {
|
|
8246
|
+
name: skillMd.name,
|
|
8247
|
+
version,
|
|
8248
|
+
description: skillMd.description,
|
|
8249
|
+
license: skillMd.license,
|
|
8250
|
+
keywords,
|
|
8251
|
+
entry: 'SKILL.md'
|
|
8252
|
+
};
|
|
8253
|
+
}
|
|
8254
|
+
/**
|
|
8255
|
+
* Scan files to include in publish
|
|
8256
|
+
*
|
|
8257
|
+
* If includePatterns is specified, only include those files/directories.
|
|
8258
|
+
* Otherwise, scan all files in the directory (excluding ignored patterns).
|
|
8259
|
+
*/ scanFiles(skillPath, includePatterns) {
|
|
8260
|
+
const files = [];
|
|
8261
|
+
const seen = new Set();
|
|
8262
|
+
// If includePatterns specified, use selective scanning
|
|
8263
|
+
if (includePatterns && includePatterns.length > 0) {
|
|
8264
|
+
// Add default files first
|
|
8265
|
+
for (const file of DEFAULT_FILES){
|
|
8266
|
+
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
8267
|
+
if (external_node_fs_.existsSync(filePath) && !seen.has(file)) {
|
|
8268
|
+
files.push(file);
|
|
8269
|
+
seen.add(file);
|
|
8314
8270
|
}
|
|
8315
8271
|
}
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
const sizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
8328
|
-
if (sizeBytes > MAX_SIZE_BYTES) return [
|
|
8329
|
-
{
|
|
8330
|
-
snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
|
|
8272
|
+
// Add files from SKILL.md metadata.files array
|
|
8273
|
+
for (const pattern of includePatterns){
|
|
8274
|
+
const targetPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, pattern);
|
|
8275
|
+
if (external_node_fs_.existsSync(targetPath)) {
|
|
8276
|
+
const stat = external_node_fs_.statSync(targetPath);
|
|
8277
|
+
if (stat.isDirectory()) // Recursively add all files in directory
|
|
8278
|
+
this.addFilesFromDir(skillPath, pattern, files, seen);
|
|
8279
|
+
else if (!seen.has(pattern)) {
|
|
8280
|
+
files.push(pattern);
|
|
8281
|
+
seen.add(pattern);
|
|
8282
|
+
}
|
|
8331
8283
|
}
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
];
|
|
8337
|
-
// ============================================================================
|
|
8338
|
-
// ContentScanner
|
|
8339
|
-
// ============================================================================
|
|
8340
|
-
/** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
|
|
8341
|
-
let rules = DEFAULT_RULES.map((r)=>({
|
|
8342
|
-
...r
|
|
8343
|
-
}));
|
|
8344
|
-
if (options?.disabledRules?.length) {
|
|
8345
|
-
const disabled = new Set(options.disabledRules);
|
|
8346
|
-
rules = rules.filter((r)=>!disabled.has(r.id));
|
|
8284
|
+
}
|
|
8285
|
+
} else // No includePatterns: scan entire directory (default behavior)
|
|
8286
|
+
this.addFilesFromDir(skillPath, '', files, seen);
|
|
8287
|
+
return files;
|
|
8347
8288
|
}
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8289
|
+
/**
|
|
8290
|
+
* Patterns to ignore when scanning directories
|
|
8291
|
+
*/ static IGNORE_PATTERNS = [
|
|
8292
|
+
'.git',
|
|
8293
|
+
'.svn',
|
|
8294
|
+
'.hg',
|
|
8295
|
+
'node_modules',
|
|
8296
|
+
'.DS_Store',
|
|
8297
|
+
'Thumbs.db',
|
|
8298
|
+
'.idea',
|
|
8299
|
+
'.vscode',
|
|
8300
|
+
'*.log',
|
|
8301
|
+
'*.tmp',
|
|
8302
|
+
'*.swp',
|
|
8303
|
+
'*.bak'
|
|
8304
|
+
];
|
|
8305
|
+
/**
|
|
8306
|
+
* Check if a file/directory should be ignored
|
|
8307
|
+
*/ shouldIgnore(name) {
|
|
8308
|
+
for (const pattern of SkillValidator.IGNORE_PATTERNS)if (pattern.startsWith('*')) {
|
|
8309
|
+
// Wildcard pattern (e.g., *.log)
|
|
8310
|
+
const ext = pattern.slice(1);
|
|
8311
|
+
if (name.endsWith(ext)) return true;
|
|
8312
|
+
} else if (name === pattern) return true;
|
|
8313
|
+
return false;
|
|
8351
8314
|
}
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
* overrides: { 'prompt-injection': 'medium' },
|
|
8370
|
-
* disabledRules: ['stealth-instructions'],
|
|
8371
|
-
* });
|
|
8372
|
-
* ```
|
|
8373
|
-
*/ class ContentScanner {
|
|
8374
|
-
rules;
|
|
8375
|
-
constructor(options){
|
|
8376
|
-
this.rules = buildRuleSet(options);
|
|
8315
|
+
/**
|
|
8316
|
+
* Recursively add files from directory
|
|
8317
|
+
*/ addFilesFromDir(basePath, dirPath, files, seen) {
|
|
8318
|
+
const fullPath = dirPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.join(basePath, dirPath) : basePath;
|
|
8319
|
+
const entries = external_node_fs_.readdirSync(fullPath, {
|
|
8320
|
+
withFileTypes: true
|
|
8321
|
+
});
|
|
8322
|
+
for (const entry of entries){
|
|
8323
|
+
// Skip ignored files/directories
|
|
8324
|
+
if (this.shouldIgnore(entry.name)) continue;
|
|
8325
|
+
const relativePath = dirPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, entry.name) : entry.name;
|
|
8326
|
+
if (entry.isDirectory()) this.addFilesFromDir(basePath, relativePath, files, seen);
|
|
8327
|
+
else if (!seen.has(relativePath)) {
|
|
8328
|
+
files.push(relativePath);
|
|
8329
|
+
seen.add(relativePath);
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8377
8332
|
}
|
|
8378
8333
|
/**
|
|
8379
|
-
*
|
|
8380
|
-
*
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8334
|
+
* Validate a skill directory for publishing
|
|
8335
|
+
*
|
|
8336
|
+
* Following agentskills.io specification:
|
|
8337
|
+
* - SKILL.md is the SOLE source of metadata
|
|
8338
|
+
* - name and description are REQUIRED in frontmatter
|
|
8339
|
+
*/ validate(skillPath) {
|
|
8340
|
+
const errors = [];
|
|
8341
|
+
const warnings = [];
|
|
8342
|
+
// Check SKILL.md exists (REQUIRED per spec)
|
|
8343
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'SKILL.md');
|
|
8344
|
+
if (!external_node_fs_.existsSync(skillMdPath)) {
|
|
8345
|
+
errors.push({
|
|
8346
|
+
field: 'SKILL.md',
|
|
8347
|
+
message: 'SKILL.md not found. This file is required for publishing.',
|
|
8348
|
+
suggestion: 'Create a SKILL.md file with name and description in YAML frontmatter'
|
|
8349
|
+
});
|
|
8350
|
+
return {
|
|
8351
|
+
valid: false,
|
|
8352
|
+
errors,
|
|
8353
|
+
warnings
|
|
8354
|
+
};
|
|
8355
|
+
}
|
|
8356
|
+
// Parse SKILL.md
|
|
8357
|
+
let skillMd;
|
|
8358
|
+
try {
|
|
8359
|
+
skillMd = parseSkillMdFile(skillMdPath);
|
|
8360
|
+
if (!skillMd) {
|
|
8361
|
+
errors.push({
|
|
8362
|
+
field: 'SKILL.md',
|
|
8363
|
+
message: 'SKILL.md must have valid YAML frontmatter with name and description',
|
|
8364
|
+
suggestion: 'Add frontmatter: ---\\nname: your-skill\\ndescription: Your description\\n---'
|
|
8397
8365
|
});
|
|
8366
|
+
return {
|
|
8367
|
+
valid: false,
|
|
8368
|
+
errors,
|
|
8369
|
+
warnings
|
|
8370
|
+
};
|
|
8398
8371
|
}
|
|
8372
|
+
} catch (error) {
|
|
8373
|
+
errors.push({
|
|
8374
|
+
field: 'SKILL.md',
|
|
8375
|
+
message: `Failed to parse SKILL.md: ${error.message}`,
|
|
8376
|
+
suggestion: 'Check the YAML frontmatter syntax is valid'
|
|
8377
|
+
});
|
|
8378
|
+
return {
|
|
8379
|
+
valid: false,
|
|
8380
|
+
errors,
|
|
8381
|
+
warnings
|
|
8382
|
+
};
|
|
8399
8383
|
}
|
|
8400
|
-
|
|
8384
|
+
// Validate name from SKILL.md
|
|
8385
|
+
const nameResult = this.validateName(skillMd.name);
|
|
8386
|
+
errors.push(...nameResult.errors);
|
|
8387
|
+
// Validate description from SKILL.md
|
|
8388
|
+
const descResult = this.validateDescription(skillMd.description);
|
|
8389
|
+
errors.push(...descResult.errors);
|
|
8390
|
+
// Check version in SKILL.md
|
|
8391
|
+
const skillMdVersion = skillMd.version || skillMd.metadata?.version;
|
|
8392
|
+
if (skillMdVersion) {
|
|
8393
|
+
// Validate the version from SKILL.md
|
|
8394
|
+
const versionResult = this.validateVersion(skillMdVersion);
|
|
8395
|
+
errors.push(...versionResult.errors);
|
|
8396
|
+
} else warnings.push({
|
|
8397
|
+
field: 'version',
|
|
8398
|
+
message: `No version specified, defaulting to "${DEFAULT_VERSION}"`,
|
|
8399
|
+
suggestion: 'Add version in SKILL.md frontmatter'
|
|
8400
|
+
});
|
|
8401
|
+
// Check keywords count (only if metadata.keywords is a valid array)
|
|
8402
|
+
const keywords = skillMd.metadata?.keywords;
|
|
8403
|
+
if (Array.isArray(keywords) && keywords.length > MAX_KEYWORDS) warnings.push({
|
|
8404
|
+
field: 'keywords',
|
|
8405
|
+
message: `Too many keywords (${keywords.length}). Recommended max: ${MAX_KEYWORDS}`
|
|
8406
|
+
});
|
|
8407
|
+
// Check license
|
|
8408
|
+
if (!skillMd.license) warnings.push({
|
|
8409
|
+
field: 'license',
|
|
8410
|
+
message: 'No license specified',
|
|
8411
|
+
suggestion: 'Add license in SKILL.md frontmatter'
|
|
8412
|
+
});
|
|
8401
8413
|
return {
|
|
8402
|
-
|
|
8403
|
-
|
|
8414
|
+
valid: 0 === errors.length,
|
|
8415
|
+
errors,
|
|
8416
|
+
warnings
|
|
8404
8417
|
};
|
|
8405
8418
|
}
|
|
8406
8419
|
/**
|
|
8407
|
-
*
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8420
|
+
* Generate integrity hash for files
|
|
8421
|
+
*/ generateIntegrity(skillPath, files) {
|
|
8422
|
+
const hash = __WEBPACK_EXTERNAL_MODULE_node_crypto__.createHash('sha256');
|
|
8423
|
+
// Sort files for consistent ordering
|
|
8424
|
+
const sortedFiles = [
|
|
8425
|
+
...files
|
|
8426
|
+
].sort();
|
|
8427
|
+
for (const file of sortedFiles){
|
|
8428
|
+
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
8429
|
+
if (external_node_fs_.existsSync(filePath)) {
|
|
8430
|
+
hash.update(file);
|
|
8431
|
+
const content = external_node_fs_.readFileSync(filePath);
|
|
8432
|
+
hash.update(content);
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
8435
|
+
return `sha256-${hash.digest('hex')}`;
|
|
8412
8436
|
}
|
|
8413
8437
|
}
|
|
8414
8438
|
/**
|
|
@@ -8773,7 +8797,7 @@ async function publishAction(skillPath, options) {
|
|
|
8773
8797
|
const validation = validator.validate(absolutePath);
|
|
8774
8798
|
// 3.5. Content security scan
|
|
8775
8799
|
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(absolutePath, 'SKILL.md');
|
|
8776
|
-
if (
|
|
8800
|
+
if (exists(skillMdPath)) {
|
|
8777
8801
|
const scanner = new ContentScanner();
|
|
8778
8802
|
const scanResult = scanner.scanFile(skillMdPath);
|
|
8779
8803
|
displayScanFindings(scanResult);
|