11 "github.com/charmbracelet/crush/internal/csync"
12 "github.com/charmbracelet/crush/internal/pubsub"
13 "github.com/google/uuid"
16var ErrorPermissionDenied = errors.New("user denied permission")
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"`
28type PermissionNotification struct {
29 ToolCallID string `json:"tool_call_id"`
30 Granted bool `json:"granted"`
31 Denied bool `json:"denied"`
34type PermissionRequest struct {
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"`
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 SetSkipRequests(skip bool)
54 SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[PermissionNotification]
57type permissionService struct {
58 *pubsub.Broker[PermissionRequest]
60 notificationBroker *pubsub.Broker[PermissionNotification]
62 sessionPermissions []PermissionRequest
63 sessionPermissionsMu sync.RWMutex
64 pendingRequests *csync.Map[string, chan bool]
65 autoApproveSessions map[string]bool
66 autoApproveSessionsMu sync.RWMutex
70 // used to make sure we only process one request at a time
72 activeRequest *PermissionRequest
75func (s *permissionService) GrantPersistent(permission PermissionRequest) {
76 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
77 ToolCallID: permission.ToolCallID,
80 respCh, ok := s.pendingRequests.Get(permission.ID)
85 s.sessionPermissionsMu.Lock()
86 s.sessionPermissions = append(s.sessionPermissions, permission)
87 s.sessionPermissionsMu.Unlock()
89 if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
94func (s *permissionService) Grant(permission PermissionRequest) {
95 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
96 ToolCallID: permission.ToolCallID,
99 respCh, ok := s.pendingRequests.Get(permission.ID)
104 if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
105 s.activeRequest = nil
109func (s *permissionService) Deny(permission PermissionRequest) {
110 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
111 ToolCallID: permission.ToolCallID,
115 respCh, ok := s.pendingRequests.Get(permission.ID)
120 if s.activeRequest != nil && s.activeRequest.ID == permission.ID {
121 s.activeRequest = nil
125func (s *permissionService) Request(opts CreatePermissionRequest) bool {
130 // tell the UI that a permission was requested
131 s.notificationBroker.Publish(pubsub.CreatedEvent, PermissionNotification{
132 ToolCallID: opts.ToolCallID,
135 defer s.requestMu.Unlock()
137 // Check if the tool/action combination is in the allowlist
138 commandKey := opts.ToolName + ":" + opts.Action
139 if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
143 s.autoApproveSessionsMu.RLock()
144 autoApprove := s.autoApproveSessions[opts.SessionID]
145 s.autoApproveSessionsMu.RUnlock()
151 fileInfo, err := os.Stat(opts.Path)
154 if fileInfo.IsDir() {
157 dir = filepath.Dir(opts.Path)
164 permission := PermissionRequest{
165 ID: uuid.New().String(),
167 SessionID: opts.SessionID,
168 ToolCallID: opts.ToolCallID,
169 ToolName: opts.ToolName,
170 Description: opts.Description,
175 s.sessionPermissionsMu.RLock()
176 for _, p := range s.sessionPermissions {
177 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
178 s.sessionPermissionsMu.RUnlock()
182 s.sessionPermissionsMu.RUnlock()
184 s.sessionPermissionsMu.RLock()
185 for _, p := range s.sessionPermissions {
186 if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
187 s.sessionPermissionsMu.RUnlock()
191 s.sessionPermissionsMu.RUnlock()
193 s.activeRequest = &permission
195 respCh := make(chan bool, 1)
196 s.pendingRequests.Set(permission.ID, respCh)
197 defer s.pendingRequests.Del(permission.ID)
199 // Publish the request
200 s.Publish(pubsub.CreatedEvent, permission)
205func (s *permissionService) AutoApproveSession(sessionID string) {
206 s.autoApproveSessionsMu.Lock()
207 s.autoApproveSessions[sessionID] = true
208 s.autoApproveSessionsMu.Unlock()
211func (s *permissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[PermissionNotification] {
212 return s.notificationBroker.Subscribe(ctx)
215func (s *permissionService) SetSkipRequests(skip bool) {
219func (s *permissionService) SkipRequests() bool {
223func NewPermissionService(workingDir string, skip bool, allowedTools []string) Service {
224 return &permissionService{
225 Broker: pubsub.NewBroker[PermissionRequest](),
226 notificationBroker: pubsub.NewBroker[PermissionNotification](),
227 workingDir: workingDir,
228 sessionPermissions: make([]PermissionRequest, 0),
229 autoApproveSessions: make(map[string]bool),
231 allowedTools: allowedTools,
232 pendingRequests: csync.NewMap[string, chan bool](),
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list