From 8f6f4d2c47ce03bb6b85aba2760ba92e16d2eb44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 01:40:24 +0000 Subject: [PATCH 1/5] Initial plan From 94f9436ea42e650bb7ec93c0c42e4d05ca30aa7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:00:09 +0000 Subject: [PATCH 2/5] Initial plan Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/daily-function-namer.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daily-function-namer.lock.yml b/.github/workflows/daily-function-namer.lock.yml index 25810e63ba..301cefdb2f 100644 --- a/.github/workflows/daily-function-namer.lock.yml +++ b/.github/workflows/daily-function-namer.lock.yml @@ -869,7 +869,7 @@ jobs: - name: Append agent step summary if: always() run: bash /opt/gh-aw/actions/append_agent_step_summary.sh - - name: Copy safe outputs + - name: Copy Safe Outputs if: always() run: | mkdir -p /tmp/gh-aw From e5251060b7c4fbb2ce58d15b19e3bd8c7f2738c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:16:08 +0000 Subject: [PATCH 3/5] fix: correct secrecy level in write-sink guard policy and update public schema Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../schemas/mcp-gateway-config.schema.json | 5 ++++ pkg/workflow/mcp_github_config.go | 30 +++++++++++++------ pkg/workflow/safeoutputs_guard_policy_test.go | 4 +-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/public/schemas/mcp-gateway-config.schema.json b/docs/public/schemas/mcp-gateway-config.schema.json index e3606d9f9d..6854513e32 100644 --- a/docs/public/schemas/mcp-gateway-config.schema.json +++ b/docs/public/schemas/mcp-gateway-config.schema.json @@ -116,6 +116,11 @@ "type": "string" }, "default": ["*"] + }, + "guard-policies": { + "type": "object", + "description": "Guard policies for access control at the MCP gateway level. The structure of guard policies is server-specific. For GitHub MCP server, see the GitHub guard policy schema. For other servers (Jira, WorkIQ), different policy schemas will apply.", + "additionalProperties": true } }, "required": ["container"], diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 06d05b996c..2acab9c897 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -259,9 +259,12 @@ 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:". +// entry in safeoutputs "accept" with a secrecy prefix that reflects the data sensitivity: +// - repos="public": only public data flows through, so prefix is "public:" +// - repos="all" or specific patterns: private data may flow through, so prefix is "private:" // -// This allows the gateway to read private data from the GitHub MCP server and still write to safeoutputs. +// This enforces the principle of least privilege: if the agent can only read public repositories, +// it should only be permitted to write data tagged with "public" secrecy to safe outputs. // Returns nil if no GitHub guard policies are configured. func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { githubPolicies := getGitHubGuardPolicies(githubTool) @@ -281,21 +284,30 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { return nil } - // Convert repos to accept list with "private:" prefix + // Convert repos to accept list with appropriate secrecy prefix. + // repos="public" restricts the agent to public repositories only, so the write-sink + // accept list uses the "public:" secrecy prefix to enforce that only public-level data + // is written to safe outputs. + // All other cases (repos="all" or specific patterns) may involve private repositories, + // so the "private:" prefix is used to allow writing data of any secrecy level. var acceptList []string 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 "public": + // Public repos only: enforce public secrecy level for safe output writes + acceptList = []string{"public:*"} + case "all": + // All repos including private: allow private secrecy level writes acceptList = []string{"private:*"} - } else { - // Single pattern - add with private: prefix + default: + // Single specific pattern (e.g., "owner/repo"): may be private, use private prefix acceptList = []string{"private:" + r} } case []any: - // Array of patterns + // Array of patterns: may include private repos, use private prefix acceptList = make([]string, 0, len(r)) for _, item := range r { if pattern, ok := item.(string); ok { @@ -303,7 +315,7 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { } } case []string: - // Array of patterns (already strings) + // Array of patterns (already strings): may include private repos, use private prefix acceptList = make([]string, 0, len(r)) for _, pattern := range r { acceptList = append(acceptList, "private:"+pattern) diff --git a/pkg/workflow/safeoutputs_guard_policy_test.go b/pkg/workflow/safeoutputs_guard_policy_test.go index 8ccf781264..d719dca029 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:* (public repos only → public secrecy level)", }, { name: "multiple repo patterns as []any", From ee9c8678be12f8f6ef478ad7a436e3e839de773e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:23:52 +0000 Subject: [PATCH 4/5] feat: add repos=public min-integrity=approved guard policy to smoke-copilot Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 15 ++++++++++++++- .github/workflows/smoke-copilot.md | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 1ad3551828..8a70125cd1 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -29,7 +29,7 @@ # - shared/github-queries-mcp-script.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"b7ace3a0bae816ff9001b09294262e8a7367f7232f18b3b9918abf9d9d27253d","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"3542ab7695605246be9d78e721d6bedbcc1b3b0c9531254643458ddf40f90c29","strict":true} name: "Smoke Copilot" "on": @@ -1813,6 +1813,12 @@ jobs: "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "approved", + "repos": "public" + } } }, "mcpscripts": { @@ -1834,6 +1840,13 @@ jobs: "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", "headers": { "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "public:*" + ] + } } }, "serena": { diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 9e54e64339..b3142ddd8e 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -36,6 +36,8 @@ tools: bash: - "*" github: + repos: public + min-integrity: approved playwright: serena: languages: From 4b900f888d710f8a20fafffc6186f60306c512f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:13:51 +0000 Subject: [PATCH 5/5] fix: don't derive write-sink guard-policy for repos=all/public (invalid bare wildcard scope) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 7 --- pkg/workflow/mcp_github_config.go | 53 +++++++++---------- pkg/workflow/safeoutputs_guard_policy_test.go | 18 ++----- 3 files changed, 30 insertions(+), 48 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 8a70125cd1..238460c77b 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1840,13 +1840,6 @@ jobs: "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", "headers": { "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" - }, - "guard-policies": { - "write-sink": { - "accept": [ - "public:*" - ] - } } }, "serena": { diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 2acab9c897..4e8075d366 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -257,15 +257,16 @@ 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 a secrecy prefix that reflects the data sensitivity: -// - repos="public": only public data flows through, so prefix is "public:" -// - repos="all" or specific patterns: private data may flow through, so prefix is "private:" +// When the GitHub MCP server has a guard-policy with an array of specific repo patterns, the +// safeoutputs MCP must also have a linked guard-policy. Each entry in the "repos" array gets a +// corresponding "private:" accept entry in the safeoutputs write-sink policy. // -// This enforces the principle of least privilege: if the agent can only read public repositories, -// it should only be permitted to write data tagged with "public" secrecy to safe outputs. -// Returns nil if no GitHub guard policies are configured. +// The safeoutputs server requires owner-scoped patterns (e.g., "private:owner/repo", +// "private:owner/*"). Bare wildcards like "private:*" are not valid because they lack an owner +// scope. For this reason, the global string keywords "all" and "public" do not produce a +// write-sink guard-policy — only array patterns with explicit owner/repo components do. +// +// Returns nil if no repos array guard policies are configured. func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { githubPolicies := getGitHubGuardPolicies(githubTool) if githubPolicies == nil { @@ -284,30 +285,24 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { return nil } - // Convert repos to accept list with appropriate secrecy prefix. - // repos="public" restricts the agent to public repositories only, so the write-sink - // accept list uses the "public:" secrecy prefix to enforce that only public-level data - // is written to safe outputs. - // All other cases (repos="all" or specific patterns) may involve private repositories, - // so the "private:" prefix is used to allow writing data of any secrecy level. + // Convert repos to accept list with "private:" prefix. + // Only array patterns produce a write-sink policy; string keywords ("all", "public") do not, + // because the safeoutputs server requires owner-scoped patterns (e.g., "private:owner/repo"). var acceptList []string switch r := repos.(type) { case string: - // Single string value (e.g., "all", "public", or a pattern) - switch r { - case "public": - // Public repos only: enforce public secrecy level for safe output writes - acceptList = []string{"public:*"} - case "all": - // All repos including private: allow private secrecy level writes - acceptList = []string{"private:*"} - default: - // Single specific pattern (e.g., "owner/repo"): may be private, use private prefix - acceptList = []string{"private:" + r} + // Global string keywords cannot be expressed as owner-scoped patterns. + // The safeoutputs server requires "prefix:owner/repo" format; bare wildcards + // like "private:*" (no owner) are not valid scopes. + if r == "all" || r == "public" { + githubConfigLog.Printf("repos=%q is a global keyword — no owner-scoped pattern possible, no safeoutputs write-sink guard-policy derived", r) + return nil } + // Single owner-scoped pattern (e.g., "owner/repo"): use private: prefix + acceptList = []string{"private:" + r} case []any: - // Array of patterns: may include private repos, use private prefix + // Array of owner-scoped patterns: generate private: accept entries acceptList = make([]string, 0, len(r)) for _, item := range r { if pattern, ok := item.(string); ok { @@ -315,7 +310,7 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { } } case []string: - // Array of patterns (already strings): may include private repos, use private prefix + // Array of owner-scoped patterns (already strings): generate private: accept entries acceptList = make([]string, 0, len(r)) for _, pattern := range r { acceptList = append(acceptList, "private:"+pattern) @@ -326,6 +321,10 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any { return nil } + if len(acceptList) == 0 { + return nil + } + // Build the write-sink policy for safeoutputs return map[string]any{ "write-sink": map[string]any{ diff --git a/pkg/workflow/safeoutputs_guard_policy_test.go b/pkg/workflow/safeoutputs_guard_policy_test.go index d719dca029..1ebb100bb2 100644 --- a/pkg/workflow/safeoutputs_guard_policy_test.go +++ b/pkg/workflow/safeoutputs_guard_policy_test.go @@ -52,13 +52,8 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { "repos": "all", "min-integrity": "approved", }, - expectedPolicies: map[string]any{ - "write-sink": map[string]any{ - "accept": []string{"private:*"}, - }, - }, - expectNil: false, - description: "repos='all' should map to private:*", + expectNil: true, + description: "repos='all' is a global keyword — no owner-scoped pattern possible, no write-sink derived", }, { name: "repos set to public", @@ -66,13 +61,8 @@ func TestDeriveSafeOutputsGuardPolicyFromGitHub(t *testing.T) { "repos": "public", "min-integrity": "none", }, - expectedPolicies: map[string]any{ - "write-sink": map[string]any{ - "accept": []string{"public:*"}, - }, - }, - expectNil: false, - description: "repos='public' should map to public:* (public repos only → public secrecy level)", + expectNil: true, + description: "repos='public' is a global keyword — no owner-scoped pattern possible, no write-sink derived", }, { name: "multiple repo patterns as []any",