Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions actions/setup/js/runtime_import.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,17 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start
let filepath = filepathOrUrl;
let isAgentsPath = false;

// Strip leading "/" or "//" (and any number of slashes) for repo-root-absolute paths
// (e.g. /.agents/skills/..., //.github/agents/...).
// After stripping, the existing .agents/ and .github/ prefix checks handle resolution correctly.
// Only strip when the result begins with .agents/ or .github/ to preserve security restrictions.
if (filepath.startsWith("/")) {
const stripped = filepath.replace(/^\/+/, "");
if (stripped.startsWith(".agents/") || stripped.startsWith(".agents\\") || stripped.startsWith(".github/") || stripped.startsWith(".github\\")) {
filepath = stripped;
}
}

// Check if this is a .agents/ path (top-level folder for skills)
if (filepath.startsWith(".agents/")) {
isAgentsPath = true;
Expand Down
48 changes: 48 additions & 0 deletions actions/setup/js/runtime_import.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,54 @@
const result = await processRuntimeImport(".agents/test-skill.md", !1, tempDir);
expect(result).toBe(content);
}),
it("should support /.agents/ prefix (leading slash, repo-root-absolute)", async () => {
// Regression test: /.agents/skills/my-skill/instructions.md should resolve to
// <workspace>/.agents/skills/my-skill/instructions.md (not .github/workflows/.agents/...)
const skillsDir = path.join(tempDir, ".agents", "skills", "my-skill");
fs.mkdirSync(skillsDir, { recursive: true });
const content = "# My Skill\n\nThis is the skill content.";
fs.writeFileSync(path.join(skillsDir, "instructions.md"), content);
const result = await processRuntimeImport("/.agents/skills/my-skill/instructions.md", !1, tempDir);
expect(result).toBe(content);
}),
it("should support //.agents/ prefix (double leading slash, repo-root-absolute)", async () => {
// Regression test: //.agents/skills/my-skill/instructions.md (double slash) should also
// resolve to <workspace>/.agents/skills/my-skill/instructions.md
const skillsDir = path.join(tempDir, ".agents", "skills", "my-skill");
fs.mkdirSync(skillsDir, { recursive: true });
const content = "# My Skill (double slash)\n\nThis is the skill content.";
fs.writeFileSync(path.join(skillsDir, "instructions.md"), content);
const result = await processRuntimeImport("//.agents/skills/my-skill/instructions.md", !1, tempDir);
expect(result).toBe(content);
}),
it("should support /.github/ prefix (leading slash, repo-root-absolute)", async () => {
// Regression test: /.github/agents/planner.md should resolve to
// <workspace>/.github/agents/planner.md (not .github/workflows/.github/agents/...)
const agentsDir = path.join(tempDir, ".github", "agents");
fs.mkdirSync(agentsDir, { recursive: true });
const content = "# Planner Agent\n\nThis is the planner content.";
fs.writeFileSync(path.join(agentsDir, "planner.md"), content);
const result = await processRuntimeImport("/.github/agents/planner.md", !1, tempDir);
expect(result).toBe(content);
}),
it("should support //.github/ prefix (double leading slash, repo-root-absolute)", async () => {
// Regression test: //.github/agents/planner.md (double slash) should also resolve to
// <workspace>/.github/agents/planner.md
const agentsDir = path.join(tempDir, ".github", "agents");
fs.mkdirSync(agentsDir, { recursive: true });
const content = "# Planner Agent (double slash)\n\nThis is the planner content.";
fs.writeFileSync(path.join(agentsDir, "planner.md"), content);
const result = await processRuntimeImport("//.github/agents/planner.md", !1, tempDir);
expect(result).toBe(content);
}),
it("should reject /-prefixed paths not under .agents/ or .github/", async () => {
// A leading "/" that does not map to .agents/ or .github/ should NOT be stripped.
// It falls through to the default branch, and path joining preserves the absolute path
// (/etc/passwd on POSIX), which is then rejected by the .github base-folder security check.
// Assert the security rejection rather than a file-not-found error so the test remains
// stable across platforms.
await expect(processRuntimeImport("/etc/passwd", !1, tempDir)).rejects.toThrow("Security: Path");

Check failure on line 515 in actions/setup/js/runtime_import.test.cjs

View workflow job for this annotation

GitHub Actions / js

runtime_import.test.cjs > runtime_import > processRuntimeImport > should reject /-prefixed paths not under .agents/ or .github/

AssertionError: expected [Function] to throw error including 'Security: Path' but got 'ERR_SYSTEM: Runtime import file not f…' Expected: "Security: Path" Received: "ERR_SYSTEM: Runtime import file not found: /tmp/runtime-import-test-f7tV5z/.github/workflows/etc/passwd" ❯ runtime_import.test.cjs:515:73
}),
it("should support nested .github/workflows/shared/ path (issue: runtime-import fails for .github/workflows/* paths)", async () => {
// Regression test: .github/workflows/shared/reporting.md should resolve to
// <workspace>/.github/workflows/shared/reporting.md (not <workspace>/workflows/shared/reporting.md)
Expand Down
Loading