======================================== Safe Output Tools Generation ======================================== This file handles tool JSON generation: it takes the full set of safe-output tool definitions (from safe-output-tools.json) and produces a filtered subset containing only those tools enabl
(data *WorkflowData, markdownPath string)
| 29 | // These tools are not in safe_outputs_tools.json and must be generated from |
| 30 | // the workflow configuration at compile time. |
| 31 | func generateDynamicTools(data *WorkflowData, markdownPath string) ([]map[string]any, error) { |
| 32 | var dynamicTools []map[string]any |
| 33 | |
| 34 | // Add custom job tools from SafeOutputs.Jobs |
| 35 | if len(data.SafeOutputs.Jobs) > 0 { |
| 36 | safeOutputsConfigLog.Printf("Adding %d custom job tools", len(data.SafeOutputs.Jobs)) |
| 37 | |
| 38 | // Sort job names for deterministic output |
| 39 | jobNames := sliceutil.SortedKeys(data.SafeOutputs.Jobs) |
| 40 | |
| 41 | for _, jobName := range jobNames { |
| 42 | jobConfig := data.SafeOutputs.Jobs[jobName] |
| 43 | normalizedJobName := stringutil.NormalizeSafeOutputIdentifier(jobName) |
| 44 | customTool := generateCustomJobToolDefinition(normalizedJobName, jobConfig) |
| 45 | dynamicTools = append(dynamicTools, customTool) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | // Add custom script tools from SafeOutputs.Scripts |
| 50 | if len(data.SafeOutputs.Scripts) > 0 { |
| 51 | safeOutputsConfigLog.Printf("Adding %d custom script tools to dynamic tools", len(data.SafeOutputs.Scripts)) |
| 52 | |
| 53 | scriptNames := sliceutil.SortedKeys(data.SafeOutputs.Scripts) |
| 54 | |
| 55 | for _, scriptName := range scriptNames { |
| 56 | scriptConfig := data.SafeOutputs.Scripts[scriptName] |
| 57 | normalizedScriptName := stringutil.NormalizeSafeOutputIdentifier(scriptName) |
| 58 | customTool := generateCustomScriptToolDefinition(normalizedScriptName, scriptConfig) |
| 59 | dynamicTools = append(dynamicTools, customTool) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | // Add custom action tools from SafeOutputs.Actions |
| 64 | // Each configured action is exposed as an MCP tool with schema derived from action.yml. |
| 65 | // The compiler resolves action.yml at compile time; if resolution fails the tool is still |
| 66 | // added with an empty inputSchema so the agent can still attempt to call it. |
| 67 | if len(data.SafeOutputs.Actions) > 0 { |
| 68 | safeOutputsConfigLog.Printf("Adding %d custom action tools to dynamic tools", len(data.SafeOutputs.Actions)) |
| 69 | |
| 70 | actionNames := sliceutil.SortedKeys(data.SafeOutputs.Actions) |
| 71 | |
| 72 | for _, actionName := range actionNames { |
| 73 | actionConfig := data.SafeOutputs.Actions[actionName] |
| 74 | customTool := generateActionToolDefinition(actionName, actionConfig) |
| 75 | dynamicTools = append(dynamicTools, customTool) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | // Add dynamic dispatch_workflow tools |
| 80 | if data.SafeOutputs.DispatchWorkflow != nil && len(data.SafeOutputs.DispatchWorkflow.Workflows) > 0 { |
| 81 | safeOutputsConfigLog.Printf("Adding %d dispatch_workflow tools", len(data.SafeOutputs.DispatchWorkflow.Workflows)) |
| 82 | |
| 83 | if data.SafeOutputs.DispatchWorkflow.WorkflowFiles == nil { |
| 84 | data.SafeOutputs.DispatchWorkflow.WorkflowFiles = make(map[string]string) |
| 85 | } |
| 86 | |
| 87 | for _, workflowName := range data.SafeOutputs.DispatchWorkflow.Workflows { |
| 88 | fileResult, err := findWorkflowFile(workflowName, markdownPath) |
no test coverage detected