permission.go

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