Ctrl K
ComunidadGuías…

internal/mcptools/register.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package mcptools

import (
	"context"
	"xprem/config"
	"xprem/internal/services"
	"xprem/internal/types"

	mcpprot "github.com/modelcontextprotocol/go-sdk/mcp"
)

// Fallback is what gates a tool when roles are not enforced; the MIT twin of
// the rbac fallback the composition root maps it to.
type Fallback int

const (
	FallbackAdminOnly Fallback = iota + 1
	FallbackAnyMember
)

// Access declares who may see and call a gated tool: the rbac permission once
// roles are enforced, the fallback until then. An empty Perm means no grant
// can ever carry it, so the fallback alone decides: {Fallback:
// FallbackAdminOnly} reads as an admin-only tool.
type Access struct {
	Perm     string
	Fallback Fallback
}

// AppPermissions is the whole authorization truth of one account on one app:
// every catalog permission, held or not.
type AppPermissions struct {
	AppID   string   `json:"appId"`
	Granted []string `json:"granted"`
	Denied  []string `json:"denied"`
}

// AccountPermissions is the account's full permission picture, one entry per
// app it can see; the MIT twin of the rbac description wire maps onto it.
type AccountPermissions struct {
	Role          string           `json:"role" jsonschema:"admin or member; admins hold every permission on every app"`
	RolesEnforced bool             `json:"rolesEnforced" jsonschema:"whether per-app roles are enforced; when false, the community defaults decide"`
	Apps          []AppPermissions `json:"apps" jsonschema:"one entry per app this account can see; apps not listed are invisible to it"`
}

// The data interfaces below are consumer-side slices of the MIT services;
// wire injects the services as-is.

// AppVisibilityFunc is the visibility decision the ee rbac service provides:
// restricted=false means every app is visible.
type AppVisibilityFunc func(ctx context.Context, principal *services.DashboardPrincipal) (restricted bool, visible map[string]bool, err error)

type AppLister interface {
	GetApps(ctx context.Context) ([]config.AppDescriptor, error)
}

type BranchLister interface {
	GetBranches(ctx context.Context, appId string) ([]types.BranchMapping, error)
	GetRuntimeVersionsWithUpdateStats(ctx context.Context, appId string, branchName string) ([]types.RuntimeVersionWithStats, error)
}

type ChannelLister interface {
	GetChannels(ctx context.Context, appId string) ([]types.ChannelMapping, error)
}

type UpdateFeedReader interface {
	GetUpdateFeed(ctx context.Context, appId string, query types.UpdateFeedQuery) ([]types.UpdateFeedItem, error)
}

type UpdateRolloutReader interface {
	GetUpdateRollout(ctx context.Context, appId string, branchName string, runtimeVersion string) ([]types.RolloutUpdate, error)
}

type BundlePatchReader interface {
	ListPatches(ctx context.Context, appId, branch, updateId string) ([]types.BundlePatch, error)
}

type CertificateReader interface {
	RetrieveAppCertificate(ctx context.Context, appId string) (string, error)
}

type BranchWriter interface {
	CreateBranch(ctx context.Context, appId string, branchName string) (int64, error)
	DeleteBranch(ctx context.Context, branchName string, appId string) error
}

type ChannelWriter interface {
	CreateChannel(ctx context.Context, appId string, branchName *string, channelName string) (int64, error)
	DeleteChannel(ctx context.Context, channelName string, appId string) error
}

type DeploymentWriter interface {
	CreateRollback(ctx context.Context, appId string, platform types.Platform, commitHash, runtimeVersion, branchName, message string) (*types.Update, error)
	RepublishUpdateByID(ctx context.Context, appId, branchName, runtimeVersion, updateId string) (*types.Update, error)
	RepublishPublishGroup(ctx context.Context, appId, branchName, runtimeVersion, publishGroup string) (*services.GroupOperationResult, error)
}

