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	allowedTools          []string
 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	if s.skip {
 83		return true
 84	}
 85
 86	// Check if the tool/action combination is in the allowlist
 87	commandKey := opts.ToolName + ":" + opts.Action
 88	if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
 89		return true
 90	}
 91
 92	s.autoApproveSessionsMu.RLock()
 93	autoApprove := slices.Contains(s.autoApproveSessions, opts.SessionID)
 94	s.autoApproveSessionsMu.RUnlock()
 95
 96	if autoApprove {
 97		return true
 98	}
 99
100	dir := filepath.Dir(opts.Path)
101	if dir == "." {
102		dir = s.workingDir
103	}
104	permission := PermissionRequest{
105		ID:          uuid.New().String(),
106		Path:        dir,
107		SessionID:   opts.SessionID,
108		ToolName:    opts.ToolName,
109		Description: opts.Description,
110		Action:      opts.Action,
111		Params:      opts.Params,
112	}
113
114	s.sessionPermissionsMu.RLock()
115	for _, p := range s.sessionPermissions {
116		if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
117			s.sessionPermissionsMu.RUnlock()
118			return true
119		}
120	}
121	s.sessionPermissionsMu.RUnlock()
122
123	respCh := make(chan bool, 1)
124
125	s.pendingRequests.Store(permission.ID, respCh)
126	defer s.pendingRequests.Delete(permission.ID)
127
128	s.Publish(pubsub.CreatedEvent, permission)
129
130	// Wait for the response indefinitely
131	return <-respCh
132}
133
134func (s *permissionService) AutoApproveSession(sessionID string) {
135	s.autoApproveSessionsMu.Lock()
136	s.autoApproveSessions = append(s.autoApproveSessions, sessionID)
137	s.autoApproveSessionsMu.Unlock()
138}
139
140func NewPermissionService(workingDir string, skip bool, allowedTools []string) Service {
141	return &permissionService{
142		Broker:             pubsub.NewBroker[PermissionRequest](),
143		workingDir:         workingDir,
144		sessionPermissions: make([]PermissionRequest, 0),
145		skip:               skip,
146		allowedTools:       allowedTools,
147	}
148}