( skill: Skill, llmContext: LlmCallContext | null, logger: LoggerLike, options: Required<FoundationScanConfig>, )
| 780 | |
| 781 | |
| 782 | async function scanSkillDirectory( |
| 783 | skill: Skill, |
| 784 | llmContext: LlmCallContext | null, |
| 785 | logger: LoggerLike, |
| 786 | options: Required<FoundationScanConfig>, |
| 787 | ): Promise<Warning | null> { |
| 788 | if (!skill.filePath) { |
| 789 | logger?.warn(`[FoundationScan] skip skill without filePath: ${skill.name ?? "(unknown)"}`); |
| 790 | return null; |
| 791 | } |
| 792 | |
| 793 | const baseDir = skill.baseDir || path.dirname(skill.filePath); |
| 794 | const textFiles = listTextFilesRecursively(baseDir); |
| 795 | logger?.info(`[FoundationScan] scanning skill=${skill.name ?? "(unknown)"}, files=${textFiles.length}`); |
| 796 | |
| 797 | const { metadataSummary, warning: metadataWarning } = (options.detectMaliciousSkills.semanticDetection && llmContext) |
| 798 | ? await scanSkillMetadata(skill, llmContext, logger) |
| 799 | : { metadataSummary: "", warning: null as Warning | null }; |
| 800 | |
| 801 | if (metadataWarning) { |
| 802 | return metadataWarning; |
| 803 | } |
| 804 | |
| 805 | for (const filePath of textFiles) { |
| 806 | if (options.detectMaliciousSkills.semanticDetection && llmContext) { |
| 807 | const warning = await scanSkillFile(skill, filePath, metadataSummary, llmContext, logger, options); |
| 808 | if (warning) { |
| 809 | return warning; |
| 810 | } |
| 811 | } |
| 812 | } |
| 813 | |
| 814 | return null; |
| 815 | } |
| 816 | |
| 817 | export async function detectMaliciousSkills( |
| 818 | workspaceDir: string, |
no test coverage detected