// Deps carries what tools need from the composition root: MIT data access
// injected as-is, and decision functions whose implementations live in ee.
// Every field is a plain method value; wire assembles the struct without a
// line of logic, and tools compose data with decisions.
type Deps struct {
	// Apps is the app store; visibility is decided by VisibleApps, never here.
	Apps           AppLister
	Branches       BranchLister
	Channels       ChannelLister
	UpdateFeed     UpdateFeedReader
	UpdateRollouts UpdateRolloutReader
	BundlePatches  BundlePatchReader
	Certificates   CertificateReader
	BranchWriter   BranchWriter
	ChannelWriter  ChannelWriter
	Deployments    DeploymentWriter
	// SSOEnabled reports whether enterprise SSO is active; get_server_config
	// surfaces it.
	SSOEnabled func(ctx context.Context) bool
	// VisibleApps is the visibility decision: restricted=false means every
	// app is visible to the principal.
	VisibleApps AppVisibilityFunc
	// CanUseSomewhere decides tool visibility at session creation: whether
	// the principal may use an access-gated tool on at least one app.
	CanUseSomewhere func(ctx context.Context, principal *services.DashboardPrincipal, access Access) bool
	// Authorize gates one tool execution on one app, with the same semantics
	// as a route guard.
	Authorize func(ctx context.Context, principal *services.DashboardPrincipal, appID string, access Access) error
	// DescribePermissions answers whoami: the account's full permission
	// picture over the given apps, granted and denied.
	DescribePermissions func(ctx context.Context, principal *services.DashboardPrincipal, appIDs []string) (AccountPermissions, error)
}

// registrations is the tool table: one line per tool, the MCP twin of a
// routes_*.go file. access nil means any authenticated account.
var registrations = []struct {
	register func(*mcpprot.Server, Deps)
	access   *Access
}{
	{register: registerWhoami},
	{register: registerGetApps},
	{register: registerGetBranches},
	{register: registerGetRuntimeVersions},
	{register: registerGetChannels},
	{register: registerGetUpdates},
	{register: registerGetChannelRollouts},
	{register: registerGetUpdateRollout},
	{register: registerGetUpdatePatches},
	{register: registerGetCertificate, access: &certificateAccess},
	{register: registerGetServerConfig},
	{register: registerCreateBranch, access: &branchCreateAccess},
	{register: registerDeleteBranch, access: &branchDeleteAccess},
	{register: registerCreateChannel, access: &channelCreateAccess},
	{register: registerDeleteChannel, access: &channelDeleteAccess},
	{register: registerRollback, access: &publishAccess},
	{register: registerRepublish, access: &publishAccess},
}

// DeclaredPermissions lists the permission strings the tool table gates on.
// This package is MIT and cannot import the ee catalog those strings belong
// to, so ee validates them against it at boot (see rbac.MustValidateMCPTools):
// a typo here would silently make a tool admin-only instead of failing.
func DeclaredPermissions() []string {
	perms := make([]string, 0, len(registrations))
	for _, registration := range registrations {
		if registration.access != nil && registration.access.Perm != "" {
			perms = append(perms, registration.access.Perm)
		}
	}
	return perms
}

// Configurator returns what NewMCPService needs: a function that populates
// one session's server with the tools its principal may use. Gated tools are
// simply absent from a session that may not use them anywhere; execution
// still re-checks the specific app through Deps.Authorize.
func Configurator(deps Deps) func(ctx context.Context, principal *services.DashboardPrincipal, server *mcpprot.Server) {
	return func(ctx context.Context, principal *services.DashboardPrincipal, server *mcpprot.Server) {
		canUse := MemoizeVisibility(deps.CanUseSomewhere)
		for _, registration := range registrations {
			if registration.access != nil && !canUse(ctx, principal, *registration.access) {
				continue
			}
			registration.register(server, deps)
		}
	}
}

// MemoizeVisibility answers each distinct access once per session build.
// Several tools share a permission, and the decision behind it reads the
// account's grants every time; the principal cannot change mid-build.
func MemoizeVisibility(canUse func(context.Context, *services.DashboardPrincipal, Access) bool) func(context.Context, *services.DashboardPrincipal, Access) bool {
	answered := map[Access]bool{}
	return func(ctx context.Context, principal *services.DashboardPrincipal, access Access) bool {
		if allowed, known := answered[access]; known {
			return allowed
		}
		allowed := canUse(ctx, principal, access)
		answered[access] = allowed
		return allowed
	}
}