permission.go

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