1package permission
  2
  3import (
  4	"context"
  5	"errors"
  6	"os"
  7	"path/filepath"
  8	"slices"
  9	"sync"
 10
 11	"github.com/charmbracelet/crush/internal/csync"
 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	ToolCallID  string `json:"tool_call_id"`
 21	ToolName    string `json:"tool_name"`
 22	Description string `json:"description"`
 23	Action      string `json:"action"`
 24	Params      any    `json:"params"`
 25	Path        string `json:"path"`
 26}
 27
 28type PermissionNotification struct {
 29	ToolCallID string `json:"tool_call_id"`
 30	Granted    bool   `json:"granted"`
 31	Denied     bool   `json:"denied"`
 32}
 33
 34type PermissionRequest struct {
 35	ID          string `json:"id"`
 36	SessionID   string `json:"session_id"`
 37	ToolCallID  string `json:"tool_call_id"`
 38	ToolName    string `json:"tool_name"`
 39	Description string `json:"description"`
 40	Action      string `json:"action"`
 41	Params      any    `json:"params"`
 42	Path        string `json:"path"`
 43}
 44
 45type Service interface {
 46	pubsub.Suscriber[PermissionRequest]
 47	GrantPersistent(permission PermissionRequest)
 48	Grant(permission PermissionRequest)
 49	Deny(permission PermissionRequest)
 50	Request(opts CreatePermissionRequest) bool
 51	AutoApproveSession(sessionID string)
 52	SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[PermissionNotification]
 53}
 54
 55type permissionService struct {
 56	*pubsub.Broker[PermissionRequest]
 57
 58	notificationBroker    *pubsub.Broker[PermissionNotification]
 59	workingDir            string
 60	sessionPermissions    []PermissionRequest
 61	sessionPermissionsMu  sync.RWMutex
 62	pendingRequests       *csync.Map[string, chan bool]
 63	autoApproveSessions   map[string]bool
 64	autoApproveSessionsMu sync.RWMutex
 65	skip                  bool
 66	allowedTools          []string
 67
 68	// used to make sure we only process one request at a time
 69	requestMu     sync.Mutex
 70	activeRequest *PermissionRequest
 71}
 72
 73func (s *permissionService) GrantPersistent(permission PermissionRequest) {
 74	s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
 75		ToolCallID: permission.ToolCallID,
 76		Granted:    true,
 77	})
 78	respCh, ok := s.pendingRequests.Get(permission.ID)
 79	if ok {
 80		respCh <- true
 81	}
 82
 83	s.sessionPermissionsMu.Lock()
 84	s.sessionPermissions = append(s.sessionPermissions, permission)
 85	s.sessionPermissionsMu.Unlock()
 86
 87	if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
 88		s.activeRequest = nil
 89	}
 90}
 91
 92func (s *permissionService) Grant(permission PermissionRequest) {
 93	s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
 94		ToolCallID: permission.ToolCallID,
 95		Granted:    true,
 96	})
 97	respCh, ok := s.pendingRequests.Get(permission.ID)
 98	if ok {
 99		respCh <- true
100	}
101
102	if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
103		s.activeRequest = nil
104	}
105}
106
107func (s *permissionService) Deny(permission PermissionRequest) {
108	s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
109		ToolCallID: permission.ToolCallID,
110		Granted:    false,
111		Denied:     true,
112	})
113	respCh, ok := s.pendingRequests.Get(permission.ID)
114	if ok {
115		respCh <- false
116	}
117
118	if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
119		s.activeRequest = nil
120	}
121}
122
123func (s *permissionService) Request(opts CreatePermissionRequest) bool {
124	if s.skip {
125		return true
126	}
127
128	// tell the UI that a permission was requested
129	s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
130		ToolCallID: opts.ToolCallID,
131	})
132	s.requestMu.Lock()
133	defer s.requestMu.Unlock()
134
135	// Check if the tool/action combination is in the allowlist
136	commandKey := opts.ToolName + ":" + opts.Action
137	if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
138		return true
139	}
140
141	s.autoApproveSessionsMu.RLock()
142	autoApprove := s.autoApproveSessions[opts.SessionID]
143	s.autoApproveSessionsMu.RUnlock()
144
145	if autoApprove {
146		return true
147	}
148
149	fileInfo, err := os.Stat(opts.Path)
150	dir := opts.Path
151	if err == nil {
152		if fileInfo.IsDir() {
153			dir = opts.Path
154		} else {
155			dir = filepath.Dir(opts.Path)
156		}
157	}
158
159	if dir == "." {
160		dir = s.workingDir
161	}
162	permission := PermissionRequest{
163		ID:          uuid.New().String(),
164		Path:        dir,
165		SessionID:   opts.SessionID,
166		ToolCallID:  opts.ToolCallID,
167		ToolName:    opts.ToolName,
168		Description: opts.Description,
169		Action:      opts.Action,
170		Params:      opts.Params,
171	}
172
173	s.sessionPermissionsMu.RLock()
174	for _, p := range s.sessionPermissions {
175		if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
176			s.sessionPermissionsMu.RUnlock()
177			return true
178		}
179	}
180	s.sessionPermissionsMu.RUnlock()
181
182	s.sessionPermissionsMu.RLock()
183	for _, p := range s.sessionPermissions {
184		if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
185			s.sessionPermissionsMu.RUnlock()
186			return true
187		}
188	}
189	s.sessionPermissionsMu.RUnlock()
190
191	s.activeRequest = &permission
192
193	respCh := make(chan bool, 1)
194	s.pendingRequests.Set(permission.ID, respCh)
195	defer s.pendingRequests.Del(permission.ID)
196
197	// Publish the request
198	s.Publish(pubsub.CreatedEvent, permission)
199
200	return <-respCh
201}
202
203func (s *permissionService) AutoApproveSession(sessionID string) {
204	s.autoApproveSessionsMu.Lock()
205	s.autoApproveSessions[sessionID] = true
206	s.autoApproveSessionsMu.Unlock()
207}
208
209func (s *permissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[PermissionNotification] {
210	return s.notificationBroker.Subscribe(ctx)
211}
212
213func NewPermissionService(workingDir string, skip bool, allowedTools []string) Service {
214	return &permissionService{
215		Broker:              pubsub.NewBroker[PermissionRequest](),
216		notificationBroker:  pubsub.NewBroker[PermissionNotification](),
217		workingDir:          workingDir,
218		sessionPermissions:  make([]PermissionRequest, 0),
219		autoApproveSessions: make(map[string]bool),
220		skip:                skip,
221		allowedTools:        allowedTools,
222		pendingRequests:     csync.NewMap[string, chan bool](),
223	}
224}