fix: accept a path-less skill root URI in normalizeSkillUri (#2295)

`skill://data-analysis` has an authority and an empty path, which RFC 3986
permits, but the guard required a pathname starting with "/". A server echoing
the skill root back in a directory read was therefore labelled "outside this
skill" instead of "not a direct child". Opaque URIs (`skill:demo`) stay
rejected; a path-less root still names no skill file, so it fails every check
that needs one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ2eszDCUqTmhtTMaznw3W
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-14 00:00:49 -04:00
commit 528a37be38

View file

@ -171,6 +171,16 @@ export function isSkillsExtensionSupported(
* rejected too: containment could not be decided on it, and silently accepting
* one would reintroduce exactly the hole this function closes.
*
* A **path-less authority** URI (`skill://data-analysis`) is accepted, and is
* not the opaque case above: RFC 3986 gives a URI with an authority an empty
* path, and the parser reports that as `pathname === ""`. It has no segments,
* so nothing survives unnormalized. It is what a skill's root directory looks
* like once `/SKILL.md` is removed, so rejecting it made a server echoing that
* root back in a directory read read as "outside this skill" when it is the
* skill itself (#2295). It names no file, so it still fails every check that
* needs one — `skillNameFromUri` wants the `/SKILL.md` suffix, and a manifest
* entry spelled this way does not start with `<root>/`.
*
* The scheme is deliberately **not** constrained. `skill://` is what SEP-2640
* recommends and what this repo's fixture serves, but the SEP only says servers
* SHOULD use it and explicitly allows a domain-native scheme (`github://…`), so
@ -186,7 +196,9 @@ export function normalizeSkillUri(uri: string): string | undefined {
} catch {
return undefined;
}
if (!parsed.pathname.startsWith("/")) return undefined;
if (parsed.pathname !== "" && !parsed.pathname.startsWith("/")) {
return undefined;
}
return canonicalizePercentEncoding(parsed.href);
}