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}