From dca466f13221367ebb9b13194af0d2b4450d4fde Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Wed, 5 Aug 2026 15:07:45 -0500 Subject: [PATCH] github-copilot-cli: avoid IFD for path inputs Validate derived agent and skill directories during builds and normalize derived skill files without evaluation-time filesystem reads. --- modules/programs/github-copilot-cli.nix | 43 ++++++++++++++++--- .../programs/github-copilot-cli/default.nix | 2 +- .../github-copilot-cli/store-path-dir.nix | 22 +++------- .../store-path-not-directory.nix | 28 ------------ .../github-copilot-cli/store-path-skills.nix | 24 +++++++++++ 5 files changed, 70 insertions(+), 49 deletions(-) delete mode 100644 tests/modules/programs/github-copilot-cli/store-path-not-directory.nix create mode 100644 tests/modules/programs/github-copilot-cli/store-path-skills.nix diff --git a/modules/programs/github-copilot-cli.nix b/modules/programs/github-copilot-cli.nix index 783369251a..a2f0785fb0 100644 --- a/modules/programs/github-copilot-cli.nix +++ b/modules/programs/github-copilot-cli.nix @@ -73,6 +73,34 @@ let } ) ) cfg.mcpServers; + + normalizeDirectory = + name: option: source: + if lib.isPath source then + source + else + pkgs.runCommandLocal name { } '' + if [[ ! -d ${lib.escapeShellArg (toString source)} ]]; then + echo ${lib.escapeShellArg "programs.github-copilot-cli.${option} must be a directory"} >&2 + exit 1 + fi + ln -s ${lib.escapeShellArg (toString source)} "$out" + ''; + + normalizeSkill = + source: + pkgs.runCommandLocal "github-copilot-cli-skill" { } '' + source=${lib.escapeShellArg (toString source)} + if [[ -d "$source" ]]; then + ln -s "$source" "$out" + elif [[ -f "$source" ]]; then + mkdir "$out" + ln -s "$source" "$out/SKILL.md" + else + echo "GitHub Copilot CLI skill source must be a file or directory: $source" >&2 + exit 1 + fi + ''; in { meta.maintainers = [ lib.maintainers.ojsef39 ]; @@ -365,11 +393,11 @@ in config = mkIf cfg.enable { assertions = [ { - assertion = !lib.hm.strings.isPathLike cfg.agents || lib.pathIsDirectory cfg.agents; + assertion = !lib.isPath cfg.agents || lib.pathIsDirectory cfg.agents; message = "`programs.github-copilot-cli.agents` must be a directory when set to a path"; } { - assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills; + assertion = !lib.isPath cfg.skills || lib.pathIsDirectory cfg.skills; message = "`programs.github-copilot-cli.skills` must be a directory when set to a path"; } ]; @@ -408,12 +436,12 @@ in }; "${cfg.configDir}/agents" = mkIf (lib.hm.strings.isPathLike cfg.agents) { - source = cfg.agents; + source = normalizeDirectory "github-copilot-cli-agents" "agents" cfg.agents; recursive = true; }; "${cfg.configDir}/skills" = mkIf (lib.hm.strings.isPathLike cfg.skills) { - source = cfg.skills; + source = normalizeDirectory "github-copilot-cli-skills" "skills" cfg.skills; recursive = true; }; } @@ -428,11 +456,16 @@ in // lib.optionalAttrs (builtins.isAttrs cfg.skills) ( lib.mapAttrs' ( name: content: - if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then + if lib.isPath content && lib.pathIsDirectory content then lib.nameValuePair "${cfg.configDir}/skills/${name}" { source = content; recursive = true; } + else if lib.hm.strings.isPathLike content && !lib.isPath content then + lib.nameValuePair "${cfg.configDir}/skills/${name}" { + source = normalizeSkill content; + recursive = true; + } else lib.nameValuePair "${cfg.configDir}/skills/${name}/SKILL.md" ( if lib.hm.strings.isPathLike content then { source = content; } else { text = content; } diff --git a/tests/modules/programs/github-copilot-cli/default.nix b/tests/modules/programs/github-copilot-cli/default.nix index 569b4701f6..a507d5fc24 100644 --- a/tests/modules/programs/github-copilot-cli/default.nix +++ b/tests/modules/programs/github-copilot-cli/default.nix @@ -6,6 +6,6 @@ github-copilot-cli-mcp-integration = ./mcp-integration.nix; github-copilot-cli-path-not-directory = ./path-not-directory.nix; github-copilot-cli-store-path-dir = ./store-path-dir.nix; - github-copilot-cli-store-path-not-directory = ./store-path-not-directory.nix; + github-copilot-cli-store-path-skills = ./store-path-skills.nix; github-copilot-cli-xdg-config-dir = ./xdg-config-dir.nix; } diff --git a/tests/modules/programs/github-copilot-cli/store-path-dir.nix b/tests/modules/programs/github-copilot-cli/store-path-dir.nix index 2af79f2187..60bf89c971 100644 --- a/tests/modules/programs/github-copilot-cli/store-path-dir.nix +++ b/tests/modules/programs/github-copilot-cli/store-path-dir.nix @@ -1,21 +1,13 @@ -{ pkgs, ... }: +{ realPkgs, ... }: let - src = pkgs.writeTextDir "agents/code-reviewer.agent.md" '' - --- - description: Review changes from a store-path directory fixture. - tools: ["*"] - --- - - Focus on correctness and missing coverage. + src = realPkgs.runCommand "github-copilot-cli-ifd-agents-directory" { } '' + mkdir -p "$out/agents" + echo '# Code Reviewer' > "$out/agents/code-reviewer.agent.md" ''; - skillsSrc = pkgs.writeTextDir "skills/external-skill/SKILL.md" '' - --- - name: external-skill - description: Store-path directory fixture. - --- - - Exercise top-level store-path directory handling. + skillsSrc = realPkgs.runCommand "github-copilot-cli-ifd-skills-directory" { } '' + mkdir -p "$out/skills/external-skill" + echo '# External Skill' > "$out/skills/external-skill/SKILL.md" ''; in { diff --git a/tests/modules/programs/github-copilot-cli/store-path-not-directory.nix b/tests/modules/programs/github-copilot-cli/store-path-not-directory.nix deleted file mode 100644 index 83ed01e333..0000000000 --- a/tests/modules/programs/github-copilot-cli/store-path-not-directory.nix +++ /dev/null @@ -1,28 +0,0 @@ -{ pkgs, ... }: -let - agentFile = pkgs.writeText "code-reviewer.agent.md" '' - --- - description: Invalid top-level agent file fixture. - tools: ["*"] - --- - ''; - - skillFile = pkgs.writeText "SKILL.md" '' - --- - name: invalid-top-level-skill - description: Invalid top-level skill file fixture. - --- - ''; -in -{ - programs.github-copilot-cli = { - enable = true; - agents = "${agentFile}"; - skills = "${skillFile}"; - }; - - test.asserts.assertions.expected = [ - "`programs.github-copilot-cli.agents` must be a directory when set to a path" - "`programs.github-copilot-cli.skills` must be a directory when set to a path" - ]; -} diff --git a/tests/modules/programs/github-copilot-cli/store-path-skills.nix b/tests/modules/programs/github-copilot-cli/store-path-skills.nix new file mode 100644 index 0000000000..c37952402c --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/store-path-skills.nix @@ -0,0 +1,24 @@ +{ realPkgs, ... }: + +let + src = realPkgs.runCommand "github-copilot-cli-ifd-skill-source" { } '' + mkdir -p "$out/skills/external-skill" + echo '# External Skill' > "$out/skills/external-skill/SKILL.md" + ''; +in +{ + programs.github-copilot-cli = { + enable = true; + skills = { + directory = "${src}/skills/external-skill"; + file = "${src}/skills/external-skill/SKILL.md"; + }; + }; + + nmt.script = '' + assertFileContent home-files/.copilot/skills/directory/SKILL.md \ + "${src}/skills/external-skill/SKILL.md" + assertFileContent home-files/.copilot/skills/file/SKILL.md \ + "${src}/skills/external-skill/SKILL.md" + ''; +}