Files
multica/server/internal/skill/reserved.go
Bohan Jiang 44feb3d06d fix(skill): canonicalize reserved SKILL.md path check across daemon + API (#3660)
A skill_file row whose path is the skill's own SKILL.md (persisted by
older builds or direct create/update API calls) collides with the
primary content the daemon writes itself, failing task prep with
errPathPreExists on every non-codex local runtime (#3489).

#3526 guarded this with strings.EqualFold(path, "SKILL.md") at the
daemon write site and the three API ingress points, but the stored path
is not canonicalized: "./SKILL.md" or "sub/../SKILL.md" slip past the
exact-match guard while filepath.Join still resolves them onto the same
SKILL.md, so prep can still break.

Extract one canonical helper, skill.IsReservedContentPath, that cleans
the path before the case-insensitive compare, and use it at all four
sites (execenv writeSkillFiles, skill create, update, single-file
upsert). Add a daemon-side regression test for writeSkillFiles ignoring
a bundled SKILL.md (exact + "./" spellings) — the load-bearing fix
previously had only API-layer coverage — plus a unit test for the helper.

Existing poisoned rows are intentionally left in place (skipped at prep)
per the decision on MUL-2928.

MUL-2928
Follow-up to #3526; supersedes #3560.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 18:23:57 +08:00

28 lines
1.1 KiB
Go

package skill
import (
"path/filepath"
"strings"
)
// ContentFilename is the canonical filename of a skill's primary content
// (the Content field). The daemon writes Content to this path itself when
// preparing the execution environment, so it is reserved: a supporting file
// may not also claim it.
const ContentFilename = "SKILL.md"
// IsReservedContentPath reports whether p targets the reserved primary
// content file (SKILL.md).
//
// The path is cleaned before comparison so non-canonical spellings like
// "./SKILL.md" or "sub/../SKILL.md" — which filepath.Join still resolves onto
// the very SKILL.md the daemon writes itself — are caught too. An exact
// string match would let them slip through both the API guards and the daemon
// guard, and the duplicate write would then fail task prep with
// errPathPreExists (or, on the nil-manifest path, clobber the primary
// content). Comparison is case-insensitive to match the rest of the SKILL.md
// handling in this package and the daemon.
func IsReservedContentPath(p string) bool {
return strings.EqualFold(filepath.Clean(p), ContentFilename)
}