| 929 | } |
| 930 | |
| 931 | func (c *Compiler) addHandlerManagerConfigEnvVar(steps *[]string, data *WorkflowData) { |
| 932 | if data.SafeOutputs == nil { |
| 933 | safeOutputsConfigLog.Print("No safe-outputs configuration, skipping handler manager config") |
| 934 | return |
| 935 | } |
| 936 | |
| 937 | safeOutputsConfigLog.Print("Building handler manager configuration for safe-outputs") |
| 938 | // config holds both per-handler configs (keyed by handler name, e.g. "add_comment") and |
| 939 | // global runtime knobs (e.g. "mentions") that safe_output_handler_manager.cjs forwards to |
| 940 | // specific handlers at startup. Handler names are the reserved keys defined in handlerRegistry; |
| 941 | // non-handler keys ("mentions") are documented in safe_outputs_config_generation.go. |
| 942 | config := make(map[string]any) |
| 943 | |
| 944 | // Collect engine-specific manifest files and path prefixes (AgentFileProvider interface). |
| 945 | // These are merged with the global runtime-derived lists so that engine-specific |
| 946 | // instruction files (e.g. CLAUDE.md, .claude/, AGENTS.md) are automatically protected. |
| 947 | extraManifestFiles, extraPathPrefixes := c.getEngineAgentFileInfo(data) |
| 948 | fullManifestFiles := getAllManifestFiles(extraManifestFiles...) |
| 949 | fullPathPrefixes := getProtectedPathPrefixes(extraPathPrefixes...) |
| 950 | |
| 951 | // For workflow_call relay workflows, inject the resolved platform repo and ref into the |
| 952 | // dispatch_workflow handler config so dispatch targets the host repo, not the caller's. |
| 953 | safeOutputs := data.SafeOutputs |
| 954 | if hasWorkflowCallTrigger(data.On) && safeOutputs.DispatchWorkflow != nil { |
| 955 | if safeOutputs.DispatchWorkflow.TargetRepoSlug == "" { |
| 956 | safeOutputs = safeOutputsWithDispatchTargetRepo(safeOutputs, "${{ needs.activation.outputs.target_repo }}") |
| 957 | safeOutputsConfigLog.Print("Injecting target_repo into dispatch_workflow config for workflow_call relay") |
| 958 | } |
| 959 | if safeOutputs.DispatchWorkflow.TargetRef == "" { |
| 960 | safeOutputs = safeOutputsWithDispatchTargetRef(safeOutputs, "${{ needs.activation.outputs.target_ref }}") |
| 961 | safeOutputsConfigLog.Print("Injecting target_ref into dispatch_workflow config for workflow_call relay") |
| 962 | } |
| 963 | } |
| 964 | |
| 965 | // Build configuration for each handler using the registry |
| 966 | for handlerName, builder := range handlerRegistry { |
| 967 | handlerConfig := builder(safeOutputs) |
| 968 | // Include handler if: |
| 969 | // 1. It returns a non-nil config (explicitly enabled, even if empty) |
| 970 | // 2. For auto-enabled handlers, include even with empty config |
| 971 | if handlerConfig != nil { |
| 972 | injectCurrentCheckoutPatchWorkspacePath(handlerName, handlerConfig, data) |
| 973 | injectCheckoutMapping(handlerName, handlerConfig, data) |
| 974 | // Augment protected-files protection with engine-specific files for handlers that use it. |
| 975 | if _, hasProtected := handlerConfig["protected_files"]; hasProtected { |
| 976 | // Extract per-handler exclusions set by the handler builder (sentinel key). |
| 977 | // These are compile-time overrides and must not be forwarded to the runtime. |
| 978 | excludeFiles := ParseStringArrayFromConfig(handlerConfig, "_protected_files_exclude", nil) |
| 979 | delete(handlerConfig, "_protected_files_exclude") |
| 980 | |
| 981 | handlerConfig["protected_files"] = sliceutil.Exclude(fullManifestFiles, excludeFiles...) |
| 982 | filteredPrefixes := sliceutil.Exclude(fullPathPrefixes, excludeFiles...) |
| 983 | if len(filteredPrefixes) > 0 { |
| 984 | handlerConfig["protected_path_prefixes"] = filteredPrefixes |
| 985 | } else { |
| 986 | delete(handlerConfig, "protected_path_prefixes") |
| 987 | } |
| 988 | // Compute which top-level dot-folder prefixes are excluded so the runtime |