8ec62491c6
* feat(repos): add confirmed repository deletion Add a destructive delete_repository tool that requires an exact owner/repo confirmation through multi-round-trip elicitation. Gate the tool to MCP protocol 2026-07-28 and newer across local and remote transports. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * refactor(inventory): generalize tool availability guards Gate protocol-restricted tools on required elicitation capabilities and enforce direct calls inside the registered handler so SDK result finalization remains intact. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * feat(http): protect MRTR request state Seal repository deletion targets for self-hosted HTTP with a stable AES-256-GCM key. Hide only delete_repository when no key is configured and expose an optional sealer interface for remote integrators. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(repos): expire deletion confirmations Bind sealed repository deletion state to the immutable repository ID and a ten-minute expiry. Re-check identity before deletion so replay cannot affect a recreated repository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(http): preserve tool and scope restrictions Apply static allowlists before removing unavailable tools and fail closed on invalid configured tool names. Model independent OAuth requirements as conjunctive groups so repository deletion requires both delete_repo and repo. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(repos): require protected confirmation state Give stdio a process-local request-state sealer and make deletion fail closed without one. Preserve legacy any-of OAuth behavior globally while documenting and enforcing delete_repository's conjunctive delete_repo and repo requirements. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(oauth): request repository deletion scope Include delete_repo in the supported OAuth scope set used by stdio login, HTTP protected-resource metadata, and tool filtering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(oauth): require deletion scope opt-in Keep delete_repo in protected-resource discovery for step-up authorization while excluding it from the default stdio OAuth grant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * refactor(oauth): derive scope sets from catalog Generate protected-resource supported scopes and the lower-risk default OAuth grant from one canonical scope definition list. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * refactor(scopes): own OAuth scope catalog Move supported and default OAuth scope policy into pkg/scopes so protected-resource metadata and stdio grants derive from the scope domain package. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(scopes): require workflow scope opt-in Keep workflow and codespace in protected-resource discovery while excluding both from the default OAuth grant alongside delete_repo. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 --------- Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
156 lines
4.5 KiB
Go
156 lines
4.5 KiB
Go
package scopes
|
|
|
|
import "github.com/github/github-mcp-server/pkg/inventory"
|
|
|
|
// ToolScopeMap maps tool names to their scope requirements.
|
|
type ToolScopeMap map[string]*ToolScopeInfo
|
|
|
|
// ToolScopeInfo contains scope information for a single tool.
|
|
type ToolScopeInfo struct {
|
|
// RequiredScopes contains the scopes that are directly required by this tool.
|
|
RequiredScopes []string
|
|
|
|
// AcceptedScopes contains all scopes that satisfy the requirements (including parent scopes).
|
|
AcceptedScopes []string
|
|
|
|
// RequiredScopeGroups contains accepted alternatives for each independently
|
|
// required scope. Every group must be satisfied.
|
|
RequiredScopeGroups [][]string
|
|
}
|
|
|
|
// globalToolScopeMap is populated from inventory when SetToolScopeMapFromInventory is called
|
|
var globalToolScopeMap ToolScopeMap
|
|
|
|
// SetToolScopeMapFromInventory builds and stores a tool scope map from an inventory.
|
|
// This should be called after building the inventory to make scopes available for middleware.
|
|
func SetToolScopeMapFromInventory(inv *inventory.Inventory) {
|
|
globalToolScopeMap = GetToolScopeMapFromInventory(inv)
|
|
}
|
|
|
|
// SetGlobalToolScopeMap sets the global tool scope map directly.
|
|
// This is useful for testing when you don't have a full inventory.
|
|
func SetGlobalToolScopeMap(m ToolScopeMap) {
|
|
globalToolScopeMap = m
|
|
}
|
|
|
|
// GetToolScopeMap returns the global tool scope map.
|
|
// Returns an empty map if SetToolScopeMapFromInventory hasn't been called yet.
|
|
func GetToolScopeMap() (ToolScopeMap, error) {
|
|
if globalToolScopeMap == nil {
|
|
return make(ToolScopeMap), nil
|
|
}
|
|
return globalToolScopeMap, nil
|
|
}
|
|
|
|
// GetToolScopeInfo returns scope information for a specific tool from the global scope map.
|
|
func GetToolScopeInfo(toolName string) (*ToolScopeInfo, error) {
|
|
m, err := GetToolScopeMap()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m[toolName], nil
|
|
}
|
|
|
|
// GetToolScopeMapFromInventory builds a tool scope map from an inventory.
|
|
// This extracts scope information from ServerTool.RequiredScopes and ServerTool.AcceptedScopes.
|
|
func GetToolScopeMapFromInventory(inv *inventory.Inventory) ToolScopeMap {
|
|
result := make(ToolScopeMap)
|
|
|
|
// Get all tools from the inventory (both enabled and disabled)
|
|
// We need all tools for scope checking purposes
|
|
allTools := inv.AllTools()
|
|
for i := range allTools {
|
|
tool := &allTools[i]
|
|
if len(tool.RequiredScopes) > 0 || len(tool.AcceptedScopes) > 0 {
|
|
result[tool.Tool.Name] = &ToolScopeInfo{
|
|
RequiredScopes: tool.RequiredScopes,
|
|
AcceptedScopes: tool.AcceptedScopes,
|
|
RequiredScopeGroups: tool.RequiredScopeGroups,
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// HasAcceptedScope checks if any of the provided user scopes satisfy the tool's requirements.
|
|
func (t *ToolScopeInfo) HasAcceptedScope(userScopes ...string) bool {
|
|
if t != nil && len(t.RequiredScopeGroups) > 0 {
|
|
return HasRequiredScopeGroups(userScopes, t.RequiredScopeGroups)
|
|
}
|
|
if t == nil || len(t.AcceptedScopes) == 0 {
|
|
return true // No scopes required
|
|
}
|
|
|
|
userScopeSet := make(map[string]bool)
|
|
for _, scope := range userScopes {
|
|
userScopeSet[scope] = true
|
|
}
|
|
|
|
for _, scope := range t.AcceptedScopes {
|
|
if userScopeSet[scope] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MissingScopes returns the required scopes that are not present in the user's scopes.
|
|
func (t *ToolScopeInfo) MissingScopes(userScopes ...string) []string {
|
|
if t == nil || len(t.RequiredScopes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Create a set of user scopes for O(1) lookup
|
|
userScopeSet := make(map[string]bool, len(userScopes))
|
|
for _, s := range userScopes {
|
|
userScopeSet[s] = true
|
|
}
|
|
|
|
if len(t.RequiredScopeGroups) > 0 {
|
|
userScopeSet := expandScopeSet(userScopes)
|
|
var missing []string
|
|
for i, group := range t.RequiredScopeGroups {
|
|
satisfied := false
|
|
for _, scope := range group {
|
|
if userScopeSet[scope] {
|
|
satisfied = true
|
|
break
|
|
}
|
|
}
|
|
if !satisfied && i < len(t.RequiredScopes) {
|
|
missing = append(missing, t.RequiredScopes[i])
|
|
}
|
|
}
|
|
return missing
|
|
}
|
|
|
|
// Check if any accepted scope is present
|
|
hasAccepted := false
|
|
for _, scope := range t.AcceptedScopes {
|
|
if userScopeSet[scope] {
|
|
hasAccepted = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasAccepted {
|
|
return nil // User has sufficient scopes
|
|
}
|
|
|
|
// Return required scopes as the minimum needed
|
|
missing := make([]string, len(t.RequiredScopes))
|
|
copy(missing, t.RequiredScopes)
|
|
return missing
|
|
}
|
|
|
|
// GetRequiredScopesSlice returns the required scopes as a slice of strings.
|
|
func (t *ToolScopeInfo) GetRequiredScopesSlice() []string {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
scopes := make([]string, len(t.RequiredScopes))
|
|
copy(scopes, t.RequiredScopes)
|
|
return scopes
|
|
}
|