permission.go

  1package permission
  2
  3import (
  4	"errors"
  5	"path/filepath"
  6	"slices"
  7	"sync"
  8
  9	"github.com/charmbracelet/crush/internal/config"
 10	"github.com/charmbracelet/crush/internal/pubsub"
 11	"github.com/google/uuid"
 12)
 13
 14var ErrorPermissionDenied = errors.New("permission denied")
 15
 16type CreatePermissionRequest struct {
 17	SessionID   string `json:"session_id"`
 18	ToolName    string `json:"tool_name"`
 19	Description string `json:"description"`
 20	Action      string `json:"action"`
 21	Params      any    `json:"params"`
 22	Path        string `json:"path"`
 23}
 24
 25type PermissionRequest struct {
 26	ID          string `json:"id"`
 27	SessionID   string `json:"session_id"`
 28	ToolName    string `json:"tool_name"`
 29	Description string `json:"description"`
 30	Action      string `json:"action"`
 31	Params      any    `json:"params"`
 32	Path        string `json:"path"`
 33}
 34
 35type Service interface {
 36	pubsub.Suscriber[PermissionRequest]
 37	GrantPersistent(permission PermissionRequest)
 38	Grant(permission PermissionRequest)
 39	Deny(permission PermissionRequest)
 40	Request(opts CreatePermissionRequest) bool
 41	AutoApproveSession(sessionID string)
 42}
 43
 44type permissionService struct {
 45	*pubsub.Broker[PermissionRequest]
 46
 47	sessionPermissions    []PermissionRequest
 48	sessionPermissionsMu  sync.RWMutex
 49	pendingRequests       sync.Map
 50	autoApproveSessions   []string
 51	autoApproveSessionsMu sync.RWMutex
 52}
 53
 54func (s *permissionService) GrantPersistent(permission PermissionRequest) {
 55	respCh, ok := s.pendingRequests.Load(permission.ID)
 56	if ok {
 57		respCh.(chan bool) <- true
 58	}
 59
 60	s.sessionPermissionsMu.Lock()
 61	s.sessionPermissions = append(s.sessionPermissions, permission)
 62	s.sessionPermissionsMu.Unlock()
 63}
 64
 65func (s *permissionService) Grant(permission PermissionRequest) {
 66	respCh, ok := s.pendingRequests.Load(permission.ID)
 67	if ok {
 68		respCh.(chan bool) <- true
 69	}
 70}
 71
 72func (s *permissionService) Deny(permission PermissionRequest) {
 73	respCh, ok := s.pendingRequests.Load(permission.ID)
 74	if ok {
 75		respCh.(chan bool) <- false
 76	}
 77}
 78
 79func (s *permissionService) Request(opts CreatePermissionRequest) bool {
 80	s.autoApproveSessionsMu.RLock()
 81	autoApprove := slices.Contains(s.autoApproveSessions, opts.SessionID)
 82	s.autoApproveSessionsMu.RUnlock()
 83
 84	if autoApprove {
 85		return true
 86	}
 87
 88	dir := filepath.Dir(opts.Path)
 89	if dir == "." {
 90		dir = config.Get().WorkingDir()
 91	}
 92	permission := PermissionRequest{
 93		ID:          uuid.New().String(),
 94		Path:        dir,
 95		SessionID:   opts.SessionID,
 96		ToolName:    opts.ToolName,
 97		Description: opts.Description,
 98		Action:      opts.Action,
 99		Params:      opts.Params,
100	}
101
102	s.sessionPermissionsMu.RLock()
103	for _, p := range s.sessionPermissions {
104		if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
105			s.sessionPermissionsMu.RUnlock()
106			return true
107		}
108	}
109	s.sessionPermissionsMu.RUnlock()
110
111	respCh := make(chan bool, 1)
112
113	s.pendingRequests.Store(permission.ID, respCh)
114	defer s.pendingRequests.Delete(permission.ID)
115
116	s.Publish(pubsub.CreatedEvent, permission)
117
118	// Wait for the response indefinitely
119	return <-respCh
120}
121
122func (s *permissionService) AutoApproveSession(sessionID string) {
123	s.autoApproveSessionsMu.Lock()
124	s.autoApproveSessions = append(s.autoApproveSessions, sessionID)
125	s.autoApproveSessionsMu.Unlock()
126}
127
128func NewPermissionService() Service {
129	return &permissionService{
130		Broker:             pubsub.NewBroker[PermissionRequest](),
131		sessionPermissions: make([]PermissionRequest, 0),
132	}
133}