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.