Skip to content

Commit 40eded8

Browse files
committed
Allow arbitrary resource subdirectories per AgentSkills.io spec
The spec allows any additional files and directories, not just references/, scripts/, and assets/. Replace the hardcoded allowlist in ResourcePath with a check that the path is in any subdirectory. Safety rules remain: no path traversal, no absolute paths, no bare filenames at the root level.
1 parent e19826c commit 40eded8

3 files changed

Lines changed: 8 additions & 10 deletions

File tree

src/SkillServer/Endpoints.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ private static async Task<IResult> UploadSkill(
239239
return Results.BadRequest(new ErrorResponse
240240
{
241241
Error = "invalid_resource_path",
242-
Message = $"Invalid resource path: '{resourceFile.FileName}'. Must be in references/, scripts/, or assets/ directories."
242+
Message = $"Invalid resource path: '{resourceFile.FileName}'. Must be a relative path in a subdirectory with no path traversal."
243243
});
244244
}
245245

src/SkillServer/Models/ValueObjects.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,6 @@ public static Sha256Digest FromHex(string hexValue)
144144
/// </summary>
145145
public readonly record struct ResourcePath
146146
{
147-
private static readonly string[] AllowedPrefixes = ["references/", "scripts/", "assets/"];
148-
149147
public string Value { get; }
150148

151149
private ResourcePath(string value) => Value = value;
@@ -157,15 +155,13 @@ public static bool TryCreate(string? value, [NotNullWhen(true)] out ResourcePath
157155
if (string.IsNullOrWhiteSpace(value))
158156
return false;
159157

160-
// No path traversal
161158
if (value.Contains("..") || value.StartsWith('/') || value.StartsWith('\\'))
162159
return false;
163160

164-
// Normalize separators
165161
var normalized = value.Replace('\\', '/');
166162

167-
// Must be in allowed directories
168-
if (!AllowedPrefixes.Any(p => normalized.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
163+
// Must be in a subdirectory — bare filenames at the root are not resources
164+
if (!normalized.Contains('/') || normalized.IndexOf('/') == normalized.Length - 1)
169165
return false;
170166

171167
result = new ResourcePath(normalized);
@@ -175,7 +171,7 @@ public static bool TryCreate(string? value, [NotNullWhen(true)] out ResourcePath
175171
public static ResourcePath Create(string value)
176172
{
177173
if (!TryCreate(value, out var result))
178-
throw new ArgumentException($"Invalid resource path: '{value}'. Must be in references/, scripts/, or assets/ directories with no path traversal.", nameof(value));
174+
throw new ArgumentException($"Invalid resource path: '{value}'. Must be a relative path in a subdirectory with no path traversal.", nameof(value));
179175
return result.Value;
180176
}
181177

tests/SkillServer.Tests/ValueObjectTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,12 @@ public sealed class ResourcePathTests
115115
[InlineData("scripts/setup.sh", true)]
116116
[InlineData("assets/logo.png", true)]
117117
[InlineData("references/deep/nested/file.md", true)]
118+
[InlineData("custom/file.txt", true)]
119+
[InlineData("examples/demo.py", true)]
118120
[InlineData("../secret.txt", false)]
119121
[InlineData("/etc/passwd", false)]
120-
[InlineData("SKILL.md", false)] // Must be in allowed directories
121-
[InlineData("random/file.txt", false)]
122+
[InlineData("SKILL.md", false)] // Bare filenames are not resources
123+
[InlineData("justadirectory/", false)]
122124
[InlineData("", false)]
123125
public void TryCreate_ValidatesCorrectly(string input, bool expectedValid)
124126
{

0 commit comments

Comments
 (0)