diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml index 97be8dcc2c8..14228131132 100644 --- a/.github/workflows/ai-moderator.lock.yml +++ b/.github/workflows/ai-moderator.lock.yml @@ -233,7 +233,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' - {"add_labels":{"allowed":["spam","ai-generated","link-spam","ai-inspected"],"max":3},"hide_comment":{"allowed_reasons":["spam"],"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"add_labels":{"allowed":["spam","ai-generated","link-spam","ai-inspected"],"max":3,"target":"*"},"hide_comment":{"allowed_reasons":["spam"],"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1}} GH_AW_SAFE_OUTPUTS_CONFIG_EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' [ diff --git a/.github/workflows/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml index e3b7b8315e8..02feef2dd47 100644 --- a/.github/workflows/dependabot-go-checker.lock.yml +++ b/.github/workflows/dependabot-go-checker.lock.yml @@ -210,7 +210,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' - {"close_issue":{"max":20,"required_title_prefix":"[deps]"},"create_issue":{"expires":48,"group":true,"max":10},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"close_issue":{"max":20,"required_title_prefix":"[deps]","target":"*"},"create_issue":{"expires":48,"group":true,"max":10},"missing_data":{},"missing_tool":{},"noop":{"max":1}} GH_AW_SAFE_OUTPUTS_CONFIG_EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' [ diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index 45e11aeab41..06da9be93ff 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -216,7 +216,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' - {"close_issue":{"max":10,"required_title_prefix":"[refactor] "},"create_issue":{"expires":48,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"close_issue":{"max":10,"required_title_prefix":"[refactor] ","target":"*"},"create_issue":{"expires":48,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} GH_AW_SAFE_OUTPUTS_CONFIG_EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' [ diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index d36182c1986..4c37b1a40ec 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -27,7 +27,7 @@ # - shared/github-queries-safe-input.md # - shared/reporting.md # -# frontmatter-hash: 1d773c2bb30e40025c0c1216fdb5d51d00dfa0505f14af0d63b3f48529cbca64 +# frontmatter-hash: ba8ac38783175d2491cfa23fdbc191b34f88eb3068c3e9b4c795c4c36094a988 name: "Smoke Copilot" "on": @@ -307,7 +307,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' - {"add_comment":{"max":2},"add_labels":{"allowed":["smoke-copilot"],"max":3},"create_issue":{"expires":2,"group":true,"max":1},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"default":null,"description":"The message to send","required":true,"type":"string"}},"output":"Slack message stub executed!"}} + {"add_comment":{"allowed_repos":["github/gh-aw"],"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"],"max":3},"create_issue":{"expires":2,"group":true,"max":1},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"default":null,"description":"The message to send","required":true,"type":"string"}},"output":"Slack message stub executed!"}} GH_AW_SAFE_OUTPUTS_CONFIG_EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' [ @@ -363,6 +363,10 @@ jobs: "item_number": { "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool will attempt to resolve the target from the current workflow context (triggering issue, PR, or discussion).", "type": "number" + }, + "repo": { + "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", + "type": "string" } }, "required": [ @@ -387,6 +391,10 @@ jobs: "type": "string" }, "type": "array" + }, + "repo": { + "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", + "type": "string" } }, "type": "object" @@ -1895,7 +1903,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"max\":1},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"allowed\":[\"smoke\"]}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"max\":1},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"allowed\":[\"smoke\"]}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 8baa843a3bb..bab2f477a15 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -49,12 +49,14 @@ safe-outputs: add-comment: hide-older-comments: true max: 2 + allowed-repos: ["github/gh-aw"] create-issue: expires: 2h group: true close-older-issues: true add-labels: allowed: [smoke-copilot] + allowed-repos: ["github/gh-aw"] remove-labels: allowed: [smoke] dispatch-workflow: diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 6d2e823c1ef..e7529f1e127 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4570,6 +4570,13 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository operations. Takes precedence over trial target repo settings." + }, + "allowed-repos": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of additional repositories in format 'owner/repo' that issues can be closed in. When specified, the agent can use a 'repo' field in the output to specify which repository to close the issue in. The target repository (current or target-repo) is always implicitly allowed." } }, "additionalProperties": false, @@ -5018,6 +5025,13 @@ "github-token": { "$ref": "#/$defs/github_token", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." + }, + "allowed-repos": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of additional repositories in format 'owner/repo' that labels can be added to. When specified, the agent can use a 'repo' field in the output to specify which repository to add labels to. The target repository (current or target-repo) is always implicitly allowed." } }, "additionalProperties": false diff --git a/pkg/workflow/safe_outputs_config_generation.go b/pkg/workflow/safe_outputs_config_generation.go index 64ef6218fb9..d4e3e5030cc 100644 --- a/pkg/workflow/safe_outputs_config_generation.go +++ b/pkg/workflow/safe_outputs_config_generation.go @@ -90,10 +90,19 @@ func generateSafeOutputsConfig(data *WorkflowData) string { ) } if data.SafeOutputs.AddComments != nil { - safeOutputsConfig["add_comment"] = generateMaxWithTargetConfig( + additionalFields := make(map[string]any) + // Note: AddCommentsConfig has Target, TargetRepoSlug, AllowedRepos but not embedded SafeOutputTargetConfig + // So we need to construct the target config manually + targetConfig := SafeOutputTargetConfig{ + Target: data.SafeOutputs.AddComments.Target, + TargetRepoSlug: data.SafeOutputs.AddComments.TargetRepoSlug, + AllowedRepos: data.SafeOutputs.AddComments.AllowedRepos, + } + safeOutputsConfig["add_comment"] = generateTargetConfigWithRepos( + targetConfig, data.SafeOutputs.AddComments.Max, 1, // default max - data.SafeOutputs.AddComments.Target, + additionalFields, ) } if data.SafeOutputs.CreateDiscussions != nil { @@ -118,11 +127,18 @@ func generateSafeOutputsConfig(data *WorkflowData) string { ) } if data.SafeOutputs.CloseIssues != nil { - safeOutputsConfig["close_issue"] = generateMaxWithRequiredFieldsConfig( + additionalFields := make(map[string]any) + if len(data.SafeOutputs.CloseIssues.RequiredLabels) > 0 { + additionalFields["required_labels"] = data.SafeOutputs.CloseIssues.RequiredLabels + } + if data.SafeOutputs.CloseIssues.RequiredTitlePrefix != "" { + additionalFields["required_title_prefix"] = data.SafeOutputs.CloseIssues.RequiredTitlePrefix + } + safeOutputsConfig["close_issue"] = generateTargetConfigWithRepos( + data.SafeOutputs.CloseIssues.SafeOutputTargetConfig, data.SafeOutputs.CloseIssues.Max, 1, // default max - data.SafeOutputs.CloseIssues.RequiredLabels, - data.SafeOutputs.CloseIssues.RequiredTitlePrefix, + additionalFields, ) } if data.SafeOutputs.CreatePullRequests != nil { @@ -152,10 +168,15 @@ func generateSafeOutputsConfig(data *WorkflowData) string { ) } if data.SafeOutputs.AddLabels != nil { - safeOutputsConfig["add_labels"] = generateMaxWithAllowedConfig( + additionalFields := make(map[string]any) + if len(data.SafeOutputs.AddLabels.Allowed) > 0 { + additionalFields["allowed"] = data.SafeOutputs.AddLabels.Allowed + } + safeOutputsConfig["add_labels"] = generateTargetConfigWithRepos( + data.SafeOutputs.AddLabels.SafeOutputTargetConfig, data.SafeOutputs.AddLabels.Max, 3, // default max - data.SafeOutputs.AddLabels.Allowed, + additionalFields, ) } if data.SafeOutputs.RemoveLabels != nil { diff --git a/pkg/workflow/safe_outputs_config_generation_helpers.go b/pkg/workflow/safe_outputs_config_generation_helpers.go index 9cc9b53943d..8101dae4269 100644 --- a/pkg/workflow/safe_outputs_config_generation_helpers.go +++ b/pkg/workflow/safe_outputs_config_generation_helpers.go @@ -155,3 +155,34 @@ func generateHideCommentConfig(max int, defaultMax int, allowedReasons []string) } return config } + +// generateTargetConfigWithRepos creates a config with target, target-repo, allowed_repos, and optional fields. +// Note on naming conventions: +// - "target-repo" uses hyphen to match frontmatter YAML format (key in config.json) +// - "allowed_repos" uses underscore to match JavaScript handler expectations (see repo_helpers.cjs) +// This inconsistency is intentional to maintain compatibility with existing handler code. +func generateTargetConfigWithRepos(targetConfig SafeOutputTargetConfig, max int, defaultMax int, additionalFields map[string]any) map[string]any { + config := generateMaxConfig(max, defaultMax) + + // Add target if specified + if targetConfig.Target != "" { + config["target"] = targetConfig.Target + } + + // Add target-repo if specified (use hyphenated key for consistency with frontmatter) + if targetConfig.TargetRepoSlug != "" { + config["target-repo"] = targetConfig.TargetRepoSlug + } + + // Add allowed_repos if specified (use underscore for consistency with handler code) + if len(targetConfig.AllowedRepos) > 0 { + config["allowed_repos"] = targetConfig.AllowedRepos + } + + // Add any additional fields + for key, value := range additionalFields { + config[key] = value + } + + return config +}