mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-09 02:08:19 +01:00
fix(context-bill): a root named '..x' no longer lets a parent link climb out
encloses() tested rel.startsWith('..'), which also matches a first
segment such as '..skills', so a link to the parent of a root under such
a name was followed out of the root. Compare the '..' segment exactly.
Adds that case plus a loop to a directory below the walk root, which only
the ancestor guard stops (found by /ship pre-landing review).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
9f16710851
commit
acb59268a7
2 files changed
+18
-1
No files matched your search
+1
-1
@@ -221,7 +221,7 @@ function entryKind(p: string, e: fs.Dirent): "file" | "dir" | null {
|
||||
*/
|
||||
function encloses(outer: string, inner: string): boolean {
|
||||
const rel = path.relative(outer, inner);
|
||||
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
||||
return rel === "" || (rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -299,6 +299,8 @@ describe("upstream fix b: installed-tree layout (repo-checkout subdir skip)", ()
|
||||
fs.symlinkSync(tmp, path.join(root, "parent"));
|
||||
fs.symlinkSync(path.join(root, "qa"), path.join(root, "qa", "references", "loop"));
|
||||
fs.symlinkSync(tmp, path.join(root, "qa", "references", "up"));
|
||||
// A loop to a directory below the walk root: only the ancestor guard stops it.
|
||||
fs.symlinkSync(path.join(root, "qa", "references"), path.join(root, "qa", "references", "again"));
|
||||
|
||||
expect(findSkillDirs(root).map((d) => path.relative(root, d))).toEqual(["qa"]);
|
||||
expect(walkMd(path.join(root, "qa")).map((f) => path.relative(root, f)).sort()).toEqual([
|
||||
@@ -310,6 +312,21 @@ describe("upstream fix b: installed-tree layout (repo-checkout subdir skip)", ()
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("refuses a parent link when the root's own name starts with '..'", () => {
|
||||
if (process.platform === "win32") return;
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "context-bill-dotdot-"));
|
||||
fs.mkdirSync(path.join(tmp, "outside"));
|
||||
fs.writeFileSync(path.join(tmp, "outside", "SKILL.md"), "---\nname: outside\ndescription: d\n---\n# o\n");
|
||||
const root = path.join(tmp, "..skills");
|
||||
fs.mkdirSync(path.join(root, "qa"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "qa", "SKILL.md"), "---\nname: qa\ndescription: d\n---\n# qa\n");
|
||||
fs.symlinkSync(tmp, path.join(root, "up"));
|
||||
|
||||
expect(findSkillDirs(root).map((d) => path.relative(root, d))).toEqual(["qa"]);
|
||||
expect(walkMd(root).map((f) => path.relative(root, f))).toEqual([path.join("qa", "SKILL.md")]);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("prefers ./skills, then .agents/skills, then .claude/skills, project before user", async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "context-bill-detect-"));
|
||||
const home = path.join(tmp, "home");
|
||||
|
||||
Reference in new issue
Block a user