Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/daily-function-namer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion .github/workflows/smoke-copilot.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .github/workflows/smoke-copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ tools:
bash:
- "*"
github:
repos: public
min-integrity: approved
playwright:
serena:
languages:
Expand Down
5 changes: 5 additions & 0 deletions docs/public/schemas/mcp-gateway-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
39 changes: 25 additions & 14 deletions pkg/workflow/mcp_github_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +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 the prefix "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 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.
// 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 {
Expand All @@ -281,29 +285,32 @@ func deriveSafeOutputsGuardPolicyFromGitHub(githubTool any) map[string]any {
return nil
}

// Convert repos to accept list with "private:" prefix
// 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)
// 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" {
// For "all" or "public", add "private:*" to accept all private repos
acceptList = []string{"private:*"}
} else {
// Single pattern - add with private: prefix
acceptList = []string{"private:" + r}
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
// 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 {
acceptList = append(acceptList, "private:"+pattern)
}
}
case []string:
// Array of patterns (already strings)
// 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)
Expand All @@ -314,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{
Expand Down
18 changes: 4 additions & 14 deletions pkg/workflow/safeoutputs_guard_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,17 @@ 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",
githubTool: map[string]any{
"repos": "public",
"min-integrity": "none",
},
expectedPolicies: map[string]any{
"write-sink": map[string]any{
"accept": []string{"private:*"},
},
},
expectNil: false,
description: "repos='public' should map to private:*",
expectNil: true,
description: "repos='public' is a global keyword — no owner-scoped pattern possible, no write-sink derived",
},
{
name: "multiple repo patterns as []any",
Expand Down