From 414c9c05a1e84d236419a9296f46718fc05ff564 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:14:09 +0000 Subject: [PATCH 1/5] Initial plan From 1817202eda7445ac565f0a1af39fc6c408fe4f28 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:24:28 +0000 Subject: [PATCH 2/5] fix: correct safeoutputs guard-policy rendering for repos="public" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed a bug where repos="public" in GitHub guard-policy was incorrectly rendered as "public:*" in safeoutputs write-sink policy. It should be rendered as just "public" (without the :* wildcard suffix). The bug was in deriveSafeOutputsGuardPolicyFromGitHub() which treated both "all" and "public" the same way, mapping both to "private:*". Now they are handled correctly: - repos="all" → write-sink accept=["private:*"] - repos="public" → write-sink accept=["public"] - repos="pattern" → write-sink accept=["private:pattern"] Also updated the code to use a tagged switch statement as suggested by the staticcheck linter (QF1003). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- pkg/workflow/mcp_github_config.go | 10 +++++++--- pkg/workflow/safeoutputs_guard_policy_test.go | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 06d05b996c..2a2d58973f 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -287,10 +287,14 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { switch r := repos.(type) { case string: // Single string value (e.g., "all", "public", or a pattern) - if r == "all" || r == "public" { - // For "all" or "public", add "private:*" to accept all private repos + switch r { + case "all": + // For "all", add "private:*" to accept all private repos acceptList = []string{"private:*"} - } else { + case "public": + // For "public", add "public" without wildcard suffix + acceptList = []string{"public"} + default: // Single pattern - add with private: prefix acceptList = []string{"private:" + r} } diff --git a/pkg/workflow/safeoutputs_guard_policy_test.go b/pkg/workflow/safeoutputs_guard_policy_test.go index 8ccf781264..78f5a040eb 100644 --- a/pkg/workflow/safeoutputs_guard_policy_test.go +++ b/pkg/workflow/safeoutputs_guard_policy_test.go @@ -68,11 +68,11 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { }, expectedPolicies: map[string]any{ "write-sink": map[string]any{ - "accept": []string{"private:*"}, + "accept": []string{"public"}, }, }, expectNil: false, - description: "repos='public' should map to private:*", + description: "repos='public' should map to public (without :* suffix)", }, { name: "multiple repo patterns as []any", From b6061b8c6806597c9fb63474f4031b4ce48eebc8 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:47:05 +0000 Subject: [PATCH 3/5] fix: correct safeoutputs guard-policy rendering for repos="public" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated deriveSafeOutputsGuardPolicyFromGitHub() to implement the correct transformation rules based on DIFC (Decentralized Information Flow Control) principles: - repos="all" or "public" → accept=[] (agent secrecy is empty) - repos="O/*" → accept=["private:O"] (owner wildcard strips wildcard) - repos="O/P*" → accept=["private:O/P*"] (prefix wildcard keeps wildcard) - repos="O/R" → accept=["private:O/R"] (specific repo keeps pattern) Added transformRepoPattern() helper function to handle owner wildcard detection and stripping. Updated all test cases to match specification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- pkg/workflow/mcp_github_config.go | 43 +++++++++++----- pkg/workflow/safeoutputs_guard_policy_test.go | 50 ++++++++++++++++--- 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 2a2d58973f..0ba3c114d3 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -258,8 +258,13 @@ func getGitHubGuardPolicies(githubTool any) map[string]any { // deriveSafeOutputsGuardPolicyFromGitHub generates a safeoutputs guard-policy from GitHub guard-policy. // When the GitHub MCP server has a guard-policy with repos, the safeoutputs MCP must also have -// a linked guard-policy. Each entry in the GitHub MCP server's "repos" must have a corresponding -// entry in safeoutputs "accept" with the prefix "private:". +// a linked guard-policy with accept field derived from repos according to these rules: +// +// Rules by repos value: +// - repos="all" or repos="public": accept=[] (write-sink not required, agent secrecy is empty) +// - repos=["O/*"]: accept=["private:O"] (owner wildcard → strip wildcard) +// - repos=["O/P*"]: accept=["private:O/P*"] (prefix wildcard → keep as-is) +// - repos=["O/R"]: accept=["private:O/R"] (specific repo → keep as-is) // // This allows the gateway to read private data from the GitHub MCP server and still write to safeoutputs. // Returns nil if no GitHub guard policies are configured. @@ -281,36 +286,33 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { return nil } - // Convert repos to accept list with "private:" prefix + // Convert repos to accept list according to the specification var acceptList []string switch r := repos.(type) { case string: // Single string value (e.g., "all", "public", or a pattern) switch r { - case "all": - // For "all", add "private:*" to accept all private repos - acceptList = []string{"private:*"} - case "public": - // For "public", add "public" without wildcard suffix - acceptList = []string{"public"} + case "all", "public": + // For "all" or "public", agent secrecy is empty, so accept=[] (write-sink not required) + acceptList = []string{} default: - // Single pattern - add with private: prefix - acceptList = []string{"private:" + r} + // Single pattern - transform according to rules + acceptList = []string{transformRepoPattern(r)} } case []any: // Array of patterns acceptList = make([]string, 0, len(r)) for _, item := range r { if pattern, ok := item.(string); ok { - acceptList = append(acceptList, "private:"+pattern) + acceptList = append(acceptList, transformRepoPattern(pattern)) } } case []string: // Array of patterns (already strings) acceptList = make([]string, 0, len(r)) for _, pattern := range r { - acceptList = append(acceptList, "private:"+pattern) + acceptList = append(acceptList, transformRepoPattern(pattern)) } default: // Unknown type, return nil @@ -326,6 +328,21 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { } } +// transformRepoPattern transforms a repos pattern to the corresponding accept pattern. +// Rules: +// - "O/*" → "private:O" (owner wildcard → strip wildcard) +// - "O/P*" → "private:O/P*" (prefix wildcard → keep as-is) +// - "O/R" → "private:O/R" (specific repo → keep as-is) +func transformRepoPattern(pattern string) string { + // Check if pattern ends with "/*" (owner wildcard) + if owner, found := strings.CutSuffix(pattern, "/*"); found { + // Strip the wildcard: "owner/*" → "private:owner" + return "private:" + owner + } + // All other patterns (including "O/P*" prefix wildcards): add "private:" prefix + return "private:" + pattern +} + func getGitHubDockerImageVersion(githubTool any) string { githubDockerImageVersion := string(constants.DefaultGitHubMCPServerVersion) // Default Docker image version // Extract version setting from tool properties diff --git a/pkg/workflow/safeoutputs_guard_policy_test.go b/pkg/workflow/safeoutputs_guard_policy_test.go index 78f5a040eb..1c86148ce9 100644 --- a/pkg/workflow/safeoutputs_guard_policy_test.go +++ b/pkg/workflow/safeoutputs_guard_policy_test.go @@ -33,18 +33,32 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { description: "Single repo pattern should get private: prefix", }, { - name: "wildcard repo pattern", + name: "owner wildcard pattern", githubTool: map[string]any{ "repos": "github/*", "min-integrity": "approved", }, expectedPolicies: map[string]any{ "write-sink": map[string]any{ - "accept": []string{"private:github/*"}, + "accept": []string{"private:github"}, }, }, expectNil: false, - description: "Wildcard pattern should get private: prefix", + description: "Owner wildcard (github/*) should strip wildcard → private:github", + }, + { + name: "repo prefix wildcard pattern", + githubTool: map[string]any{ + "repos": "github/gh-aw*", + "min-integrity": "approved", + }, + expectedPolicies: map[string]any{ + "write-sink": map[string]any{ + "accept": []string{"private:github/gh-aw*"}, + }, + }, + expectNil: false, + description: "Repo prefix wildcard should keep as-is with private: prefix", }, { name: "repos set to all", @@ -54,11 +68,11 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { }, expectedPolicies: map[string]any{ "write-sink": map[string]any{ - "accept": []string{"private:*"}, + "accept": []string{}, }, }, expectNil: false, - description: "repos='all' should map to private:*", + description: "repos='all' should map to empty accept array (agent secrecy is empty)", }, { name: "repos set to public", @@ -68,11 +82,11 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { }, expectedPolicies: map[string]any{ "write-sink": map[string]any{ - "accept": []string{"public"}, + "accept": []string{}, }, }, expectNil: false, - description: "repos='public' should map to public (without :* suffix)", + description: "repos='public' should map to empty accept array (agent secrecy is empty)", }, { name: "multiple repo patterns as []any", @@ -92,7 +106,7 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { }, }, expectNil: false, - description: "Array of patterns should all get private: prefix", + description: "Array of prefix patterns should all get private: prefix", }, { name: "multiple repo patterns as []string", @@ -114,6 +128,26 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { expectNil: false, description: "[]string array should all get private: prefix", }, + { + name: "mixed patterns with owner wildcard", + githubTool: map[string]any{ + "repos": []string{ + "github/*", + "microsoft/copilot", + }, + "min-integrity": "approved", + }, + expectedPolicies: map[string]any{ + "write-sink": map[string]any{ + "accept": []string{ + "private:github", + "private:microsoft/copilot", + }, + }, + }, + expectNil: false, + description: "Owner wildcard (github/*) should transform to private:github, specific repo should keep pattern", + }, { name: "no repos configured", githubTool: map[string]any{ From 93f530f5c71f26866564cab372f7ae168a753bc1 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:01:07 +0000 Subject: [PATCH 4/5] test: add comprehensive array handling test cases for guard policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added test cases to ensure comprehensive coverage of array handling with multiple entries: - Array with all three pattern types (owner wildcard, prefix wildcard, specific repo) - Array with multiple owner wildcards These tests verify that the transformRepoPattern() function correctly handles different combinations of patterns in arrays, ensuring each pattern type is transformed according to the specification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- pkg/workflow/safeoutputs_guard_policy_test.go | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pkg/workflow/safeoutputs_guard_policy_test.go b/pkg/workflow/safeoutputs_guard_policy_test.go index 1c86148ce9..08c989b8ec 100644 --- a/pkg/workflow/safeoutputs_guard_policy_test.go +++ b/pkg/workflow/safeoutputs_guard_policy_test.go @@ -148,6 +148,50 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { expectNil: false, description: "Owner wildcard (github/*) should transform to private:github, specific repo should keep pattern", }, + { + name: "array with all three pattern types", + githubTool: map[string]any{ + "repos": []string{ + "github/*", // owner wildcard + "microsoft/copilot*", // prefix wildcard + "google/gemini", // specific repo + }, + "min-integrity": "approved", + }, + expectedPolicies: map[string]any{ + "write-sink": map[string]any{ + "accept": []string{ + "private:github", + "private:microsoft/copilot*", + "private:google/gemini", + }, + }, + }, + expectNil: false, + description: "Array with owner wildcard, prefix wildcard, and specific repo should all transform correctly", + }, + { + name: "array with multiple owner wildcards", + githubTool: map[string]any{ + "repos": []any{ + "github/*", + "microsoft/*", + "google/*", + }, + "min-integrity": "approved", + }, + expectedPolicies: map[string]any{ + "write-sink": map[string]any{ + "accept": []string{ + "private:github", + "private:microsoft", + "private:google", + }, + }, + }, + expectNil: false, + description: "Multiple owner wildcards should all strip the wildcard suffix", + }, { name: "no repos configured", githubTool: map[string]any{ From a6e714f8b1c0bd732afa4f5125ab407c7835d8f2 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:16:24 +0000 Subject: [PATCH 5/5] fix: align guard-policy handling with specification for all/public Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/workflow/mcp_github_config.go | 8 ++++---- pkg/workflow/safeoutputs_guard_policy_test.go | 18 ++++-------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 0ba3c114d3..bb9d783179 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -261,13 +261,13 @@ func getGitHubGuardPolicies(githubTool any) map[string]any { // a linked guard-policy with accept field derived from repos according to these rules: // // Rules by repos value: -// - repos="all" or repos="public": accept=[] (write-sink not required, agent secrecy is empty) +// - repos="all" or repos="public": returns nil (write-sink not required, agent secrecy is empty) // - repos=["O/*"]: accept=["private:O"] (owner wildcard → strip wildcard) // - repos=["O/P*"]: accept=["private:O/P*"] (prefix wildcard → keep as-is) // - repos=["O/R"]: accept=["private:O/R"] (specific repo → keep as-is) // // This allows the gateway to read private data from the GitHub MCP server and still write to safeoutputs. -// Returns nil if no GitHub guard policies are configured. +// Returns nil if no GitHub guard policies are configured or if repos="all" or repos="public". func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { githubPolicies := getGitHubGuardPolicies(githubTool) if githubPolicies == nil { @@ -294,8 +294,8 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { // Single string value (e.g., "all", "public", or a pattern) switch r { case "all", "public": - // For "all" or "public", agent secrecy is empty, so accept=[] (write-sink not required) - acceptList = []string{} + // For "all" or "public", agent secrecy is empty, so write-sink not required + return nil default: // Single pattern - transform according to rules acceptList = []string{transformRepoPattern(r)} diff --git a/pkg/workflow/safeoutputs_guard_policy_test.go b/pkg/workflow/safeoutputs_guard_policy_test.go index 08c989b8ec..fbd3d1c433 100644 --- a/pkg/workflow/safeoutputs_guard_policy_test.go +++ b/pkg/workflow/safeoutputs_guard_policy_test.go @@ -66,13 +66,8 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { "repos": "all", "min-integrity": "approved", }, - expectedPolicies: map[string]any{ - "write-sink": map[string]any{ - "accept": []string{}, - }, - }, - expectNil: false, - description: "repos='all' should map to empty accept array (agent secrecy is empty)", + expectNil: true, + description: "repos='all' should return nil (write-sink not required, agent secrecy is empty)", }, { name: "repos set to public", @@ -80,13 +75,8 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { "repos": "public", "min-integrity": "none", }, - expectedPolicies: map[string]any{ - "write-sink": map[string]any{ - "accept": []string{}, - }, - }, - expectNil: false, - description: "repos='public' should map to empty accept array (agent secrecy is empty)", + expectNil: true, + description: "repos='public' should return nil (write-sink not required, agent secrecy is empty)", }, { name: "multiple repo patterns as []any",