From 6426490fe776508eaf0ad218b802d87b0b514186 Mon Sep 17 00:00:00 2001 From: m3tm3re Date: Fri, 10 Apr 2026 17:10:46 +0200 Subject: [PATCH] feat(lib): implement OpenCode renderer in agents.nix --- lib/agents.nix | 77 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/lib/agents.nix b/lib/agents.nix index d059726..fe1af6f 100644 --- a/lib/agents.nix +++ b/lib/agents.nix @@ -28,14 +28,83 @@ # Returns the canonical attrset from lib.loadAgents (keyed by slug). loadCanonical = {agentsInput}: agentsInput.lib.loadAgents; - # Stub: OpenCode renderer — produces .opencode/agents/*.md files. - # To be implemented in Task 9. + # OpenCode renderer — produces a directory of .opencode/agents/*.md files. + # Each file has YAML frontmatter (description, mode, optional model, + # optional permission) followed by the agent's systemPrompt content. renderForOpencode = { pkgs, canonical, modelOverrides ? {}, - }: - pkgs.runCommand "opencode-agents" {} "echo stub > $out"; + }: let + # Split a rule string on the LAST colon to get { pattern, action }. + # e.g. "rm -rf *:ask" → pattern="rm -rf *", action="ask" + # e.g. "/run/agenix/**:deny" → pattern="/run/agenix/**", action="deny" + parseRule = ruleStr: let + parts = lib.strings.splitString ":" ruleStr; + # last element is the action, everything before joined with ":" is pattern + action = lib.last parts; + pattern = lib.concatStringsSep ":" (lib.init parts); + in {inherit pattern action;}; + + # Render one permission section to YAML lines (list of strings). + # intent-only → single line: " : " + # intent+rules → nested block starting with "*": , then specific rules + renderPermSection = tool: section: + if !(section ? rules) || section.rules == [] + then [" ${tool}: ${section.intent}"] + else let + parsedRules = map parseRule section.rules; + wildcardLine = " \"*\": ${section.intent}"; + ruleLines = map (r: " \"${r.pattern}\": ${r.action}") parsedRules; + in + [" ${tool}:"] ++ [wildcardLine] ++ ruleLines; + + # Render the full permission block for an agent. + # Returns empty list if no permissions. + renderPermBlock = permissions: + if permissions == {} || permissions == null + then [] + else + ["permission:"] + ++ lib.concatLists ( + lib.mapAttrsToList renderPermSection permissions + ); + + # Build the YAML frontmatter string for one agent. + mkFrontmatter = name: agent: let + descLine = "description: \"${agent.description}.\""; + modeLine = "mode: ${agent.mode}"; + modelLine = + lib.optionalString + (modelOverrides ? ${name}) + "model: ${modelOverrides.${name}}\n"; + permBlock = renderPermBlock (agent.permissions or {}); + permLines = + if permBlock == [] + then "" + else lib.concatStringsSep "\n" permBlock + "\n"; + in "---\n${descLine}\n${modeLine}\n${modelLine}${permLines}---\n"; + + # Build the full markdown file content for one agent. + mkAgentContent = name: agent: + (mkFrontmatter name agent) + agent.systemPrompt; + + # Write each agent as a derivation text file. + mkAgentFile = name: agent: + pkgs.writeText "${name}.md" (mkAgentContent name agent); + + # Attrset of name → derivation for each agent. + agentFiles = lib.mapAttrs mkAgentFile canonical; + + # Shell commands to copy each file into $out. + copyCommands = lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: file: "cp ${file} $out/${name}.md") agentFiles + ); + in + pkgs.runCommand "opencode-agents" {} '' + mkdir -p $out + ${copyCommands} + ''; # Stub: Claude Code renderer — produces .claude/agents/*.md files. # To be implemented in Task 10.