reskill 1.16.0-beta.1 → 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 +871 -870
- package/dist/core/content-scanner.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/scanner.js +1 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -7483,955 +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
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7767
|
+
}
|
|
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));
|
|
7779
|
+
}
|
|
7780
|
+
if (options?.overrides) for (const rule of rules){
|
|
7781
|
+
const override = options.overrides[rule.id];
|
|
7782
|
+
if (override) rule.level = override;
|
|
7783
|
+
}
|
|
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);
|
|
7872
7809
|
}
|
|
7873
7810
|
/**
|
|
7874
|
-
*
|
|
7875
|
-
*
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
const
|
|
7881
|
-
|
|
7882
|
-
|
|
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
|
+
});
|
|
7830
|
+
}
|
|
7831
|
+
}
|
|
7832
|
+
const hasHighRisk = findings.some((f)=>'high' === f.level);
|
|
7883
7833
|
return {
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
description: skillMd.description,
|
|
7887
|
-
license: skillMd.license,
|
|
7888
|
-
keywords,
|
|
7889
|
-
entry: 'SKILL.md'
|
|
7834
|
+
passed: !hasHighRisk,
|
|
7835
|
+
findings
|
|
7890
7836
|
};
|
|
7891
7837
|
}
|
|
7892
7838
|
/**
|
|
7893
|
-
* Scan
|
|
7894
|
-
*
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
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;
|
|
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);
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
/**
|
|
7848
|
+
* Publisher - Handle Git information and publish payload building
|
|
7849
|
+
*
|
|
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';
|
|
7926
7855
|
}
|
|
7856
|
+
}
|
|
7857
|
+
// ============================================================================
|
|
7858
|
+
// Publisher Class
|
|
7859
|
+
// ============================================================================
|
|
7860
|
+
class Publisher {
|
|
7927
7861
|
/**
|
|
7928
|
-
*
|
|
7929
|
-
*/
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
'
|
|
7942
|
-
|
|
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;
|
|
7884
|
+
}
|
|
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
|
|
7895
|
+
}
|
|
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
|
|
7904
|
+
}
|
|
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
|
|
7918
|
+
}
|
|
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`);
|
|
7929
|
+
}
|
|
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
|
|
7940
|
+
}
|
|
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
|
+
}
|
|
7943
7953
|
/**
|
|
7944
|
-
*
|
|
7945
|
-
*/
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
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;
|
|
7952
7971
|
}
|
|
7953
7972
|
/**
|
|
7954
|
-
*
|
|
7955
|
-
*/
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
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;
|
|
7968
8030
|
}
|
|
7969
8031
|
}
|
|
8032
|
+
return total;
|
|
7970
8033
|
}
|
|
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
|
|
8046
|
+
// ============================================================================
|
|
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
|
+
];
|
|
8058
|
+
// ============================================================================
|
|
8059
|
+
// SkillValidator Class
|
|
8060
|
+
// ============================================================================
|
|
8061
|
+
class SkillValidator {
|
|
7971
8062
|
/**
|
|
7972
|
-
* Validate
|
|
8063
|
+
* Validate skill name format
|
|
7973
8064
|
*
|
|
7974
|
-
*
|
|
7975
|
-
* -
|
|
7976
|
-
* -
|
|
7977
|
-
|
|
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) {
|
|
7978
8071
|
const errors = [];
|
|
7979
|
-
|
|
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)) {
|
|
8072
|
+
if (!name) {
|
|
7983
8073
|
errors.push({
|
|
7984
|
-
field: '
|
|
7985
|
-
message: '
|
|
7986
|
-
suggestion: '
|
|
8074
|
+
field: 'name',
|
|
8075
|
+
message: 'Skill name is required',
|
|
8076
|
+
suggestion: 'Add "name" field to SKILL.md frontmatter'
|
|
7987
8077
|
});
|
|
7988
8078
|
return {
|
|
7989
8079
|
valid: false,
|
|
7990
8080
|
errors,
|
|
7991
|
-
warnings
|
|
8081
|
+
warnings: []
|
|
7992
8082
|
};
|
|
7993
8083
|
}
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
}
|
|
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`
|
|
8088
|
+
});
|
|
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
|
+
};
|
|
8128
|
+
}
|
|
8129
|
+
/**
|
|
8130
|
+
* Validate version format (semver)
|
|
8131
|
+
*/ validateVersion(version) {
|
|
8132
|
+
const errors = [];
|
|
8133
|
+
if (!version) {
|
|
8011
8134
|
errors.push({
|
|
8012
|
-
field: '
|
|
8013
|
-
message:
|
|
8014
|
-
suggestion: '
|
|
8135
|
+
field: 'version',
|
|
8136
|
+
message: 'Version is required',
|
|
8137
|
+
suggestion: 'Add "version" field to SKILL.md frontmatter (e.g., "1.0.0")'
|
|
8015
8138
|
});
|
|
8016
8139
|
return {
|
|
8017
8140
|
valid: false,
|
|
8018
8141
|
errors,
|
|
8019
|
-
warnings
|
|
8142
|
+
warnings: []
|
|
8020
8143
|
};
|
|
8021
8144
|
}
|
|
8022
|
-
//
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
}
|
|
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)}"`
|
|
8151
|
+
});
|
|
8152
|
+
return {
|
|
8153
|
+
valid: false,
|
|
8154
|
+
errors,
|
|
8155
|
+
warnings: []
|
|
8156
|
+
};
|
|
8157
|
+
}
|
|
8158
|
+
if (!__WEBPACK_EXTERNAL_MODULE_semver__.valid(version)) errors.push({
|
|
8035
8159
|
field: 'version',
|
|
8036
|
-
message: `
|
|
8037
|
-
suggestion: '
|
|
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'
|
|
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"'
|
|
8050
8162
|
});
|
|
8051
8163
|
return {
|
|
8052
8164
|
valid: 0 === errors.length,
|
|
8053
8165
|
errors,
|
|
8054
|
-
warnings
|
|
8166
|
+
warnings: []
|
|
8055
8167
|
};
|
|
8056
8168
|
}
|
|
8057
8169
|
/**
|
|
8058
|
-
*
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
]
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
}
|
|
8076
|
-
/**
|
|
8077
|
-
* ContentScanner - Detect malicious patterns in SKILL.md content
|
|
8078
|
-
*
|
|
8079
|
-
* Features:
|
|
8080
|
-
* - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
|
|
8081
|
-
* - 6 built-in detection rules across 3 risk levels
|
|
8082
|
-
* - Configurable: override levels, disable rules, add custom rules
|
|
8083
|
-
* - Pure string operations in scan() — no fs dependency, suitable for server use
|
|
8084
|
-
* - scanFile() convenience method for CLI use
|
|
8085
|
-
*/ // ============================================================================
|
|
8086
|
-
// Safe Zone Masking
|
|
8087
|
-
// ============================================================================
|
|
8088
|
-
/**
|
|
8089
|
-
* Mask safe zones in Markdown content with spaces, preserving line structure.
|
|
8090
|
-
*
|
|
8091
|
-
* Safe zones (content replaced with spaces):
|
|
8092
|
-
* - YAML frontmatter (`---` ... `---` at file start)
|
|
8093
|
-
* - Fenced code blocks (``` or ~~~)
|
|
8094
|
-
* - Indented code blocks (4 spaces / tab after blank line)
|
|
8095
|
-
* - Blockquotes (`> ` prefix)
|
|
8096
|
-
* - Inline code (`` `...` ``)
|
|
8097
|
-
* - Double-quoted text (`"..."`, min 3 chars between quotes)
|
|
8098
|
-
*
|
|
8099
|
-
* Line breaks are preserved so line numbers remain correct.
|
|
8100
|
-
*/ function maskSafeZones(content) {
|
|
8101
|
-
const lines = content.split('\n');
|
|
8102
|
-
const result = [];
|
|
8103
|
-
let inFrontmatter = false;
|
|
8104
|
-
let inFencedCode = false;
|
|
8105
|
-
let fenceChar = '';
|
|
8106
|
-
let fenceLength = 0;
|
|
8107
|
-
let prevLineBlank = false;
|
|
8108
|
-
let prevLineIndentedCode = false;
|
|
8109
|
-
for(let i = 0; i < lines.length; i++){
|
|
8110
|
-
const line = lines[i];
|
|
8111
|
-
// --- YAML Frontmatter (only at file start) ---
|
|
8112
|
-
if (0 === i && '---' === line.trim()) {
|
|
8113
|
-
inFrontmatter = true;
|
|
8114
|
-
result.push(maskLine(line));
|
|
8115
|
-
continue;
|
|
8116
|
-
}
|
|
8117
|
-
if (inFrontmatter) {
|
|
8118
|
-
result.push(maskLine(line));
|
|
8119
|
-
if ('---' === line.trim()) inFrontmatter = false;
|
|
8120
|
-
continue;
|
|
8121
|
-
}
|
|
8122
|
-
// --- Fenced code blocks (``` or ~~~) ---
|
|
8123
|
-
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
8124
|
-
if (!inFencedCode && fenceMatch) {
|
|
8125
|
-
inFencedCode = true;
|
|
8126
|
-
fenceChar = fenceMatch[1][0];
|
|
8127
|
-
fenceLength = fenceMatch[1].length;
|
|
8128
|
-
result.push(maskLine(line));
|
|
8129
|
-
prevLineBlank = false;
|
|
8130
|
-
prevLineIndentedCode = false;
|
|
8131
|
-
continue;
|
|
8132
|
-
}
|
|
8133
|
-
if (inFencedCode) {
|
|
8134
|
-
result.push(maskLine(line));
|
|
8135
|
-
const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
|
|
8136
|
-
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
|
|
8137
|
-
prevLineBlank = false;
|
|
8138
|
-
prevLineIndentedCode = false;
|
|
8139
|
-
continue;
|
|
8140
|
-
}
|
|
8141
|
-
// --- Blockquote ---
|
|
8142
|
-
if (/^>\s?/.test(line)) {
|
|
8143
|
-
result.push(maskLine(line));
|
|
8144
|
-
prevLineBlank = false;
|
|
8145
|
-
prevLineIndentedCode = false;
|
|
8146
|
-
continue;
|
|
8147
|
-
}
|
|
8148
|
-
// --- Indented code block (4 spaces or tab, after blank line) ---
|
|
8149
|
-
if (/^(?: |\t)/.test(line) && (prevLineBlank || prevLineIndentedCode)) {
|
|
8150
|
-
result.push(maskLine(line));
|
|
8151
|
-
prevLineBlank = false;
|
|
8152
|
-
prevLineIndentedCode = true;
|
|
8153
|
-
continue;
|
|
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'
|
|
8182
|
+
});
|
|
8183
|
+
return {
|
|
8184
|
+
valid: false,
|
|
8185
|
+
errors,
|
|
8186
|
+
warnings: []
|
|
8187
|
+
};
|
|
8154
8188
|
}
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
prevLineIndentedCode = false;
|
|
8159
|
-
}
|
|
8160
|
-
return result.join('\n');
|
|
8161
|
-
}
|
|
8162
|
-
/** Replace all characters in a line with spaces (preserving length) */ function maskLine(line) {
|
|
8163
|
-
return ' '.repeat(line.length);
|
|
8164
|
-
}
|
|
8165
|
-
/**
|
|
8166
|
-
* Mask inline code (`` `...` ``) and double-quoted text (`"..."`) within a line.
|
|
8167
|
-
* Uses regex replacement for efficiency (avoids char-by-char concatenation on long lines).
|
|
8168
|
-
* Single quotes are NOT masked to avoid false matches with apostrophes.
|
|
8169
|
-
*/ function maskInline(line) {
|
|
8170
|
-
let result = line;
|
|
8171
|
-
// Inline code: `...`
|
|
8172
|
-
result = result.replace(/`[^`]+`/g, (m)=>' '.repeat(m.length));
|
|
8173
|
-
// Double-quoted text: "..." (min 3 chars between quotes)
|
|
8174
|
-
result = result.replace(/"[^"]{3,}"/g, (m)=>' '.repeat(m.length));
|
|
8175
|
-
return result;
|
|
8176
|
-
}
|
|
8177
|
-
// ============================================================================
|
|
8178
|
-
// Rule Helpers
|
|
8179
|
-
// ============================================================================
|
|
8180
|
-
/** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
|
|
8181
|
-
const lines = content.split('\n');
|
|
8182
|
-
const matches = [];
|
|
8183
|
-
for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
|
|
8184
|
-
matches.push({
|
|
8185
|
-
line: i + 1
|
|
8189
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) errors.push({
|
|
8190
|
+
field: 'description',
|
|
8191
|
+
message: `Description must be at most ${MAX_DESCRIPTION_LENGTH} characters`
|
|
8186
8192
|
});
|
|
8187
|
-
|
|
8193
|
+
// Note: angle brackets are allowed per agentskills.io spec
|
|
8194
|
+
return {
|
|
8195
|
+
valid: 0 === errors.length,
|
|
8196
|
+
errors,
|
|
8197
|
+
warnings: []
|
|
8198
|
+
};
|
|
8188
8199
|
}
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
/(?:you\s+are|you're)\s+(?:now\s+)?entering\s+(?:a\s+)?new\s+(?:mode|context|session)/i,
|
|
8212
|
-
// Chinese patterns (中文提示词注入)
|
|
8213
|
-
/[忽无][略视]\s*(所有\s*)?(之前的?|先前的?|以前的?)?\s*(指令|指示|规则|约束|限制)/,
|
|
8214
|
-
/你现在是/,
|
|
8215
|
-
/从现在开始.{0,10}你是/,
|
|
8216
|
-
/新的系统提示词/,
|
|
8217
|
-
/[覆改]写?\s*(你的|系统)\s*(提示词|规则|指令|安全)/,
|
|
8218
|
-
/忘记\s*(所有\s*)?(之前的?|先前的?)?\s*(指令|指示|规则|约束)/,
|
|
8219
|
-
/进入.{0,5}新的?\s*(模式|上下文|会话)/,
|
|
8220
|
-
/不要遵守.{0,10}(安全|限制|规则|约束)/,
|
|
8221
|
-
/解除.{0,5}(限制|约束|安全)/,
|
|
8222
|
-
/无限制模式/,
|
|
8223
|
-
/安全模式已关闭/
|
|
8224
|
-
])
|
|
8225
|
-
},
|
|
8226
|
-
// Rule 2: Data Exfiltration (high)
|
|
8227
|
-
{
|
|
8228
|
-
id: 'data-exfiltration',
|
|
8229
|
-
level: 'high',
|
|
8230
|
-
message: 'Detected potential data exfiltration command',
|
|
8231
|
-
skipSafeZones: true,
|
|
8232
|
-
check: (content)=>{
|
|
8233
|
-
const lines = content.split('\n');
|
|
8234
|
-
const matches = [];
|
|
8235
|
-
const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
|
|
8236
|
-
const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
|
|
8237
|
-
for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
|
|
8238
|
-
line: i + 1
|
|
8239
|
-
});
|
|
8240
|
-
return matches;
|
|
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
|
|
8241
8222
|
}
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
skipSafeZones: false,
|
|
8249
|
-
check: (content)=>{
|
|
8250
|
-
const matches = [];
|
|
8251
|
-
const lines = content.split('\n');
|
|
8252
|
-
// Zero-width characters (suspicious in any context)
|
|
8253
|
-
const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
|
|
8254
|
-
for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
|
|
8255
|
-
line: i + 1,
|
|
8256
|
-
snippet: 'Zero-width Unicode characters detected'
|
|
8257
|
-
});
|
|
8258
|
-
// Long base64-like strings (>200 continuous chars)
|
|
8259
|
-
const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
|
|
8260
|
-
for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
|
|
8261
|
-
line: i + 1,
|
|
8262
|
-
snippet: 'Suspicious base64-encoded block detected'
|
|
8263
|
-
});
|
|
8264
|
-
// Large HTML comments (>200 chars of content)
|
|
8265
|
-
const commentRegex = /<!--([\s\S]{200,}?)-->/g;
|
|
8266
|
-
let match;
|
|
8267
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
|
|
8268
|
-
while(null !== (match = commentRegex.exec(content))){
|
|
8269
|
-
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
8270
|
-
matches.push({
|
|
8271
|
-
line: lineNum,
|
|
8272
|
-
snippet: `Large HTML comment block (${match[1].length} chars)`
|
|
8273
|
-
});
|
|
8274
|
-
}
|
|
8275
|
-
return matches;
|
|
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);
|
|
8276
8229
|
}
|
|
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
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
/在用户不知情的情况下/,
|
|
8318
|
-
/瞒着用户/
|
|
8319
|
-
];
|
|
8320
|
-
// Safe patterns to exclude (common in legitimate DevOps/automation skills)
|
|
8321
|
-
const safePatterns = [
|
|
8322
|
-
/silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i,
|
|
8323
|
-
// Chinese safe patterns (中文合法自动化用语)
|
|
8324
|
-
/悄悄地?\s*(?:忽略|跳过|丢弃|抑制|继续|静默)/
|
|
8325
|
-
];
|
|
8326
|
-
const lines = content.split('\n');
|
|
8327
|
-
const matches = [];
|
|
8328
|
-
for(let i = 0; i < lines.length; i++){
|
|
8329
|
-
const line = lines[i];
|
|
8330
|
-
if (!safePatterns.some((p)=>p.test(line))) {
|
|
8331
|
-
for (const pattern of patterns)if (pattern.test(line)) {
|
|
8332
|
-
matches.push({
|
|
8333
|
-
line: i + 1
|
|
8334
|
-
});
|
|
8335
|
-
break;
|
|
8336
|
-
}
|
|
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);
|
|
8337
8270
|
}
|
|
8338
8271
|
}
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
const sizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
8351
|
-
if (sizeBytes > MAX_SIZE_BYTES) return [
|
|
8352
|
-
{
|
|
8353
|
-
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
|
+
}
|
|
8354
8283
|
}
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
];
|
|
8360
|
-
// ============================================================================
|
|
8361
|
-
// ContentScanner
|
|
8362
|
-
// ============================================================================
|
|
8363
|
-
/** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
|
|
8364
|
-
let rules = DEFAULT_RULES.map((r)=>({
|
|
8365
|
-
...r
|
|
8366
|
-
}));
|
|
8367
|
-
if (options?.disabledRules?.length) {
|
|
8368
|
-
const disabled = new Set(options.disabledRules);
|
|
8369
|
-
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;
|
|
8370
8288
|
}
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
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;
|
|
8374
8314
|
}
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
* overrides: { 'prompt-injection': 'medium' },
|
|
8393
|
-
* disabledRules: ['stealth-instructions'],
|
|
8394
|
-
* });
|
|
8395
|
-
* ```
|
|
8396
|
-
*/ class ContentScanner {
|
|
8397
|
-
rules;
|
|
8398
|
-
constructor(options){
|
|
8399
|
-
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
|
+
}
|
|
8400
8332
|
}
|
|
8401
8333
|
/**
|
|
8402
|
-
*
|
|
8403
|
-
*
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
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---'
|
|
8420
8365
|
});
|
|
8366
|
+
return {
|
|
8367
|
+
valid: false,
|
|
8368
|
+
errors,
|
|
8369
|
+
warnings
|
|
8370
|
+
};
|
|
8421
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
|
+
};
|
|
8422
8383
|
}
|
|
8423
|
-
|
|
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
|
+
});
|
|
8424
8413
|
return {
|
|
8425
|
-
|
|
8426
|
-
|
|
8414
|
+
valid: 0 === errors.length,
|
|
8415
|
+
errors,
|
|
8416
|
+
warnings
|
|
8427
8417
|
};
|
|
8428
8418
|
}
|
|
8429
8419
|
/**
|
|
8430
|
-
*
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
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')}`;
|
|
8435
8436
|
}
|
|
8436
8437
|
}
|
|
8437
8438
|
/**
|
|
@@ -8796,7 +8797,7 @@ async function publishAction(skillPath, options) {
|
|
|
8796
8797
|
const validation = validator.validate(absolutePath);
|
|
8797
8798
|
// 3.5. Content security scan
|
|
8798
8799
|
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(absolutePath, 'SKILL.md');
|
|
8799
|
-
if (
|
|
8800
|
+
if (exists(skillMdPath)) {
|
|
8800
8801
|
const scanner = new ContentScanner();
|
|
8801
8802
|
const scanResult = scanner.scanFile(skillMdPath);
|
|
8802
8803
|
displayScanFindings(scanResult);
|