diff --git a/modules/programs/codex.nix b/modules/programs/codex.nix index 14662db1..c238c144 100644 --- a/modules/programs/codex.nix +++ b/modules/programs/codex.nix @@ -99,12 +99,14 @@ in If an attribute set is used, the attribute name becomes the skill directory name, and the value is either: - - Inline content as a string (creates {file}`//SKILL.md`) - - A path to a file (creates {file}`//SKILL.md`) - - A path to a directory (creates {file}`//` with all files) + - Inline content as a string (creates a generated skill directory at {file}`//`) + - A path to a file (creates a generated skill directory at {file}`//`) + - A path to a directory (symlinks {file}`//` to that directory) If a path is used, it is expected to contain one folder per skill name, each - containing a {file}`SKILL.md`. The directory is symlinked to {file}`/`. + containing a {file}`SKILL.md`. Each top-level skill entry is symlinked into + {file}`/`, leaving {file}`/` itself as a normal + directory so unmanaged skills can coexist. The skills target directory depends on Codex version: - {file}`~/.agents/skills` for Codex >= 0.94.0 @@ -145,6 +147,30 @@ in configFileName = if isTomlConfig then "config.toml" else "config.yaml"; skillsDir = if isAgentsSkillsSupported then ".agents/skills" else "${configDir}/skills"; + # TODO: Remove this workaround once Codex supports symlinked SKILL.md + # files again. Upstream only supports symlinking the containing skill + # directory today: https://github.com/openai/codex/issues/10470 + mkSkillDir = + content: + pkgs.writeTextDir "SKILL.md" (if lib.isPath content then builtins.readFile content else content); + skillSources = + if builtins.isAttrs cfg.skills then + cfg.skills + else if lib.isPath cfg.skills && lib.pathIsDirectory cfg.skills then + lib.mapAttrs (name: _type: cfg.skills + "/${name}") (builtins.readDir cfg.skills) + else + { }; + mkSkillEntry = + name: content: + if lib.isPath content && lib.pathIsDirectory content then + lib.nameValuePair "${skillsDir}/${name}" { + source = content; + } + else + lib.nameValuePair "${skillsDir}/${name}" { + source = mkSkillDir content; + }; + transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) ( lib.mapAttrs ( _name: server: @@ -189,23 +215,8 @@ in "${configDir}/AGENTS.md" = lib.mkIf (cfg.custom-instructions != "") { text = cfg.custom-instructions; }; - "${skillsDir}" = lib.mkIf (lib.isPath cfg.skills) { - source = cfg.skills; - recursive = true; - }; } - // (lib.mapAttrs' ( - name: content: - if lib.isPath content && lib.pathIsDirectory content then - lib.nameValuePair "${skillsDir}/${name}" { - source = content; - recursive = true; - } - else - lib.nameValuePair "${skillsDir}/${name}/SKILL.md" ( - if lib.isPath content then { source = content; } else { text = content; } - ) - ) (if builtins.isAttrs cfg.skills then cfg.skills else { })); + // lib.mapAttrs' mkSkillEntry skillSources; sessionVariables = mkIf useXdgDirectories { CODEX_HOME = "${config.xdg.configHome}/codex"; diff --git a/tests/modules/programs/codex/skills-dir.nix b/tests/modules/programs/codex/skills-dir.nix index eaa48d53..27dca73e 100644 --- a/tests/modules/programs/codex/skills-dir.nix +++ b/tests/modules/programs/codex/skills-dir.nix @@ -13,7 +13,14 @@ in }; nmt.script = '' + if [[ -L home-files/.agents/skills ]]; then + fail "Expected home-files/.agents/skills to remain a normal directory so unmanaged skills can coexist." + fi + assertLinkExists home-files/.agents/skills/skill-one assertFileExists home-files/.agents/skills/skill-one/SKILL.md + if [[ -L home-files/.agents/skills/skill-one/SKILL.md ]]; then + fail "Expected home-files/.agents/skills/skill-one/SKILL.md to be a regular file inside a symlinked skill directory." + fi assertFileContent home-files/.agents/skills/skill-one/SKILL.md \ ${./skills-dir/skill-one/SKILL.md} ''; diff --git a/tests/modules/programs/codex/skills-inline-legacy-path.nix b/tests/modules/programs/codex/skills-inline-legacy-path.nix index 5030a40f..b9bb27c6 100644 --- a/tests/modules/programs/codex/skills-inline-legacy-path.nix +++ b/tests/modules/programs/codex/skills-inline-legacy-path.nix @@ -17,7 +17,14 @@ in }; nmt.script = '' + if [[ -L home-files/.codex/skills ]]; then + fail "Expected home-files/.codex/skills to remain a normal directory so unmanaged skills can coexist." + fi + assertLinkExists home-files/.codex/skills/inline-skill assertFileExists home-files/.codex/skills/inline-skill/SKILL.md + if [[ -L home-files/.codex/skills/inline-skill/SKILL.md ]]; then + fail "Expected home-files/.codex/skills/inline-skill/SKILL.md to be a regular file inside a symlinked skill directory." + fi assertFileContent home-files/.codex/skills/inline-skill/SKILL.md \ ${builtins.toFile "expected-inline-skill.md" '' # Inline Skill diff --git a/tests/modules/programs/codex/skills-inline-null-package.nix b/tests/modules/programs/codex/skills-inline-null-package.nix index c3bc019f..63e4f469 100644 --- a/tests/modules/programs/codex/skills-inline-null-package.nix +++ b/tests/modules/programs/codex/skills-inline-null-package.nix @@ -10,7 +10,11 @@ }; nmt.script = '' + assertLinkExists home-files/.agents/skills/inline-skill assertFileExists home-files/.agents/skills/inline-skill/SKILL.md + if [[ -L home-files/.agents/skills/inline-skill/SKILL.md ]]; then + fail "Expected home-files/.agents/skills/inline-skill/SKILL.md to be a regular file inside a symlinked skill directory." + fi assertFileContent home-files/.agents/skills/inline-skill/SKILL.md \ ${builtins.toFile "expected-inline-skill.md" '' # Inline Skill diff --git a/tests/modules/programs/codex/skills-inline.nix b/tests/modules/programs/codex/skills-inline.nix index 80ad05de..8f18227d 100644 --- a/tests/modules/programs/codex/skills-inline.nix +++ b/tests/modules/programs/codex/skills-inline.nix @@ -22,15 +22,34 @@ in skills = { inline-skill = inlineSkill; file-skill = ./skill-file.md; + dir-skill = ./skills-dir/skill-one; }; }; nmt.script = '' + if [[ -L home-files/.agents/skills ]]; then + fail "Expected home-files/.agents/skills to remain a normal directory so unmanaged skills can coexist." + fi + assertLinkExists home-files/.agents/skills/inline-skill assertFileExists home-files/.agents/skills/inline-skill/SKILL.md + if [[ -L home-files/.agents/skills/inline-skill/SKILL.md ]]; then + fail "Expected home-files/.agents/skills/inline-skill/SKILL.md to be a regular file inside a symlinked skill directory." + fi assertFileContent home-files/.agents/skills/inline-skill/SKILL.md \ ${builtins.toFile "expected-inline-skill.md" inlineSkill} + assertLinkExists home-files/.agents/skills/file-skill assertFileExists home-files/.agents/skills/file-skill/SKILL.md + if [[ -L home-files/.agents/skills/file-skill/SKILL.md ]]; then + fail "Expected home-files/.agents/skills/file-skill/SKILL.md to be a regular file inside a symlinked skill directory." + fi assertFileContent home-files/.agents/skills/file-skill/SKILL.md \ ${./skill-file.md} + assertLinkExists home-files/.agents/skills/dir-skill + assertFileExists home-files/.agents/skills/dir-skill/SKILL.md + if [[ -L home-files/.agents/skills/dir-skill/SKILL.md ]]; then + fail "Expected home-files/.agents/skills/dir-skill/SKILL.md to be a regular file inside a symlinked skill directory." + fi + assertFileContent home-files/.agents/skills/dir-skill/SKILL.md \ + ${./skills-dir/skill-one/SKILL.md} ''; }