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