Skip to content

Commit 14d2ab3

Browse files
Standardize file skills terminology on 'directory' (#5205)
Rename authored identifiers, XML docs, log messages, and comments from 'folder' to 'directory' across the file skills codebase for consistency with the agentskills.io specification and .NET conventions. Public API changes (experimental): - ScriptFolders → ScriptDirectories - ResourceFolders → ResourceDirectories .NET BCL API calls (Directory.Exists, Path.GetDirectoryName, etc.) were already using 'directory' and are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e5f7b9c commit 14d2ab3

4 files changed

Lines changed: 141 additions & 141 deletions

File tree

dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs

Lines changed: 54 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
3232
private const string SkillFileName = "SKILL.md";
3333
private const int MaxSearchDepth = 2;
3434

35-
// "." means the skill directory root itself (no sub-folder descent constraint)
36-
private const string RootFolderIndicator = ".";
35+
// "." means the skill directory root itself (no subdirectory descent constraint)
36+
private const string RootDirectoryIndicator = ".";
3737

3838
private static readonly string[] s_defaultScriptExtensions = [".py", ".js", ".sh", ".ps1", ".cs", ".csx"];
3939
private static readonly string[] s_defaultResourceExtensions = [".md", ".json", ".yaml", ".yml", ".csv", ".xml", ".txt"];
4040

41-
// Standard sub-folder names per https://agentskills.io/specification#directory-structure
42-
private static readonly string[] s_defaultScriptFolders = ["scripts"];
43-
private static readonly string[] s_defaultResourceFolders = ["references", "assets"];
41+
// Standard subdirectory names per https://agentskills.io/specification#directory-structure
42+
private static readonly string[] s_defaultScriptDirectories = ["scripts"];
43+
private static readonly string[] s_defaultResourceDirectories = ["references", "assets"];
4444

4545
// Matches YAML frontmatter delimited by "---" lines. Group 1 = content between delimiters.
4646
// Multiline makes ^/$ match line boundaries; Singleline makes . match newlines across the block.
@@ -63,8 +63,8 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
6363
private readonly IEnumerable<string> _skillPaths;
6464
private readonly HashSet<string> _allowedResourceExtensions;
6565
private readonly HashSet<string> _allowedScriptExtensions;
66-
private readonly IReadOnlyList<string> _scriptFolders;
67-
private readonly IReadOnlyList<string> _resourceFolders;
66+
private readonly IReadOnlyList<string> _scriptDirectories;
67+
private readonly IReadOnlyList<string> _resourceDirectories;
6868
private readonly AgentFileSkillScriptRunner? _scriptRunner;
6969
private readonly ILogger _logger;
7070

@@ -111,13 +111,13 @@ public AgentFileSkillsSource(
111111
options?.AllowedScriptExtensions ?? s_defaultScriptExtensions,
112112
StringComparer.OrdinalIgnoreCase);
113113

114-
this._scriptFolders = options?.ScriptFolders is not null
115-
? [.. ValidateAndNormalizeFolderNames(options.ScriptFolders, this._logger)]
116-
: s_defaultScriptFolders;
114+
this._scriptDirectories = options?.ScriptDirectories is not null
115+
? [.. ValidateAndNormalizeDirectoryNames(options.ScriptDirectories, this._logger)]
116+
: s_defaultScriptDirectories;
117117

118-
this._resourceFolders = options?.ResourceFolders is not null
119-
? [.. ValidateAndNormalizeFolderNames(options.ResourceFolders, this._logger)]
120-
: s_defaultResourceFolders;
118+
this._resourceDirectories = options?.ResourceDirectories is not null
119+
? [.. ValidateAndNormalizeDirectoryNames(options.ResourceDirectories, this._logger)]
120+
: s_defaultResourceDirectories;
121121

122122
this._scriptRunner = scriptRunner;
123123
}
@@ -303,41 +303,41 @@ private bool TryParseFrontmatter(string content, string skillFilePath, [NotNullW
303303
}
304304

305305
/// <summary>
306-
/// Scans configured resource folders within a skill directory for resource files matching the configured extensions.
306+
/// Scans configured resource directories within a skill directory for resource files matching the configured extensions.
307307
/// </summary>
308308
/// <remarks>
309-
/// By default, scans <c>references/</c> and <c>assets/</c> sub-folders as specified by the
309+
/// By default, scans <c>references/</c> and <c>assets/</c> subdirectories as specified by the
310310
/// <see href="https://agentskills.io/specification">Agent Skills specification</see>.
311-
/// Configure <see cref="AgentFileSkillsSourceOptions.ResourceFolders"/> to scan different or
311+
/// Configure <see cref="AgentFileSkillsSourceOptions.ResourceDirectories"/> to scan different or
312312
/// additional directories, including <c>"."</c> for the skill root itself.
313313
/// Each file is validated against path-traversal and symlink-escape checks; unsafe files are skipped.
314314
/// </remarks>
315315
private List<AgentFileSkillResource> DiscoverResourceFiles(string skillDirectoryFullPath, string skillName)
316316
{
317317
var resources = new List<AgentFileSkillResource>();
318318

319-
foreach (string folder in this._resourceFolders.Distinct(StringComparer.OrdinalIgnoreCase))
319+
foreach (string directory in this._resourceDirectories.Distinct(StringComparer.OrdinalIgnoreCase))
320320
{
321-
bool isRootFolder = string.Equals(folder, RootFolderIndicator, StringComparison.Ordinal);
321+
bool isRootDirectory = string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal);
322322

323323
// GetFullPath normalizes mixed separators (e.g. "C:\skill\scripts/f1" → "C:\skill\scripts\f1")
324-
string targetDirectory = isRootFolder
324+
string targetDirectory = isRootDirectory
325325
? skillDirectoryFullPath
326-
: Path.GetFullPath(Path.Combine(skillDirectoryFullPath, folder)) + Path.DirectorySeparatorChar;
326+
: Path.GetFullPath(Path.Combine(skillDirectoryFullPath, directory)) + Path.DirectorySeparatorChar;
327327

328328
if (!Directory.Exists(targetDirectory))
329329
{
330330
continue;
331331
}
332332

333333
// Directory-level symlink check: skip if targetDirectory (or any intermediate
334-
// segment) is a reparse point. The root folder is excluded — it's a caller-supplied
334+
// segment) is a reparse point. The root directory is excluded — it's a caller-supplied
335335
// trusted path, and the security boundary guards files within it, not the path itself.
336-
if (!isRootFolder && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath))
336+
if (!isRootDirectory && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath))
337337
{
338338
if (this._logger.IsEnabled(LogLevel.Warning))
339339
{
340-
LogResourceSymlinkFolder(this._logger, skillName, SanitizePathForLog(folder));
340+
LogResourceSymlinkDirectory(this._logger, skillName, SanitizePathForLog(directory));
341341
}
342342

343343
continue;
@@ -380,7 +380,7 @@ private List<AgentFileSkillResource> DiscoverResourceFiles(string skillDirectory
380380
// e.g. "references/../../../etc/shadow" → "/etc/shadow"
381381
string resolvedFilePath = Path.GetFullPath(filePath);
382382

383-
// Path containment: reject if the resolved path escapes the target folder.
383+
// Path containment: reject if the resolved path escapes the target directory.
384384
// e.g. "/etc/shadow".StartsWith("/skills/myskill/references/") → false → skip
385385
if (!resolvedFilePath.StartsWith(targetDirectory, StringComparison.OrdinalIgnoreCase))
386386
{
@@ -416,41 +416,41 @@ private List<AgentFileSkillResource> DiscoverResourceFiles(string skillDirectory
416416
}
417417

418418
/// <summary>
419-
/// Scans configured script folders within a skill directory for script files matching the configured extensions.
419+
/// Scans configured script directories within a skill directory for script files matching the configured extensions.
420420
/// </summary>
421421
/// <remarks>
422-
/// By default, scans the <c>scripts/</c> sub-folder as specified by the
422+
/// By default, scans the <c>scripts/</c> subdirectory as specified by the
423423
/// <see href="https://agentskills.io/specification">Agent Skills specification</see>.
424-
/// Configure <see cref="AgentFileSkillsSourceOptions.ScriptFolders"/> to scan different or
424+
/// Configure <see cref="AgentFileSkillsSourceOptions.ScriptDirectories"/> to scan different or
425425
/// additional directories, including <c>"."</c> for the skill root itself.
426426
/// Each file is validated against path-traversal and symlink-escape checks; unsafe files are skipped.
427427
/// </remarks>
428428
private List<AgentFileSkillScript> DiscoverScriptFiles(string skillDirectoryFullPath, string skillName)
429429
{
430430
var scripts = new List<AgentFileSkillScript>();
431431

432-
foreach (string folder in this._scriptFolders.Distinct(StringComparer.OrdinalIgnoreCase))
432+
foreach (string directory in this._scriptDirectories.Distinct(StringComparer.OrdinalIgnoreCase))
433433
{
434-
bool isRootFolder = string.Equals(folder, RootFolderIndicator, StringComparison.Ordinal);
434+
bool isRootDirectory = string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal);
435435

436436
// GetFullPath normalizes mixed separators (e.g. "C:\skill\scripts/f1" → "C:\skill\scripts\f1")
437-
string targetDirectory = isRootFolder
437+
string targetDirectory = isRootDirectory
438438
? skillDirectoryFullPath
439-
: Path.GetFullPath(Path.Combine(skillDirectoryFullPath, folder)) + Path.DirectorySeparatorChar;
439+
: Path.GetFullPath(Path.Combine(skillDirectoryFullPath, directory)) + Path.DirectorySeparatorChar;
440440

441441
if (!Directory.Exists(targetDirectory))
442442
{
443443
continue;
444444
}
445445

446446
// Directory-level symlink check: skip if targetDirectory (or any intermediate
447-
// segment) is a reparse point. The root folder is excluded — it's a caller-supplied
447+
// segment) is a reparse point. The root directory is excluded — it's a caller-supplied
448448
// trusted path, and the security boundary guards files within it, not the path itself.
449-
if (!isRootFolder && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath))
449+
if (!isRootDirectory && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath))
450450
{
451451
if (this._logger.IsEnabled(LogLevel.Warning))
452452
{
453-
LogScriptSymlinkFolder(this._logger, skillName, SanitizePathForLog(folder));
453+
LogScriptSymlinkDirectory(this._logger, skillName, SanitizePathForLog(directory));
454454
}
455455

456456
continue;
@@ -480,7 +480,7 @@ private List<AgentFileSkillScript> DiscoverScriptFiles(string skillDirectoryFull
480480
// e.g. "scripts/../../../etc/shadow" → "/etc/shadow"
481481
string resolvedFilePath = Path.GetFullPath(filePath);
482482

483-
// Path containment: reject if the resolved path escapes the target folder.
483+
// Path containment: reject if the resolved path escapes the target directory.
484484
// e.g. "/etc/shadow".StartsWith("/skills/myskill/scripts/") → false → skip
485485
if (!resolvedFilePath.StartsWith(targetDirectory, StringComparison.OrdinalIgnoreCase))
486486
{
@@ -541,8 +541,8 @@ private static bool HasSymlinkInPath(string pathToCheck, string trustedBasePath)
541541
}
542542

543543
/// <summary>
544-
/// Normalizes a relative path or folder name by stripping a leading "./"/".\",
545-
/// trimming trailing directory separators, and replacing backslashes with forward
544+
/// Normalizes a relative path or directory name by stripping a leading "./"/".\",
545+
/// trimming trailing separators, and replacing backslashes with forward
546546
/// slashes.
547547
/// </summary>
548548
private static string NormalizePath(string path)
@@ -602,36 +602,36 @@ private static void ValidateExtensions(IEnumerable<string>? extensions)
602602
}
603603
}
604604

605-
private static IEnumerable<string> ValidateAndNormalizeFolderNames(IEnumerable<string> folders, ILogger logger)
605+
private static IEnumerable<string> ValidateAndNormalizeDirectoryNames(IEnumerable<string> directories, ILogger logger)
606606
{
607-
foreach (string folder in folders)
607+
foreach (string directory in directories)
608608
{
609-
if (string.IsNullOrWhiteSpace(folder))
609+
if (string.IsNullOrWhiteSpace(directory))
610610
{
611-
throw new ArgumentException("Folder names must not be null or whitespace.", nameof(folders));
611+
throw new ArgumentException("Directory names must not be null or whitespace.", nameof(directories));
612612
}
613613

614614
// "." is valid — it means the skill root directory.
615-
if (string.Equals(folder, RootFolderIndicator, StringComparison.Ordinal))
615+
if (string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal))
616616
{
617-
yield return folder;
617+
yield return directory;
618618
continue;
619619
}
620620

621621
// Reject absolute paths and any path segments that escape upward.
622-
if (Path.IsPathRooted(folder) || ContainsParentTraversalSegment(folder))
622+
if (Path.IsPathRooted(directory) || ContainsParentTraversalSegment(directory))
623623
{
624-
LogFolderNameSkippedInvalid(logger, folder);
624+
LogDirectoryNameSkippedInvalid(logger, directory);
625625
continue;
626626
}
627627

628-
yield return NormalizePath(folder);
628+
yield return NormalizePath(directory);
629629
}
630630
}
631631

632-
private static bool ContainsParentTraversalSegment(string folder)
632+
private static bool ContainsParentTraversalSegment(string directory)
633633
{
634-
foreach (string segment in folder.Split('/', '\\'))
634+
foreach (string segment in directory.Split('/', '\\'))
635635
{
636636
if (segment == "..")
637637
{
@@ -666,8 +666,8 @@ private static bool ContainsParentTraversalSegment(string folder)
666666
[LoggerMessage(LogLevel.Warning, "Skipping resource in skill '{SkillName}': '{ResourcePath}' is a symlink that resolves outside the skill directory")]
667667
private static partial void LogResourceSymlinkEscape(ILogger logger, string skillName, string resourcePath);
668668

669-
[LoggerMessage(LogLevel.Warning, "Skipping resource folder '{FolderName}' in skill '{SkillName}': folder path contains a symlink")]
670-
private static partial void LogResourceSymlinkFolder(ILogger logger, string skillName, string folderName);
669+
[LoggerMessage(LogLevel.Warning, "Skipping resource directory '{DirectoryName}' in skill '{SkillName}': directory path contains a symlink")]
670+
private static partial void LogResourceSymlinkDirectory(ILogger logger, string skillName, string directoryName);
671671

672672
[LoggerMessage(LogLevel.Debug, "Skipping file '{FilePath}' in skill '{SkillName}': extension '{Extension}' is not in the allowed list")]
673673
private static partial void LogResourceSkippedExtension(ILogger logger, string skillName, string filePath, string extension);
@@ -678,9 +678,9 @@ private static bool ContainsParentTraversalSegment(string folder)
678678
[LoggerMessage(LogLevel.Warning, "Skipping script in skill '{SkillName}': '{ScriptPath}' is a symlink that resolves outside the skill directory")]
679679
private static partial void LogScriptSymlinkEscape(ILogger logger, string skillName, string scriptPath);
680680

681-
[LoggerMessage(LogLevel.Warning, "Skipping script folder '{FolderName}' in skill '{SkillName}': folder path contains a symlink")]
682-
private static partial void LogScriptSymlinkFolder(ILogger logger, string skillName, string folderName);
681+
[LoggerMessage(LogLevel.Warning, "Skipping script directory '{DirectoryName}' in skill '{SkillName}': directory path contains a symlink")]
682+
private static partial void LogScriptSymlinkDirectory(ILogger logger, string skillName, string directoryName);
683683

684-
[LoggerMessage(LogLevel.Warning, "Skipping invalid folder name '{FolderName}': must be a relative path with no '..' segments")]
685-
private static partial void LogFolderNameSkippedInvalid(ILogger logger, string folderName);
684+
[LoggerMessage(LogLevel.Warning, "Skipping invalid directory name '{DirectoryName}': must be a relative path with no '..' segments")]
685+
private static partial void LogDirectoryNameSkippedInvalid(ILogger logger, string directoryName);
686686
}

dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSourceOptions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public sealed class AgentFileSkillsSourceOptions
3232
public IEnumerable<string>? AllowedScriptExtensions { get; set; }
3333

3434
/// <summary>
35-
/// Gets or sets relative folder paths to scan for script files within each skill directory.
35+
/// Gets or sets relative directory paths to scan for script files within each skill directory.
3636
/// Values may be single-segment names (e.g., <c>"scripts"</c>) or multi-segment relative
3737
/// paths (e.g., <c>"sub/scripts"</c>). Use <c>"."</c> to include files directly at the
3838
/// skill root. Leading <c>"./"</c> prefixes, trailing separators, and backslashes are
@@ -42,10 +42,10 @@ public sealed class AgentFileSkillsSourceOptions
4242
/// <see href="https://agentskills.io/specification">Agent Skills specification</see>).
4343
/// When set, replaces the defaults entirely.
4444
/// </summary>
45-
public IEnumerable<string>? ScriptFolders { get; set; }
45+
public IEnumerable<string>? ScriptDirectories { get; set; }
4646

4747
/// <summary>
48-
/// Gets or sets relative folder paths to scan for resource files within each skill directory.
48+
/// Gets or sets relative directory paths to scan for resource files within each skill directory.
4949
/// Values may be single-segment names (e.g., <c>"references"</c>) or multi-segment relative
5050
/// paths (e.g., <c>"sub/resources"</c>). Use <c>"."</c> to include files directly at the
5151
/// skill root. Leading <c>"./"</c> prefixes, trailing separators, and backslashes are
@@ -55,5 +55,5 @@ public sealed class AgentFileSkillsSourceOptions
5555
/// <see href="https://agentskills.io/specification">Agent Skills specification</see>).
5656
/// When set, replaces the defaults entirely.
5757
/// </summary>
58-
public IEnumerable<string>? ResourceFolders { get; set; }
58+
public IEnumerable<string>? ResourceDirectories { get; set; }
5959
}

0 commit comments

Comments
 (0)